@icanbwell/baileyai-chat-ui 1.0.16 → 1.0.18
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/README.md +199 -1
- package/dist/baileyai-chat-ui.cjs +3 -3
- package/dist/baileyai-chat-ui.css +1 -1
- package/dist/baileyai-chat-ui.js +2029 -522
- package/dist/index.d.ts +522 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -142,6 +142,166 @@ that case. If a host needs `privacyNote` to survive this (e.g. it's a persistent
|
|
|
142
142
|
disclosure rather than a first-impression nicety), pass `privacyNote={{ ..., alwaysVisible: true }}`
|
|
143
143
|
to render it in every state, not just the empty one.
|
|
144
144
|
|
|
145
|
+
## Using the Bailey client
|
|
146
|
+
|
|
147
|
+
`createBaileyClient` is a framework-free client for the Bailey API. It implements
|
|
148
|
+
`BaileyChatTransport` (so it can be passed straight to `useBaileyChat`) and adds conversation
|
|
149
|
+
calls. The host supplies only `baseUrl` and `getHeaders`; the library never reads or stores tokens.
|
|
150
|
+
|
|
151
|
+
```tsx
|
|
152
|
+
import { createBaileyClient, useBaileyChat, useBaileyConversations, createSessionConversationStorage } from '@icanbwell/baileyai-chat-ui';
|
|
153
|
+
|
|
154
|
+
const client = createBaileyClient({
|
|
155
|
+
baseUrl: 'https://api.example.com/bailey/v1', // the API root, including /bailey/v1
|
|
156
|
+
getHeaders: async () => ({ Authorization: `Bearer ${await getAuthToken()}` }),
|
|
157
|
+
onAuthError: (error) => signOut(error.status), // fires once on 401/403
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
const chat = useBaileyChat({
|
|
161
|
+
transport: client,
|
|
162
|
+
model: 'bailey-default',
|
|
163
|
+
instructions: 'You are Bailey...',
|
|
164
|
+
storage: createSessionConversationStorage(), // optional; see below
|
|
165
|
+
});
|
|
166
|
+
const history = useBaileyConversations({ client }); // conversations, loadMore, refresh, remove
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
- Client methods: `listConversations`, `getConversation`, `deleteConversation`, `submitFeedback`,
|
|
170
|
+
`clearFeedback`, and `streamChat`. Failures reject with `BaileyApiError` (`status`, and `kind` of
|
|
171
|
+
`http`, `network`, `timeout`, `aborted` or `invalid-response`); its message is fixed per kind and never
|
|
172
|
+
contains a response body. For `network` and `timeout`, `error.cause` holds the original thrown error
|
|
173
|
+
(the failed fetch's error, or the abort reason); `http`, `invalid-response` and `aborted` have no cause.
|
|
174
|
+
Browsers do not expose whether a failed fetch was CORS, DNS or offline (all are a generic `TypeError`),
|
|
175
|
+
so `kind` plus `cause` is all a host can show or log. Do not show `cause.message` to users without
|
|
176
|
+
review, and do not log the request URL. `invalid-response` means a read (`listConversations`, `getConversation`) got a 2xx whose body was
|
|
177
|
+
not JSON (or a 204); writes (`submitFeedback`, `clearFeedback`, `deleteConversation`) still accept an
|
|
178
|
+
empty or non-JSON success body. `streamChat` keeps the `BaileyChatTransport` contract: it returns the
|
|
179
|
+
HTTP status and raw body for non-2xx responses instead of throwing. If the connection drops after the
|
|
180
|
+
response began it returns `interrupted: true`, and `useBaileyChat` then ends the turn in the error
|
|
181
|
+
state (the partial text stays visible, `onAssistantMessageComplete` does not fire). A custom
|
|
182
|
+
transport should set `interrupted: true` in that case, and must resolve `streamChat` only when the
|
|
183
|
+
response body has closed.
|
|
184
|
+
When no response arrives (`fetch` rejects, or `getHeaders` throws) `streamChat` still resolves with
|
|
185
|
+
`status: undefined` and the fixed `errorMessage: 'Request failed'`, and now also sets `errorKind: 'network'`
|
|
186
|
+
so a host can choose its own copy. `errorCause` carries the error `fetch` threw; it is not set when
|
|
187
|
+
`getHeaders` threw (it can carry auth detail). Both fields are optional and additive, and
|
|
188
|
+
`useBaileyChat`'s copy is unchanged. A caller abort and the idle timeout still reject with the abort
|
|
189
|
+
reason (`TimeoutError` for the idle timeout), and a mid-stream drop still resolves `interrupted: true`
|
|
190
|
+
without `errorKind`. Browsers do not distinguish CORS, DNS or offline, so do not show `errorCause.message`
|
|
191
|
+
to users without review and do not log the request URL.
|
|
192
|
+
A multi-byte character cut off at the end of the stream is flushed as a final `onChunk`.
|
|
193
|
+
- Every call has a timeout (`requestTimeoutMs`, default 30s; the stream uses an idle timeout,
|
|
194
|
+
`idleTimeoutMs`). A failed best-effort cleanup is reported through `onCleanupError` and never thrown.
|
|
195
|
+
- **Nothing is logged** by the client or the hooks, and request/response bodies are never placed in
|
|
196
|
+
errors or callbacks.
|
|
197
|
+
- **Conversation id.** `useBaileyChat` returns a read-only `conversationId`. It comes from the
|
|
198
|
+
`conversationId` option (a seed), from `storage`, or from the `X-Conversation-Id` response header
|
|
199
|
+
the transport reports via `onConversationId`. `newChat()` clears it. The library does not create
|
|
200
|
+
ids. Once an id is held it is sent as the `x-chat-id` header, and **only the newest message** goes in
|
|
201
|
+
`input` because the server prepends its stored history and does not deduplicate. With no id held
|
|
202
|
+
(for example the first turn, before the server reports one) the full transcript is sent, exactly
|
|
203
|
+
as before. Pass `sendFullTranscript: true` to always send the full transcript **and never the id**
|
|
204
|
+
(stateless requests; escape hatch, e.g. where the server does not persist; the library cannot
|
|
205
|
+
detect that itself). The id is withheld because with one the server would prepend its stored
|
|
206
|
+
history to the full transcript and duplicate it in the model context.
|
|
207
|
+
- **`conversationVia` (opt-in, default `'header'`).** `createBaileyClient({ conversationVia: 'body' })`
|
|
208
|
+
sends the held id as the `conversation` field of the request body instead of `x-chat-id` (never
|
|
209
|
+
both). See the `conversationVia` JSDoc for when it is safe to switch. Independently of that option,
|
|
210
|
+
`useBaileyChat` also adopts `response.conversation.id` from `response.completed` / `response.created`
|
|
211
|
+
events when present (non-empty string only), ahead of the `X-Conversation-Id` header value.
|
|
212
|
+
- **The id is persisted in sessionStorage by default (per tab).** A reload resumes the conversation
|
|
213
|
+
(the messages themselves are loaded with `loadConversation`). Pass `storage: false` to disable
|
|
214
|
+
persistence, or your own `BaileyConversationStorage` to store it elsewhere. A storage that throws
|
|
215
|
+
(Safari private mode, blocked storage) never reaches your render: chat continues with the id in
|
|
216
|
+
memory, `persistence` becomes `'degraded'` (`'ok'` normally, `'off'` for `storage: false`) and
|
|
217
|
+
`onStorageError(error, operation)` is called. A `sub` that is provided but blank is a host error
|
|
218
|
+
reported the same way (operation `'key'`); the id then stays in memory and the shared key is not used.
|
|
219
|
+
Append your own surface name to `storageKey` so surfaces do not share a conversation. Selecting an
|
|
220
|
+
old conversation (`loadConversation`) always replaces the stored id. Hosts should clear the stored
|
|
221
|
+
id on logout through the same `storage`; the library has no logout hook.
|
|
222
|
+
- **`sub` keys and validates the stored id (optional).** Pass the signed-in user's `sub` and the stored
|
|
223
|
+
id lives under `${storageKey}:${sub}` and is rejected (and removed) on read unless it starts with
|
|
224
|
+
`<sub>:`, so a different user in the same browser session never inherits the previous user's id. A
|
|
225
|
+
seed and any id reported by the transport are validated the same way. `sub` is used for nothing
|
|
226
|
+
else: **the library never creates a conversation id**, it only reads the one the server reports
|
|
227
|
+
(`X-Conversation-Id`) or one you seed. Without `sub` the stored id is held unvalidated; after a
|
|
228
|
+
shared-browser user switch the server rejects the foreign id and the library adopts the
|
|
229
|
+
server-reported one, so the worst case is a lost first-turn context.
|
|
230
|
+
- **Lost server context is surfaced.** A turn that sent a held id signals context loss when the
|
|
231
|
+
server did not continue that conversation, for one of two reasons. `reason: 'id'`: it came back
|
|
232
|
+
with no id, or a different one (persistence off, an id it does not know). `reason: 'history'`: it
|
|
233
|
+
reported the same id but `conversation.history_status` on `response.created` / `response.completed`
|
|
234
|
+
(the last one the turn carries wins) is `'failed'` or `'empty'`, meaning the stored history was not
|
|
235
|
+
loaded. In both cases the hook drops the held id and its stored value, clears the paging state, sets
|
|
236
|
+
`contextLost: true` until the next turn, and calls `onContextLoss({ sentId, reportedId, reason })`
|
|
237
|
+
(the library always sets `reason`, though it is optional in the type; `reportedId` is absent when the server reported none); the next turn sends
|
|
238
|
+
the full transcript. `'loaded'`, an absent field and any unrecognised value change nothing, and a
|
|
239
|
+
first turn (no id sent) never signals loss. A host-seeded id whose turn reports `'empty'` also
|
|
240
|
+
signals loss: a seeded id is only valid for resuming a stored conversation. The backend also sends
|
|
241
|
+
`history_loaded` (number of prior messages loaded); the library does not read it, it is for host
|
|
242
|
+
diagnostics. A turn that fails outright (4xx/5xx) is reported as an error as before. Older backends
|
|
243
|
+
do not send `history_status`, so for them only `reason: 'id'` can occur and a history load that
|
|
244
|
+
fails while the server reports the same id is still not detectable. Residual risk until the backend
|
|
245
|
+
change (baileyai PR #554, language-model-common PR #136) is deployed and released: the `'history'`
|
|
246
|
+
path is inert.
|
|
247
|
+
- **Persistence off on the server.** Stable conversation ids only help where the server persists
|
|
248
|
+
conversations (the conversation routes return 503 when persistence is off). The library does not
|
|
249
|
+
probe for this: enable stable ids (pass a `client` and keep the default storage) only in environments
|
|
250
|
+
where persistence is on, and otherwise set `sendFullTranscript: true` so every request carries the
|
|
251
|
+
full transcript and no conversation id.
|
|
252
|
+
- Existing hosts that pass none of these options behave as before until a transport reports a
|
|
253
|
+
conversation id: a custom `BaileyChatTransport` keeps working (`conversationId` and
|
|
254
|
+
`onConversationId` are optional transport params, and `conversationId` is only passed once an id is
|
|
255
|
+
held), and none of the hosts' transports report one today. Only a transport that does (such as this
|
|
256
|
+
client) makes the hook hold, persist and send an id.
|
|
257
|
+
- **Loading a conversation.** Pass `client` to `useBaileyChat` to get `loadConversation({ conversationId })`
|
|
258
|
+
(newest 100 messages), `loadEarlier()` / `hasEarlier` (older pages) and `historyStatus`. Hidden rows
|
|
259
|
+
are dropped, each assistant message gets `serverMessageId`, and a stored rating is mapped to
|
|
260
|
+
`message.feedback`. Loading holds the id, so the next turn sends only the newest message and the
|
|
261
|
+
loaded history is not re-sent. Failures reject with `BaileyApiError`, leave the messages intact and
|
|
262
|
+
set `historyStatus` to `'error'`; a response that arrives after `newChat()` or another load is
|
|
263
|
+
discarded. `mapTranscript` and `mergeEarlierPage` are exported for hosts that fetch transcripts
|
|
264
|
+
themselves. `loadMessages` still never changes the held id.
|
|
265
|
+
- **Feedback.** `createBaileyFeedbackHandlers({ client, getConversationId })` returns `onSubmit` / `onClear`
|
|
266
|
+
for `BaileyFeedbackButtons`:
|
|
267
|
+
|
|
268
|
+
```tsx
|
|
269
|
+
const handlers = createBaileyFeedbackHandlers({ client, getConversationId: () => chat.conversationId });
|
|
270
|
+
// inside renderMessageActions:
|
|
271
|
+
<BaileyFeedbackButtons messageId={m.serverMessageId} initialRating={m.feedback?.rating} {...handlers} />
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
Any rejection (including a 404, or no conversation id held) is a failure the buttons show; nothing is
|
|
275
|
+
retried automatically. The comment goes only into the request body.
|
|
276
|
+
- Not included yet: automatic resume on mount. A restored id does not load the transcript; call
|
|
277
|
+
`loadConversation` yourself (the history drawer below does this when the user picks a row).
|
|
278
|
+
|
|
279
|
+
## Conversation history drawer
|
|
280
|
+
|
|
281
|
+
An opt-in, built-in history UI: pass a Bailey client to the panel and the "Chat history" item in the
|
|
282
|
+
"..." menu opens a drawer inside the panel (no dialog dependency, nothing fetched until it opens).
|
|
283
|
+
|
|
284
|
+
```tsx
|
|
285
|
+
const client = createBaileyClient({ baseUrl, getHeaders });
|
|
286
|
+
const chat = useBaileyChat({ transport: client, client, model: 'bailey-default', instructions: '...' });
|
|
287
|
+
|
|
288
|
+
<BaileyChatPanel chat={chat} history={{ client }} locale="en" />
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
- Rows show the title (or "Untitled conversation"), a relative time, and the message count (hidden in
|
|
292
|
+
narrow panels); the active conversation is marked with a tint, an accent bar and the word "Current".
|
|
293
|
+
Selecting a row calls `chat.loadConversation` and closes the drawer. Delete asks for an inline
|
|
294
|
+
confirmation (no undo window); deleting the active conversation resets the chat. "Load more" follows
|
|
295
|
+
the server's cursor.
|
|
296
|
+
- Errors show fixed copy (a separate "session expired" message for a 401/403 on the list call, the
|
|
297
|
+
connection message otherwise) with Retry; error text and titles never reach logs or `title` attributes.
|
|
298
|
+
- `onHistoryClick` is the host override: when both `history` and `onHistoryClick` are given, your callback
|
|
299
|
+
runs and the drawer never mounts. A host that passes neither renders exactly as before.
|
|
300
|
+
- Every string is a `labels` key (`historyTitle`, `historyEmptyTitle`, `historySessionExpiredTitle`, and so
|
|
301
|
+
on, all with English defaults). The session-expired wording is a default for copy review. Relative times
|
|
302
|
+
use `Intl.RelativeTimeFormat` with `locale` (default `navigator.language`): a future time shows "now",
|
|
303
|
+
and a time without an offset is read as UTC.
|
|
304
|
+
|
|
145
305
|
## Resuming a conversation
|
|
146
306
|
|
|
147
307
|
`useBaileyChat` also returns `loadMessages(messages)`, for restoring a transcript a host fetched
|
|
@@ -167,7 +327,8 @@ be mid-stream.
|
|
|
167
327
|
|
|
168
328
|
`loadMessages` only replaces `useBaileyChat`'s own state — a host still owns fetching the
|
|
169
329
|
transcript and its own loading/error UI, and owns conversation identity (which id is active,
|
|
170
|
-
where it's persisted)
|
|
330
|
+
where it's persisted) outside this package, unless it opts in to the id handling described in
|
|
331
|
+
[Using the Bailey client](#using-the-bailey-client).
|
|
171
332
|
|
|
172
333
|
### Known limitations
|
|
173
334
|
|
|
@@ -236,6 +397,43 @@ Pass `privacyNote` to render a short compliance/privacy disclosure as a footer b
|
|
|
236
397
|
|
|
237
398
|
By default the note only renders in the empty state, before the first message. Pass `alwaysVisible: true` to keep it showing for the entire conversation instead — see [Sending a hidden turn](#sending-a-hidden-turn) above for why a host might need this.
|
|
238
399
|
|
|
400
|
+
## Collecting feedback
|
|
401
|
+
|
|
402
|
+
`BaileyFeedbackButtons` renders thumbs up/down on an assistant message, with an inline form (reason chips and a comment) after a thumbs-down or an "Add a comment" click. It is opt-in and never calls an API itself: your app owns auth and networking and passes `onSubmit`/`onClear`. No extra install or subpath; it is part of the base entry.
|
|
403
|
+
|
|
404
|
+
```tsx
|
|
405
|
+
<BaileyChatPanel
|
|
406
|
+
chat={chat}
|
|
407
|
+
renderMessageActions={(message) =>
|
|
408
|
+
message.serverMessageId ? (
|
|
409
|
+
<BaileyFeedbackButtons
|
|
410
|
+
messageId={message.serverMessageId}
|
|
411
|
+
onSubmit={({ messageId, rating, reasonCodes, comment }) =>
|
|
412
|
+
submitFeedback({ messageId, rating, reasonCodes, comment })
|
|
413
|
+
}
|
|
414
|
+
onClear={({ messageId }) => clearFeedback({ messageId })}
|
|
415
|
+
/>
|
|
416
|
+
) : null
|
|
417
|
+
}
|
|
418
|
+
/>
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
Pass a stable function for `renderMessageActions`. The control renders nothing when `messageId` is empty.
|
|
422
|
+
|
|
423
|
+
**Message ID.** `BaileyMessage.serverMessageId` is the backend's assistant message ID (the `id` field is minted client-side and is not usable for this). `useBaileyChat` reads it from the `message_id` field of the `response.completed` SSE event. A transport that reads it from a response header (`X-Message-Id`, non-streaming responses) can report it early through the optional `onMessageId(messageId)` callback of `streamChat`, parallel to `onRequestId`. `X-Message-Id` is only readable from a browser cross-origin if the backend exposes it through CORS (`Access-Control-Expose-Headers`), so browser hosts should prefer the `response.completed` event (or the `message_id` in the response body) over the header. A missing or non-string `message_id` is ignored, leaving `serverMessageId` undefined, so the control does not render against an older backend.
|
|
424
|
+
|
|
425
|
+
**Behavior.**
|
|
426
|
+
|
|
427
|
+
- Clicking a thumb calls `onSubmit({ messageId, rating })` immediately. The form's Submit calls `onSubmit` again with `{ messageId, rating, reasonCodes?, comment? }` (omitted when empty); the backend call is an upsert, so this replaces the earlier write. Clicking the selected thumb, or Undo, calls `onClear({ messageId })`.
|
|
428
|
+
- At most one request is in flight per `messageId`; edits made meanwhile are coalesced to the latest state and sent afterwards.
|
|
429
|
+
- A rejected promise means failure: the optimistic selection reverts, `labels.feedbackErrorText` is shown and the draft is kept.
|
|
430
|
+
- Any rejection, including a 404, is shown as an error and is never retried automatically; the user retries by hand. The backend emits `message_id` only after the assistant message is committed, so a message that carries a `serverMessageId` exists and a 404 on it is a real failure. Render the control only for messages that have a `serverMessageId`.
|
|
431
|
+
- `reasonOptions` selects a subset (and order) of the backend codes `inaccurate`, `unhelpful`, `unsafe`, `too_long`, `other`; an unknown code is treated as `other`. `maxCommentLength` defaults to 2000 and can only be lowered. `initialRating` shows a previously stored rating.
|
|
432
|
+
- The comment is PHI-capable. It is passed to `onSubmit` only, never to the console, the trace panel or any other callback. Do not log it in your handler either.
|
|
433
|
+
- Strings are overridable through the `labels` prop (`BaileyFeedbackLabels`), merged shallowly with English defaults, like `BaileyChatPanel`'s labels.
|
|
434
|
+
|
|
435
|
+
Minimum backend: the assistant `message_id` on `response.completed` (and `X-Message-Id` for non-streaming responses) and the feedback routes from BAI-1231 (baileyai, BAI-1234). On a backend without them the buttons simply do not appear. A `503` from the feedback API means feedback is disabled for that environment; hide the control from your side for the session.
|
|
436
|
+
|
|
239
437
|
## Localization
|
|
240
438
|
|
|
241
439
|
Every visible string in `BaileyChatPanel` defaults to English. Pass a `labels` prop with
|