@botpress/webchat 4.3.2 → 4.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,737 @@
1
+ Webchat Components Architecture
2
+
3
+ ## Target Architecture
4
+
5
+ ### Component Tree
6
+
7
+ ```
8
+ <WebchatProvider clientId apiUrl userCredentials conversationId>
9
+ Context {
10
+ client: Client (raw HTTP client, not scoped)
11
+ clientState, setClientState
12
+ conversationId
13
+ userCredentials (UserCredentials)
14
+ error, setError
15
+ emitter (full event emitter object)
16
+ openConversation
17
+ isTyping
18
+ isAwaitingResponse
19
+ setAwaitingResponse
20
+ }
21
+
22
+ // Hooks available inside provider (subscribe to stores + build scoped clients):
23
+ // const { messages, participants, sendMessage, saveMessageFeedback, sendEvent, uploadFile, conversationId, status, on, error, isTyping, isAwaitingResponse } = useActiveConversation()
24
+ // const { listConversations, openConversation } = useConversations()
25
+ // const { getUser, updateUser } = useUser()
26
+ //
27
+ // Future: useConversations will include reactive history
28
+ // const { listConversations, openConversation, history: { data, isLoading, error, refetch } } = useConversations()
29
+
30
+ <Chat />
31
+ </WebchatProvider>
32
+ ```
33
+
34
+ ### Core Concepts
35
+
36
+ #### WebchatProvider
37
+
38
+ - Top-level provider component that **replaces the deprecated `useWebchat` hook**
39
+ - Props: `clientId`, `apiUrl`, `userCredentials`, `conversationId`
40
+ - **Only responsible for initialization**: creates client, starts conversation, sets up event handlers
41
+ - Context exposes connection state + the raw `Client` — but NOT a `ScopedClient`, NOT actions, NOT conversation data
42
+ - Does NOT contain rendering logic
43
+
44
+ **Migration from `useWebchat`:** The `useWebchat` hook is deprecated. Use `WebchatProvider` with the new hooks (`useActiveConversation`, `useConversations`, `useUser`) instead. See Migration Path section for details.
45
+
46
+ #### Context — What it provides
47
+
48
+ The context is purely connection state:
49
+
50
+ - `client` — the raw `Client` instance (not scoped). Hooks use this to implement actions.
51
+ - `clientState` — `'connecting' | 'connected' | 'error' | 'disconnected'`
52
+ - `setClientState` — function to update the client state
53
+ - `conversationId` — active conversation ID
54
+ - `userCredentials` — `UserCredentials` (`userId`, `userToken`)
55
+ - `error` — `WebchatError | undefined`
56
+ - `setError` — function to update the error state
57
+ - `emitter` — full event emitter object (includes `on`, `emit`, etc.)
58
+ - `openConversation` — opens/switches to a conversation (accepts object with `conversationId?` and `userToken?`, returns Promise)
59
+ - `isTyping` — boolean (set by event handlers in the provider)
60
+ - `isAwaitingResponse` — boolean (set by event handlers in the provider)
61
+ - `setAwaitingResponse` — function to update the awaiting response state
62
+
63
+ The context does **not** provide conversation data (`messages`, `participants`) or actions (`sendMessage`, `uploadFile`, etc.). Those belong in the hooks.
64
+
65
+ #### Hooks — Where data + actions live
66
+
67
+ Each hook reads `client`, `conversationId`, and `user` from context, subscribes to stores for reactive data, and implements its own scoped operations:
68
+
69
+ **Public Hooks (exported to consumers):**
70
+
71
+ - **`useActiveConversation()`** — `messages`, `participants`, `sendMessage`, `saveMessageFeedback`, `sendEvent`, `uploadFile`, `conversationId`, `status`, `on`, `error`, `isTyping`, `isAwaitingResponse` (operations on the current active conversation + connection state)
72
+ - **`useConversations()`** — `listConversations`, `openConversation` (conversation management and switching). Future: will include `history` for reactive conversation list.
73
+ - **`useUser()`** — `getUser`, `updateUser`
74
+
75
+ **Internal Hooks (NOT exported publicly):**
76
+
77
+ - **`useMessages()`** — message data and actions (used internally by `useWebchat` and `useActiveConversation`)
78
+ - **`useParticipants()`** — participant data (used internally by `useWebchat` and `useActiveConversation`)
79
+ - **`useEvent()`** — event subscription and emission (used internally by `useWebchat` and `useActiveConversation`)
80
+ - **`useFiles()`** — file upload functionality (used internally by `useWebchat` and `useActiveConversation`)
81
+ - **`useConversationList()`** — SWR-based conversation list with caching (used internally by `useConversations`). Returns `conversations`, `isLoading`, `error`, `refresh`.
82
+ - **`useInitialization()`** — initialization logic (used internally by `WebchatProvider`)
83
+
84
+ This separation means:
85
+
86
+ - Context is purely connection state (what came out of initialization)
87
+ - Hooks own all conversation data (via store subscriptions) and all actions (via raw client)
88
+ - Public hooks can be used independently inside the provider
89
+ - Hooks subscribe to stores directly — they don't go through context for conversation data
90
+ - During migration, `useActiveConversation` internally composes `useMessages`, `useParticipants`, `useEvent`, and `useFiles` to provide a unified API
91
+
92
+ #### Plugin System (Future)
93
+
94
+ - Not implemented yet, placeholder for future feature
95
+ - Idea: plugins as objects with event handler hooks using middleware pattern
96
+ - Each handler receives a `vanilla` callback (the default behavior)
97
+
98
+ ```typescript
99
+ // Future API concept:
100
+ type Plugin = {
101
+ onMessageCreated?: (vanilla: () => void) => void
102
+ }
103
+
104
+ const myPlugin: Plugin = {
105
+ onMessageCreated: (vanilla) => {
106
+ doSomething()
107
+ vanilla()
108
+ },
109
+ }
110
+ ```
111
+
112
+ ---
113
+
114
+ ## Implementation Plan
115
+
116
+ ### Context Shape
117
+
118
+ ```typescript
119
+ type ClientStates = 'connecting' | 'connected' | 'error' | 'disconnected'
120
+
121
+ type WebchatContext = {
122
+ // Raw HTTP client — hooks use this to implement actions
123
+ client: Client | undefined
124
+
125
+ // Connection state
126
+ clientState: ClientStates
127
+ setClientState: (state: ClientStates) => void
128
+ conversationId: string | undefined
129
+ userCredentials: UserCredentials | undefined
130
+ error: WebchatError | undefined
131
+ setError: (error?: WebchatError) => void
132
+
133
+ // UI state (set by event handlers in the provider)
134
+ isTyping: boolean
135
+ isAwaitingResponse: boolean
136
+ setAwaitingResponse: (state: boolean, timeout?: number) => void
137
+
138
+ // Controls
139
+ openConversation: (params: { conversationId?: string; userToken?: string }) => Promise<void>
140
+
141
+ // Events
142
+ emitter: ReturnType<typeof createEventEmitter<Events>>
143
+ }
144
+ ```
145
+
146
+ ### useActiveConversation Hook
147
+
148
+ ```typescript
149
+ type UseActiveConversationReturn = {
150
+ // Conversation ID
151
+ conversationId?: string
152
+
153
+ // Reactive state (subscribed to stores directly)
154
+ messages: BlockMessage[]
155
+ participants: User[]
156
+
157
+ // Actions (business logic: HTTP queries + store mutations)
158
+ sendMessage: (payload: IntegrationMessage['payload']) => Promise<void>
159
+ saveMessageFeedback: (messageId: string, feedback: Feedback) => Promise<void>
160
+ sendEvent: (
161
+ event: Record<string, unknown>,
162
+ options?: { bindConversation?: boolean; bindUser?: boolean }
163
+ ) => Promise<void>
164
+ uploadFile: (file: File) => Promise<{ fileUrl: string; name: string; type: FileType; fileId: string }>
165
+
166
+ // Connection state
167
+ status: ClientStates
168
+ error?: WebchatError
169
+ isTyping: boolean
170
+ isAwaitingResponse: boolean
171
+
172
+ // Event subscription
173
+ on: ReturnType<typeof createEventEmitter<Events>>['on']
174
+ }
175
+ ```
176
+
177
+ The hook internally:
178
+
179
+ 1. Reads `client`, `conversationId`, `user` from context
180
+ 2. **During migration**: composes internal hooks to provide unified API:
181
+ - Calls `useMessages()` to get `messages`, `sendMessage`, `saveMessageFeedback`
182
+ - Calls `useParticipants()` to get `participants`
183
+ - Calls `useEvent()` to get `sendEvent`
184
+ - Calls `useFiles()` to get `uploadFile`
185
+
186
+ **Note**: The internal hooks (`useMessages`, `useParticipants`, `useEvent`, `useFiles`) handle:
187
+
188
+ - Store subscriptions (e.g., `getUseMessagesStore(conversationId)` for reactive data)
189
+ - Business logic (e.g., optimistic updates with clientMessageId, nanoid in `sendMessage`)
190
+ - HTTP operations (e.g., presigned URL upload in `uploadFile`)
191
+
192
+ ### useConversations Hook
193
+
194
+ ```typescript
195
+ type UseConversationsReturn = {
196
+ listConversations: () => Promise<ListConversationsResponse>
197
+ openConversation: (conversationId?: string) => void
198
+ }
199
+ ```
200
+
201
+ The hook internally:
202
+
203
+ 1. Reads `client`, `openConversation`, `userCredentials` from context
204
+ 2. Implements `listConversations` via the raw client
205
+ 3. Exposes a wrapped `openConversation` from context for switching conversations
206
+
207
+ **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`.
208
+
209
+ ### useUser Hook
210
+
211
+ ```typescript
212
+ type UseUserReturn = {
213
+ getUser: () => Promise<UserResponse>
214
+ updateUser: (user: UserProfile) => Promise<User>
215
+ }
216
+ ```
217
+
218
+ The hook internally:
219
+
220
+ 1. Reads `client` from context
221
+ 2. Implements `getUser` and `updateUser` via the raw client
222
+
223
+ ### useEvent Hook (Internal - Migration)
224
+
225
+ ```typescript
226
+ type UseEventProps = {
227
+ client?: Client
228
+ conversationId?: string
229
+ user?: UserCredentials
230
+ }
231
+
232
+ type UseEventReturn = {
233
+ sendEvent: (
234
+ event: Record<string, unknown>,
235
+ options?: { bindConversation?: boolean; bindUser?: boolean }
236
+ ) => Promise<void>
237
+ }
238
+ ```
239
+
240
+ The hook internally:
241
+
242
+ 1. Accepts `client`, `conversationId`, `user` as props for composability
243
+ 2. Implements `sendEvent` via the raw client (creates event with optional binding)
244
+
245
+ **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`.
246
+
247
+ ### useMessages Hook (Internal - Migration)
248
+
249
+ ```typescript
250
+ type UseMessagesReturn = {
251
+ messages: BlockMessage[]
252
+ sendMessage: (payload: IntegrationMessage['payload']) => Promise<void>
253
+ saveMessageFeedback: (messageId: string, feedback: Feedback) => Promise<void>
254
+ }
255
+ ```
256
+
257
+ The hook internally:
258
+
259
+ 1. Reads `client`, `conversationId`, `user` from context
260
+ 2. Subscribes to `getUseMessagesStore(conversationId)` for reactive `messages`
261
+ 3. Implements `sendMessage` business logic (optimistic updates with clientMessageId, nanoid)
262
+ 4. Implements `saveMessageFeedback` (local-first update + HTTP call)
263
+
264
+ **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.
265
+
266
+ ### useParticipants Hook (Internal - Migration)
267
+
268
+ ```typescript
269
+ type UseParticipantsReturn = {
270
+ participants: User[]
271
+ }
272
+ ```
273
+
274
+ The hook internally:
275
+
276
+ 1. Reads `conversationId` from context
277
+ 2. Subscribes to `getUseParticipantStore(conversationId)` for reactive `participants`
278
+
279
+ **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`.
280
+
281
+ ### useFiles Hook (Internal - Migration)
282
+
283
+ ```typescript
284
+ type UseFilesProps = {
285
+ client?: Client
286
+ conversationId?: string
287
+ user?: UserCredentials
288
+ }
289
+
290
+ type UseFilesReturn = {
291
+ uploadFile: (file: File) => Promise<{ fileUrl: string; name: string; type: FileType; fileId: string }>
292
+ }
293
+ ```
294
+
295
+ The hook internally:
296
+
297
+ 1. Accepts `client`, `conversationId`, `user` as props (not from context) for composability
298
+ 2. Implements `uploadFile` (file buffer reading via `getFileBuffer` utility, presigned URL upload, etc.)
299
+
300
+ **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`.
301
+
302
+ **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.
303
+
304
+ ### Usage in WebchatProvider
305
+
306
+ ```typescript
307
+ // Provider creates the raw client and manages stores directly
308
+ const [httpClient, setHttpClient] = useState<Client | undefined>(undefined)
309
+
310
+ // In openConversation (initialization):
311
+ const messagesStore = getUseMessagesStore(conversation.id)
312
+ messagesStore.getState().setMessages(blockMessages)
313
+
314
+ const participantsStore = getUseParticipantStore(conversation.id)
315
+ participantsStore.getState().setParticipants(participants)
316
+
317
+ // Event handlers access stores directly (no closure issues):
318
+ on('message_created', (ev) => {
319
+ messagesStore.getState().saveMessage(integrationMessageToBlockMessage(ev))
320
+ })
321
+ on('participant_added', (ev) => {
322
+ participantsStore.getState().addParticipant(ev.participant)
323
+ })
324
+
325
+ // Context value — purely connection state:
326
+ const contextValue = {
327
+ client: httpClient,
328
+ clientState,
329
+ setClientState,
330
+ conversationId: activeConversationId,
331
+ userCredentials,
332
+ error,
333
+ setError,
334
+ isTyping,
335
+ isAwaitingResponse,
336
+ setAwaitingResponse,
337
+ openConversation,
338
+ emitter,
339
+ }
340
+ ```
341
+
342
+ ### Usage by Consumers
343
+
344
+ ```typescript
345
+ // Inside WebchatProvider — hooks read from context + stores:
346
+ const {
347
+ conversationId,
348
+ messages,
349
+ participants,
350
+ sendMessage,
351
+ saveMessageFeedback,
352
+ uploadFile,
353
+ sendEvent,
354
+ status,
355
+ error,
356
+ isTyping,
357
+ isAwaitingResponse,
358
+ on,
359
+ } = useActiveConversation()
360
+
361
+ const { listConversations, openConversation } = useConversations()
362
+ const { getUser, updateUser } = useUser()
363
+
364
+ // Example: Subscribe to events
365
+ on('message_created', (event) => {
366
+ console.log('New message:', event)
367
+ })
368
+
369
+ // Example: Switch conversations
370
+ const response = await listConversations()
371
+ openConversation(response.conversations[0].id)
372
+
373
+ // Future: Reactive conversation history
374
+ // const { history, openConversation } = useConversations()
375
+ // if (history.isLoading) return <Spinner />
376
+ // history.data.map(conv => <ConversationItem key={conv.id} onClick={() => openConversation(conv.id)} />)
377
+ ```
378
+
379
+ ---
380
+
381
+ ## Data Flow
382
+
383
+ 1. `WebchatProvider` receives `clientId`, `apiUrl`, `userCredentials`, `conversationId`
384
+ 2. Provider initializes connection via `initializeConversation`
385
+ 3. Populates message and participant stores directly (imperative access)
386
+ 4. Sets up event handlers that mutate stores directly (no closure issues)
387
+ 5. Context exposes connection state + raw client to children
388
+ 6. `useConversation` subscribes to stores for reactive data (`messages`, `participants`)
389
+ 7. Hooks (`useActiveConversation`, `useConversations`, `useUser`) read `client` from context and implement actions, with `useActiveConversation` internally composing `useMessages`, `useParticipants`, `useEvent`, and `useFiles`
390
+ 8. `<Chat />` and other components consume context + hooks
391
+
392
+ ## Store Architecture
393
+
394
+ - **Message store**: per-conversation Zustand store (`getUseMessagesStore(convId)`)
395
+ - Keyed by `messages-{conversationId}` (no custom storageKey prefix)
396
+ - NOT persisted (in-memory only)
397
+ - Has `saveMessage` for clientMessageId deduplication
398
+ - **Participant store**: per-conversation Zustand store (`getUseParticipantStore(convId)`)
399
+ - Keyed by `participants-{conversationId}` (no custom storageKey prefix)
400
+ - NOT persisted (in-memory only)
401
+ - **Composer file store**: per-conversation Zustand store (accessed via `useFiles` hook)
402
+ - Stores files being composed in the message composer
403
+ - Persisted to localStorage for persistence across page reloads
404
+ - Managed internally by `useFiles` hook
405
+ - Stores populated imperatively by the provider (init + event handlers)
406
+ - Stores subscribed reactively by hooks (`useActiveConversation`) for React rendering
407
+
408
+ ## Key Design Decisions
409
+
410
+ - **`useWebchat` is DEPRECATED** — migrate to `WebchatProvider` with `useActiveConversation`, `useConversations`, and `useUser` hooks
411
+ - Context is purely connection state — no conversation data, no actions
412
+ - Conversation data (`messages`, `participants`) lives in hooks via direct store subscriptions
413
+ - Actions live in hooks (`useActiveConversation`, `useConversations`, `useUser`), not in the context
414
+ - Context provides the raw `Client`, NOT a `ScopedClient` — hooks build scoped operations themselves
415
+ - Event handlers access stores directly via `store.getState()` (not through hook closures)
416
+ - Provider handles all initialization; child components are purely presentational
417
+ - Strict Mode double-mount handled with `cancelled` flag in useEffect
418
+ - Internal hooks (`useMessages`, `useParticipants`, `useEvent`, `useFiles`) are composed by public hooks to provide focused functionality
419
+ - Shared utilities extracted to `src/utils/` (e.g., `getFileBuffer`)
420
+ - Method naming follows store conventions: `saveMessageFeedback` (not `updateMessageFeedback` or `addMessageFeedback`)
421
+
422
+ ## Migration Path
423
+
424
+ ### ⚠️ Deprecation Notice
425
+
426
+ **`useWebchat` is deprecated.** Please migrate to the new provider-based architecture.
427
+
428
+ #### Before (Deprecated):
429
+
430
+ ```typescript
431
+ import { useWebchat } from '@botpress/webchat'
432
+
433
+ function MyComponent() {
434
+ const { messages, participants, client, sendMessage, listConversations } = useWebchat({ clientId, apiUrl, user })
435
+
436
+ // Use the data...
437
+ }
438
+ ```
439
+
440
+ #### After (Recommended):
441
+
442
+ ```typescript
443
+ import { WebchatProvider, useActiveConversation, useConversations, useUser } from '@botpress/webchat'
444
+
445
+ function App() {
446
+ return (
447
+ <WebchatProvider clientId={clientId} apiUrl={apiUrl} user={user}>
448
+ <MyComponent />
449
+ </WebchatProvider>
450
+ )
451
+ }
452
+
453
+ function MyComponent() {
454
+ // Active conversation operations + connection state
455
+ const {
456
+ conversationId,
457
+ messages,
458
+ participants,
459
+ sendMessage,
460
+ uploadFile,
461
+ status,
462
+ error,
463
+ isTyping,
464
+ isAwaitingResponse,
465
+ on
466
+ } = useActiveConversation()
467
+
468
+ // Conversation management
469
+ const { listConversations, openConversation } = useConversations()
470
+
471
+ // User operations
472
+ const { getUser, updateUser } = useUser()
473
+
474
+ // Use the data...
475
+ }
476
+ ```
477
+
478
+ ### Migration Details
479
+
480
+ - `useWebchat` → deprecated, initialization logic moves into `WebchatProvider`. During migration, `useWebchat` composes `useMessages`, `useParticipants`, and `useFiles` internally.
481
+ - `useMessages` → refactored as internal hook with message data + actions, NOT exported publicly
482
+ - `useParticipants` → refactored as internal hook with participant data, NOT exported publicly
483
+ - `useEvent` → created as internal hook for event operations, NOT exported publicly
484
+ - `useFiles` → created as internal hook for file upload, NOT exported publicly
485
+ - `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.
486
+ - `useConversations` → new public hook for conversation management (listing, switching conversations)
487
+ - `useConversationList` → internal hook using SWR for cached conversation list (NOT exported publicly, used internally by `useConversations`)
488
+ - `ScopedClient` methods → split across `useActiveConversation` (messages, events, file uploads), `useConversations` (conversation management), and `useUser` (user operations)
489
+ - Public API exposes `useActiveConversation` (current conversation operations + state), `useConversations` (conversation management), and `useUser` (user operations)
490
+ - Internal hooks (useMessages, useParticipants, useEvent, useFiles, useInitialization, useConversationList) are NO LONGER exported publicly
491
+ - Stores remain unchanged (they are the source of truth)
492
+
493
+ **Internal Hook Composition Pattern:**
494
+ Both deprecated `useWebchat` and new `useActiveConversation` internally use:
495
+
496
+ - `useMessages()` for message data and actions
497
+ - `useParticipants()` for participant data
498
+ - `useEvent()` for event subscription and emission
499
+ - `useFiles()` for file uploads
500
+
501
+ This allows code reuse during migration while presenting a clean public API.
502
+
503
+ ---
504
+
505
+ ## Future Roadmap
506
+
507
+ ### Planned API Changes
508
+
509
+ #### 1. Enhanced `useConversations` Hook
510
+
511
+ **Current State:**
512
+
513
+ ```typescript
514
+ const { listConversations, openConversation } = useConversations()
515
+
516
+ // Manual conversation list management
517
+ const conversations = await listConversations()
518
+ ```
519
+
520
+ **Future State:**
521
+ The `useConversations` hook will internally use `useConversationList` (which will remain internal) to provide reactive conversation history with SWR-based caching:
522
+
523
+ ```typescript
524
+ const {
525
+ // Actions
526
+ listConversations, // One-off manual fetch
527
+ openConversation, // Switch to a conversation
528
+
529
+ // Reactive conversation history (SWR-powered)
530
+ history: {
531
+ data, // Conversations array (sorted, filtered, enriched)
532
+ error, // WebchatError | undefined
533
+ isLoading, // boolean
534
+ refetch, // () => Promise<void>
535
+ },
536
+ } = useConversations()
537
+ ```
538
+
539
+ **Benefits:**
540
+
541
+ - **Single source of truth** - All conversation operations in one hook
542
+ - **Reactive by default** - Components automatically re-render when history changes
543
+ - **Grouped state** - Related data grouped under `history` namespace
544
+ - **Automatic caching** - SWR handles deduplication, revalidation, and cache management
545
+ - **Clean API** - `useConversationList` stays internal, reducing API surface
546
+
547
+ **Implementation:**
548
+
549
+ - `useConversationList` will remain an internal hook (not exported)
550
+ - `useConversations` will compose `useConversationList` internally
551
+ - The `history` object provides the SWR-powered reactive state
552
+ - Consumers use a single hook instead of two
553
+
554
+ **Example Usage:**
555
+
556
+ ```typescript
557
+ function ConversationSwitcher() {
558
+ const { history, openConversation } = useConversations()
559
+
560
+ if (history.isLoading) return <Spinner />
561
+ if (history.error) return <Error error={history.error} />
562
+
563
+ return (
564
+ <div>
565
+ {history.data.map(conv => (
566
+ <button key={conv.id} onClick={() => openConversation(conv.id)}>
567
+ {conv.lastMessage.text}
568
+ </button>
569
+ ))}
570
+ <button onClick={history.refetch}>Refresh</button>
571
+ </div>
572
+ )
573
+ }
574
+ ```
575
+
576
+ #### 2. Consistent Reactive State Pattern Across All Hooks
577
+
578
+ All public hooks will adopt the same pattern of grouping reactive state into nested objects, similar to the `history` pattern:
579
+
580
+ **Examples:**
581
+
582
+ ```typescript
583
+ // useActiveConversation - Future state grouping
584
+ const {
585
+ // Current conversation
586
+ conversationId,
587
+
588
+ // Reactive message state
589
+ messages: {
590
+ data, // BlockMessage[]
591
+ isLoading,
592
+ error,
593
+ },
594
+
595
+ // Reactive participant state
596
+ participants: {
597
+ data, // User[]
598
+ isLoading,
599
+ error,
600
+ },
601
+
602
+ // Actions
603
+ sendMessage,
604
+ saveMessageFeedback,
605
+ sendEvent,
606
+ uploadFile,
607
+
608
+ // Connection state
609
+ status,
610
+ on,
611
+ isTyping,
612
+ isAwaitingResponse,
613
+ } = useActiveConversation()
614
+
615
+ // useUser - Future state grouping
616
+ const {
617
+ profile: {
618
+ data, // UserProfile
619
+ isLoading,
620
+ error,
621
+ refetch,
622
+ },
623
+ updateUser,
624
+ } = useUser()
625
+ ```
626
+
627
+ **Benefits:**
628
+
629
+ - Consistent API patterns across all hooks
630
+ - Clear distinction between actions and reactive state
631
+ - Better TypeScript inference
632
+ - Easier to understand loading and error states
633
+
634
+ #### 3. Optional Override Functions
635
+
636
+ Public hooks will accept optional configuration to override default behavior:
637
+
638
+ ```typescript
639
+ // Custom file upload implementation
640
+ const { uploadFile } = useActiveConversation({
641
+ customUploadFile: async (file: File) => {
642
+ // Custom upload logic (e.g., direct S3 upload, compression, etc.)
643
+ const url = await myCustomUploadService(file)
644
+ return { fileUrl: url, name: file.name, type: 'image', fileId: generateId() }
645
+ },
646
+ })
647
+
648
+ // Custom message sending
649
+ const { sendMessage } = useActiveConversation({
650
+ customSendMessage: async (payload) => {
651
+ // Custom pre-processing, validation, etc.
652
+ await validateMessage(payload)
653
+ return defaultSendMessage(payload)
654
+ },
655
+ })
656
+
657
+ // Custom conversation list fetching
658
+ const { history } = useConversations({
659
+ customListConversations: async () => {
660
+ // Custom filtering, sorting, or data source
661
+ return await myCustomConversationSource()
662
+ },
663
+ })
664
+ ```
665
+
666
+ **Use Cases:**
667
+
668
+ - Custom file upload services (direct S3, Cloudinary, etc.)
669
+ - Message preprocessing/validation
670
+ - Custom data sources or caching strategies
671
+ - Analytics tracking on actions
672
+ - Rate limiting or throttling
673
+ - Custom error handling
674
+
675
+ **Implementation Pattern:**
676
+
677
+ ```typescript
678
+ type UseActiveConversationOptions = {
679
+ customUploadFile?: (file: File) => Promise<UploadResult>
680
+ customSendMessage?: (payload: MessagePayload) => Promise<void>
681
+ onError?: (error: WebchatError) => void
682
+ }
683
+
684
+ function useActiveConversation(options?: UseActiveConversationOptions) {
685
+ // Use custom implementations when provided, fall back to defaults
686
+ const uploadFile = options?.customUploadFile ?? defaultUploadFile
687
+ // ...
688
+ }
689
+ ```
690
+
691
+ #### 4. Internal Hooks
692
+
693
+ The following hooks will remain internal (not exported publicly):
694
+
695
+ - `useMessages` - Message data and actions
696
+ - `useParticipants` - Participant data
697
+ - `useEvent` - Event operations
698
+ - `useFiles` - File upload functionality
699
+ - `useInitialization` - Provider initialization logic
700
+ - **`useConversationList`** - SWR-based conversation list (used by `useConversations`)
701
+
702
+ **Rationale:**
703
+
704
+ - Reduces API surface area
705
+ - Encourages using the high-level, composed hooks
706
+ - Allows internal refactoring without breaking changes
707
+ - Provides clear migration path for consumers
708
+
709
+ ---
710
+
711
+ ## Files
712
+
713
+ ### Created
714
+
715
+ - ✅ `src/providers/WebchatProvider.tsx` — provider component
716
+ - ✅ `src/hooks/useActiveConversation.ts` — active conversation data + actions hook (composes `useMessages`, `useParticipants`, `useEvent`, `useFiles` internally)
717
+ - ✅ `src/hooks/useConversations.ts` — conversation management hook (list, switch conversations)
718
+ - ✅ `src/hooks/useUser.ts` — user actions hook
719
+ - ✅ `src/hooks/useEvent.ts` — internal event hook (NOT exported publicly, used by `useWebchat` and `useActiveConversation`)
720
+ - ✅ `src/hooks/useFiles.ts` — internal file upload hook (NOT exported publicly, used by `useWebchat` and `useActiveConversation`)
721
+ - ✅ `src/utils/file.ts` — file utilities (`getFileBuffer`)
722
+
723
+ ### Modified
724
+
725
+ - ✅ `src/hooks/useWebchat.ts` — deprecated, refactored to compose `useMessages`, `useParticipants`, `useEvent`, `useFiles` internally
726
+ - ✅ `src/hooks/useMessages.ts` — refactored to internal implementation (NOT exported publicly), renamed `updateMessageFeedback` → `saveMessageFeedback`
727
+ - ✅ `src/hooks/useParticipants.ts` — refactored to internal implementation (NOT exported publicly)
728
+ - ✅ `src/hooks/useFiles.ts` — refactored to accept props for composability (no longer reads from context directly)
729
+ - ✅ `src/hooks/index.ts` — exports public hooks (`useActiveConversation`, `useConversations`, `useUser`) only; internal hooks (useMessages, useParticipants, useEvent, useFiles, useInitialization, useConversationList) are no longer exported
730
+ - ✅ `src/providers/index.ts` — exports `WebchatProvider`
731
+ - ✅ `src/index.ts` — exports new public API (`useActiveConversation`, `useConversations`, `useUser`, `useWebchat`)
732
+ - ✅ `src/types/client.ts` — exports `UserResponse`, `UserProfile`, `User` types
733
+ - ✅ `src/utils/index.ts` — re-exports file utilities
734
+
735
+ ### Deleted
736
+
737
+ - ✅ `src/utils/uploadFile.ts` — functionality moved into `useFiles` hook