Webchat Components Architecture

## Target Architecture

### Component Tree

```
<WebchatProvider clientId apiUrl userCredentials conversationId>
  Context {
    client: Client (raw HTTP client, not scoped)
    clientState, setClientState
    conversationId
    userCredentials (UserCredentials)
    error, setError
    emitter (full event emitter object)
    openConversation
    isTyping
    isAwaitingResponse
    setAwaitingResponse
  }

  // Hooks available inside provider (subscribe to stores + build scoped clients):
  // const { messages, participants, sendMessage, saveMessageFeedback, sendEvent, uploadFile, conversationId, status, on, error, isTyping, isAwaitingResponse } = useActiveConversation()
  // const { listConversations, openConversation } = useConversations()
  // const { getUser, updateUser } = useUser()
  //
  // Future: useConversations will include reactive history
  // const { listConversations, openConversation, history: { data, isLoading, error, refetch } } = useConversations()

  <Chat />
</WebchatProvider>
```

### Core Concepts

#### WebchatProvider

- Top-level provider component that **replaces the deprecated `useWebchat` hook**
- Props: `clientId`, `apiUrl`, `userCredentials`, `conversationId`
- **Only responsible for initialization**: creates client, starts conversation, sets up event handlers
- Context exposes connection state + the raw `Client` — but NOT a `ScopedClient`, NOT actions, NOT conversation data
- Does NOT contain rendering logic

**Migration from `useWebchat`:** The `useWebchat` hook is deprecated. Use `WebchatProvider` with the new hooks (`useActiveConversation`, `useConversations`, `useUser`) instead. See Migration Path section for details.

#### Context — What it provides

The context is purely connection state:

- `client` — the raw `Client` instance (not scoped). Hooks use this to implement actions.
- `clientState` — `'connecting' | 'connected' | 'error' | 'disconnected'`
- `setClientState` — function to update the client state
- `conversationId` — active conversation ID
- `userCredentials` — `UserCredentials` (`userId`, `userToken`)
- `error` — `WebchatError | undefined`
- `setError` — function to update the error state
- `emitter` — full event emitter object (includes `on`, `emit`, etc.)
- `openConversation` — opens/switches to a conversation (accepts object with `conversationId?` and `userToken?`, returns Promise)
- `isTyping` — boolean (set by event handlers in the provider)
- `isAwaitingResponse` — boolean (set by event handlers in the provider)
- `setAwaitingResponse` — function to update the awaiting response state

The context does **not** provide conversation data (`messages`, `participants`) or actions (`sendMessage`, `uploadFile`, etc.). Those belong in the hooks.

#### Hooks — Where data + actions live

Each hook reads `client`, `conversationId`, and `user` from context, subscribes to stores for reactive data, and implements its own scoped operations:

**Public Hooks (exported to consumers):**

- **`useActiveConversation()`** — `messages`, `participants`, `sendMessage`, `saveMessageFeedback`, `sendEvent`, `uploadFile`, `conversationId`, `status`, `on`, `error`, `isTyping`, `isAwaitingResponse` (operations on the current active conversation + connection state)
- **`useConversations()`** — `listConversations`, `openConversation` (conversation management and switching). Future: will include `history` for reactive conversation list.
- **`useUser()`** — `getUser`, `updateUser`

**Internal Hooks (NOT exported publicly):**

- **`useMessages()`** — message data and actions (used internally by `useWebchat` and `useActiveConversation`)
- **`useParticipants()`** — participant data (used internally by `useWebchat` and `useActiveConversation`)
- **`useEvent()`** — event subscription and emission (used internally by `useWebchat` and `useActiveConversation`)
- **`useFiles()`** — file upload functionality (used internally by `useWebchat` and `useActiveConversation`)
- **`useConversationList()`** — SWR-based conversation list with caching (used internally by `useConversations`). Returns `conversations`, `isLoading`, `error`, `refresh`.
- **`useInitialization()`** — initialization logic (used internally by `WebchatProvider`)

This separation means:

- Context is purely connection state (what came out of initialization)
- Hooks own all conversation data (via store subscriptions) and all actions (via raw client)
- Public hooks can be used independently inside the provider
- Hooks subscribe to stores directly — they don't go through context for conversation data
- During migration, `useActiveConversation` internally composes `useMessages`, `useParticipants`, `useEvent`, and `useFiles` to provide a unified API

#### Plugin System (Future)

- Not implemented yet, placeholder for future feature
- Idea: plugins as objects with event handler hooks using middleware pattern
- Each handler receives a `vanilla` callback (the default behavior)

```typescript
// Future API concept:
type Plugin = {
  onMessageCreated?: (vanilla: () => void) => void
}

const myPlugin: Plugin = {
  onMessageCreated: (vanilla) => {
    doSomething()
    vanilla()
  },
}
```

---

## Implementation Plan

### Context Shape

```typescript
type ClientStates = 'connecting' | 'connected' | 'error' | 'disconnected'

type WebchatContext = {
  // Raw HTTP client — hooks use this to implement actions
  client: Client | undefined

  // Connection state
  clientState: ClientStates
  setClientState: (state: ClientStates) => void
  conversationId: string | undefined
  userCredentials: UserCredentials | undefined
  error: WebchatError | undefined
  setError: (error?: WebchatError) => void

  // UI state (set by event handlers in the provider)
  isTyping: boolean
  isAwaitingResponse: boolean
  setAwaitingResponse: (state: boolean, timeout?: number) => void

  // Controls
  openConversation: (params: { conversationId?: string; userToken?: string }) => Promise<void>

  // Events
  emitter: ReturnType<typeof createEventEmitter<Events>>
}
```

### useActiveConversation Hook

```typescript
type UseActiveConversationReturn = {
  // Conversation ID
  conversationId?: string

  // Reactive state (subscribed to stores directly)
  messages: BlockMessage[]
  participants: User[]

  // Actions (business logic: HTTP queries + store mutations)
  sendMessage: (payload: IntegrationMessage['payload']) => Promise<void>
  saveMessageFeedback: (messageId: string, feedback: Feedback) => Promise<void>
  sendEvent: (
    event: Record<string, unknown>,
    options?: { bindConversation?: boolean; bindUser?: boolean }
  ) => Promise<void>
  uploadFile: (file: File) => Promise<{ fileUrl: string; name: string; type: FileType; fileId: string }>

  // Connection state
  status: ClientStates
  error?: WebchatError
  isTyping: boolean
  isAwaitingResponse: boolean

  // Event subscription
  on: ReturnType<typeof createEventEmitter<Events>>['on']
}
```

The hook internally:

1. Reads `client`, `conversationId`, `user` from context
2. **During migration**: composes internal hooks to provide unified API:
   - Calls `useMessages()` to get `messages`, `sendMessage`, `saveMessageFeedback`
   - Calls `useParticipants()` to get `participants`
   - Calls `useEvent()` to get `sendEvent`
   - Calls `useFiles()` to get `uploadFile`

**Note**: The internal hooks (`useMessages`, `useParticipants`, `useEvent`, `useFiles`) handle:

- Store subscriptions (e.g., `getUseMessagesStore(conversationId)` for reactive data)
- Business logic (e.g., optimistic updates with clientMessageId, nanoid in `sendMessage`)
- HTTP operations (e.g., presigned URL upload in `uploadFile`)

### useConversations Hook

```typescript
type UseConversationsReturn = {
  listConversations: () => Promise<ListConversationsResponse>
  openConversation: (conversationId?: string) => void
}
```

The hook internally:

1. Reads `client`, `openConversation`, `userCredentials` from context
2. Implements `listConversations` via the raw client
3. Exposes a wrapped `openConversation` from context for switching conversations

**Note**: This hook provides conversation management operations (listing, switching between conversations). Use `useActiveConversation` for working with the current conversation's messages and participants. The current conversation ID is available via `useActiveConversation().conversationId`.

### useUser Hook

```typescript
type UseUserReturn = {
  getUser: () => Promise<UserResponse>
  updateUser: (user: UserProfile) => Promise<User>
}
```

The hook internally:

1. Reads `client` from context
2. Implements `getUser` and `updateUser` via the raw client

### useEvent Hook (Internal - Migration)

```typescript
type UseEventProps = {
  client?: Client
  conversationId?: string
  user?: UserCredentials
}

type UseEventReturn = {
  sendEvent: (
    event: Record<string, unknown>,
    options?: { bindConversation?: boolean; bindUser?: boolean }
  ) => Promise<void>
}
```

The hook internally:

1. Accepts `client`, `conversationId`, `user` as props for composability
2. Implements `sendEvent` via the raw client (creates event with optional binding)

**Note**: This hook is NOT exported publicly. It's used internally by `useWebchat` (deprecated) and `useConversation` during the migration period. Consumers access event sending through `useConversation().sendEvent` and event subscription through context's `on`.

### useMessages Hook (Internal - Migration)

```typescript
type UseMessagesReturn = {
  messages: BlockMessage[]
  sendMessage: (payload: IntegrationMessage['payload']) => Promise<void>
  saveMessageFeedback: (messageId: string, feedback: Feedback) => Promise<void>
}
```

The hook internally:

1. Reads `client`, `conversationId`, `user` from context
2. Subscribes to `getUseMessagesStore(conversationId)` for reactive `messages`
3. Implements `sendMessage` business logic (optimistic updates with clientMessageId, nanoid)
4. Implements `saveMessageFeedback` (local-first update + HTTP call)

**Note**: This hook is NOT exported publicly. It's used internally by `useWebchat` (deprecated) and `useConversation` during the migration period. Consumers access messages through `useConversation().messages`. The method was renamed from `updateMessageFeedback` to `saveMessageFeedback` for consistency with store terminology.

### useParticipants Hook (Internal - Migration)

```typescript
type UseParticipantsReturn = {
  participants: User[]
}
```

The hook internally:

1. Reads `conversationId` from context
2. Subscribes to `getUseParticipantStore(conversationId)` for reactive `participants`

**Note**: This hook is NOT exported publicly. It's used internally by `useWebchat` (deprecated) and `useConversation` during the migration period. Consumers access participants through `useConversation().participants`.

### useFiles Hook (Internal - Migration)

```typescript
type UseFilesProps = {
  client?: Client
  conversationId?: string
  user?: UserCredentials
}

type UseFilesReturn = {
  uploadFile: (file: File) => Promise<{ fileUrl: string; name: string; type: FileType; fileId: string }>
}
```

The hook internally:

1. Accepts `client`, `conversationId`, `user` as props (not from context) for composability
2. Implements `uploadFile` (file buffer reading via `getFileBuffer` utility, presigned URL upload, etc.)

**Note**: This hook is NOT exported publicly. It's used internally by `useWebchat` (deprecated) and `useConversation` during the migration period. Consumers access file upload through `useConversation().uploadFile`.

**Implementation Detail**: Refactored to accept props instead of reading from context directly, making it properly composable within both `useWebchat` and `useConversation`. The `getFileBuffer` utility is extracted to `src/utils/file.ts` for reuse.

### Usage in WebchatProvider

```typescript
// Provider creates the raw client and manages stores directly
const [httpClient, setHttpClient] = useState<Client | undefined>(undefined)

// In openConversation (initialization):
const messagesStore = getUseMessagesStore(conversation.id)
messagesStore.getState().setMessages(blockMessages)

const participantsStore = getUseParticipantStore(conversation.id)
participantsStore.getState().setParticipants(participants)

// Event handlers access stores directly (no closure issues):
on('message_created', (ev) => {
  messagesStore.getState().saveMessage(integrationMessageToBlockMessage(ev))
})
on('participant_added', (ev) => {
  participantsStore.getState().addParticipant(ev.participant)
})

// Context value — purely connection state:
const contextValue = {
  client: httpClient,
  clientState,
  setClientState,
  conversationId: activeConversationId,
  userCredentials,
  error,
  setError,
  isTyping,
  isAwaitingResponse,
  setAwaitingResponse,
  openConversation,
  emitter,
}
```

### Usage by Consumers

```typescript
// Inside WebchatProvider — hooks read from context + stores:
const {
  conversationId,
  messages,
  participants,
  sendMessage,
  saveMessageFeedback,
  uploadFile,
  sendEvent,
  status,
  error,
  isTyping,
  isAwaitingResponse,
  on,
} = useActiveConversation()

const { listConversations, openConversation } = useConversations()
const { getUser, updateUser } = useUser()

// Example: Subscribe to events
on('message_created', (event) => {
  console.log('New message:', event)
})

// Example: Switch conversations
const response = await listConversations()
openConversation(response.conversations[0].id)

// Future: Reactive conversation history
// const { history, openConversation } = useConversations()
// if (history.isLoading) return <Spinner />
// history.data.map(conv => <ConversationItem key={conv.id} onClick={() => openConversation(conv.id)} />)
```

---

## Data Flow

1. `WebchatProvider` receives `clientId`, `apiUrl`, `userCredentials`, `conversationId`
2. Provider initializes connection via `initializeConversation`
3. Populates message and participant stores directly (imperative access)
4. Sets up event handlers that mutate stores directly (no closure issues)
5. Context exposes connection state + raw client to children
6. `useConversation` subscribes to stores for reactive data (`messages`, `participants`)
7. Hooks (`useActiveConversation`, `useConversations`, `useUser`) read `client` from context and implement actions, with `useActiveConversation` internally composing `useMessages`, `useParticipants`, `useEvent`, and `useFiles`
8. `<Chat />` and other components consume context + hooks

## Store Architecture

- **Message store**: per-conversation Zustand store (`getUseMessagesStore(convId)`)
  - Keyed by `messages-{conversationId}` (no custom storageKey prefix)
  - NOT persisted (in-memory only)
  - Has `saveMessage` for clientMessageId deduplication
- **Participant store**: per-conversation Zustand store (`getUseParticipantStore(convId)`)
  - Keyed by `participants-{conversationId}` (no custom storageKey prefix)
  - NOT persisted (in-memory only)
- **Composer file store**: per-conversation Zustand store (accessed via `useFiles` hook)
  - Stores files being composed in the message composer
  - Persisted to localStorage for persistence across page reloads
  - Managed internally by `useFiles` hook
- Stores populated imperatively by the provider (init + event handlers)
- Stores subscribed reactively by hooks (`useActiveConversation`) for React rendering

## Key Design Decisions

- **`useWebchat` is DEPRECATED** — migrate to `WebchatProvider` with `useActiveConversation`, `useConversations`, and `useUser` hooks
- Context is purely connection state — no conversation data, no actions
- Conversation data (`messages`, `participants`) lives in hooks via direct store subscriptions
- Actions live in hooks (`useActiveConversation`, `useConversations`, `useUser`), not in the context
- Context provides the raw `Client`, NOT a `ScopedClient` — hooks build scoped operations themselves
- Event handlers access stores directly via `store.getState()` (not through hook closures)
- Provider handles all initialization; child components are purely presentational
- Strict Mode double-mount handled with `cancelled` flag in useEffect
- Internal hooks (`useMessages`, `useParticipants`, `useEvent`, `useFiles`) are composed by public hooks to provide focused functionality
- Shared utilities extracted to `src/utils/` (e.g., `getFileBuffer`)
- Method naming follows store conventions: `saveMessageFeedback` (not `updateMessageFeedback` or `addMessageFeedback`)

## Migration Path

### ⚠️ Deprecation Notice

**`useWebchat` is deprecated.** Please migrate to the new provider-based architecture.

#### Before (Deprecated):

```typescript
import { useWebchat } from '@botpress/webchat'

function MyComponent() {
  const { messages, participants, client, sendMessage, listConversations } = useWebchat({ clientId, apiUrl, user })

  // Use the data...
}
```

#### After (Recommended):

```typescript
import { WebchatProvider, useActiveConversation, useConversations, useUser } from '@botpress/webchat'

function App() {
  return (
    <WebchatProvider clientId={clientId} apiUrl={apiUrl} user={user}>
      <MyComponent />
    </WebchatProvider>
  )
}

function MyComponent() {
  // Active conversation operations + connection state
  const {
    conversationId,
    messages,
    participants,
    sendMessage,
    uploadFile,
    status,
    error,
    isTyping,
    isAwaitingResponse,
    on
  } = useActiveConversation()

  // Conversation management
  const { listConversations, openConversation } = useConversations()

  // User operations
  const { getUser, updateUser } = useUser()

  // Use the data...
}
```

### Migration Details

- `useWebchat` → deprecated, initialization logic moves into `WebchatProvider`. During migration, `useWebchat` composes `useMessages`, `useParticipants`, and `useFiles` internally.
- `useMessages` → refactored as internal hook with message data + actions, NOT exported publicly
- `useParticipants` → refactored as internal hook with participant data, NOT exported publicly
- `useEvent` → created as internal hook for event operations, NOT exported publicly
- `useFiles` → created as internal hook for file upload, NOT exported publicly
- `useActiveConversation` → new public hook that composes `useMessages`, `useParticipants`, `useEvent`, and `useFiles` internally to provide unified API for the active conversation. Returns conversation data, actions, connection state, and event subscription.
- `useConversations` → new public hook for conversation management (listing, switching conversations)
- `useConversationList` → internal hook using SWR for cached conversation list (NOT exported publicly, used internally by `useConversations`)
- `ScopedClient` methods → split across `useActiveConversation` (messages, events, file uploads), `useConversations` (conversation management), and `useUser` (user operations)
- Public API exposes `useActiveConversation` (current conversation operations + state), `useConversations` (conversation management), and `useUser` (user operations)
- Internal hooks (useMessages, useParticipants, useEvent, useFiles, useInitialization, useConversationList) are NO LONGER exported publicly
- Stores remain unchanged (they are the source of truth)

**Internal Hook Composition Pattern:**
Both deprecated `useWebchat` and new `useActiveConversation` internally use:

- `useMessages()` for message data and actions
- `useParticipants()` for participant data
- `useEvent()` for event subscription and emission
- `useFiles()` for file uploads

This allows code reuse during migration while presenting a clean public API.

---

## Future Roadmap

### Planned API Changes

#### 1. Enhanced `useConversations` Hook

**Current State:**

```typescript
const { listConversations, openConversation } = useConversations()

// Manual conversation list management
const conversations = await listConversations()
```

**Future State:**
The `useConversations` hook will internally use `useConversationList` (which will remain internal) to provide reactive conversation history with SWR-based caching:

```typescript
const {
  // Actions
  listConversations, // One-off manual fetch
  openConversation, // Switch to a conversation

  // Reactive conversation history (SWR-powered)
  history: {
    data, // Conversations array (sorted, filtered, enriched)
    error, // WebchatError | undefined
    isLoading, // boolean
    refetch, // () => Promise<void>
  },
} = useConversations()
```

**Benefits:**

- **Single source of truth** - All conversation operations in one hook
- **Reactive by default** - Components automatically re-render when history changes
- **Grouped state** - Related data grouped under `history` namespace
- **Automatic caching** - SWR handles deduplication, revalidation, and cache management
- **Clean API** - `useConversationList` stays internal, reducing API surface

**Implementation:**

- `useConversationList` will remain an internal hook (not exported)
- `useConversations` will compose `useConversationList` internally
- The `history` object provides the SWR-powered reactive state
- Consumers use a single hook instead of two

**Example Usage:**

```typescript
function ConversationSwitcher() {
  const { history, openConversation } = useConversations()

  if (history.isLoading) return <Spinner />
  if (history.error) return <Error error={history.error} />

  return (
    <div>
      {history.data.map(conv => (
        <button key={conv.id} onClick={() => openConversation(conv.id)}>
          {conv.lastMessage.text}
        </button>
      ))}
      <button onClick={history.refetch}>Refresh</button>
    </div>
  )
}
```

#### 2. Consistent Reactive State Pattern Across All Hooks

All public hooks will adopt the same pattern of grouping reactive state into nested objects, similar to the `history` pattern:

**Examples:**

```typescript
// useActiveConversation - Future state grouping
const {
  // Current conversation
  conversationId,

  // Reactive message state
  messages: {
    data, // BlockMessage[]
    isLoading,
    error,
  },

  // Reactive participant state
  participants: {
    data, // User[]
    isLoading,
    error,
  },

  // Actions
  sendMessage,
  saveMessageFeedback,
  sendEvent,
  uploadFile,

  // Connection state
  status,
  on,
  isTyping,
  isAwaitingResponse,
} = useActiveConversation()

// useUser - Future state grouping
const {
  profile: {
    data, // UserProfile
    isLoading,
    error,
    refetch,
  },
  updateUser,
} = useUser()
```

**Benefits:**

- Consistent API patterns across all hooks
- Clear distinction between actions and reactive state
- Better TypeScript inference
- Easier to understand loading and error states

#### 3. Optional Override Functions

Public hooks will accept optional configuration to override default behavior:

```typescript
// Custom file upload implementation
const { uploadFile } = useActiveConversation({
  customUploadFile: async (file: File) => {
    // Custom upload logic (e.g., direct S3 upload, compression, etc.)
    const url = await myCustomUploadService(file)
    return { fileUrl: url, name: file.name, type: 'image', fileId: generateId() }
  },
})

// Custom message sending
const { sendMessage } = useActiveConversation({
  customSendMessage: async (payload) => {
    // Custom pre-processing, validation, etc.
    await validateMessage(payload)
    return defaultSendMessage(payload)
  },
})

// Custom conversation list fetching
const { history } = useConversations({
  customListConversations: async () => {
    // Custom filtering, sorting, or data source
    return await myCustomConversationSource()
  },
})
```

**Use Cases:**

- Custom file upload services (direct S3, Cloudinary, etc.)
- Message preprocessing/validation
- Custom data sources or caching strategies
- Analytics tracking on actions
- Rate limiting or throttling
- Custom error handling

**Implementation Pattern:**

```typescript
type UseActiveConversationOptions = {
  customUploadFile?: (file: File) => Promise<UploadResult>
  customSendMessage?: (payload: MessagePayload) => Promise<void>
  onError?: (error: WebchatError) => void
}

function useActiveConversation(options?: UseActiveConversationOptions) {
  // Use custom implementations when provided, fall back to defaults
  const uploadFile = options?.customUploadFile ?? defaultUploadFile
  // ...
}
```

#### 4. Internal Hooks

The following hooks will remain internal (not exported publicly):

- `useMessages` - Message data and actions
- `useParticipants` - Participant data
- `useEvent` - Event operations
- `useFiles` - File upload functionality
- `useInitialization` - Provider initialization logic
- **`useConversationList`** - SWR-based conversation list (used by `useConversations`)

**Rationale:**

- Reduces API surface area
- Encourages using the high-level, composed hooks
- Allows internal refactoring without breaking changes
- Provides clear migration path for consumers

---

## Files

### Created

- ✅ `src/providers/WebchatProvider.tsx` — provider component
- ✅ `src/hooks/useActiveConversation.ts` — active conversation data + actions hook (composes `useMessages`, `useParticipants`, `useEvent`, `useFiles` internally)
- ✅ `src/hooks/useConversations.ts` — conversation management hook (list, switch conversations)
- ✅ `src/hooks/useUser.ts` — user actions hook
- ✅ `src/hooks/useEvent.ts` — internal event hook (NOT exported publicly, used by `useWebchat` and `useActiveConversation`)
- ✅ `src/hooks/useFiles.ts` — internal file upload hook (NOT exported publicly, used by `useWebchat` and `useActiveConversation`)
- ✅ `src/utils/file.ts` — file utilities (`getFileBuffer`)

### Modified

- ✅ `src/hooks/useWebchat.ts` — deprecated, refactored to compose `useMessages`, `useParticipants`, `useEvent`, `useFiles` internally
- ✅ `src/hooks/useMessages.ts` — refactored to internal implementation (NOT exported publicly), renamed `updateMessageFeedback` → `saveMessageFeedback`
- ✅ `src/hooks/useParticipants.ts` — refactored to internal implementation (NOT exported publicly)
- ✅ `src/hooks/useFiles.ts` — refactored to accept props for composability (no longer reads from context directly)
- ✅ `src/hooks/index.ts` — exports public hooks (`useActiveConversation`, `useConversations`, `useUser`) only; internal hooks (useMessages, useParticipants, useEvent, useFiles, useInitialization, useConversationList) are no longer exported
- ✅ `src/providers/index.ts` — exports `WebchatProvider`
- ✅ `src/index.ts` — exports new public API (`useActiveConversation`, `useConversations`, `useUser`, `useWebchat`)
- ✅ `src/types/client.ts` — exports `UserResponse`, `UserProfile`, `User` types
- ✅ `src/utils/index.ts` — re-exports file utilities

### Deleted

- ✅ `src/utils/uploadFile.ts` — functionality moved into `useFiles` hook
