@botpress/webchat 4.3.2 → 4.4.0
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.
- package/.storybook/main.ts +10 -27
- package/.storybook/preview.tsx +2 -2
- package/.turbo/turbo-build.log +8 -8
- package/ARCHITECTURE.md +737 -0
- package/dist/index.d.ts +247 -38
- package/dist/index.js +15362 -9400
- package/dist/index.umd.cjs +122 -113
- package/dist/style.css +1 -1
- package/package.json +13 -14
package/ARCHITECTURE.md
ADDED
|
@@ -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
|