@icanbwell/baileyai-chat-ui 1.0.15 → 1.0.17
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 +180 -1
- package/dist/baileyai-chat-ui.cjs +3 -3
- package/dist/baileyai-chat-ui.css +1 -1
- package/dist/baileyai-chat-ui.js +2029 -534
- package/dist/index.d.ts +497 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -142,6 +142,147 @@ 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
|
+
- Every call has a timeout (`requestTimeoutMs`, default 30s; the stream uses an idle timeout,
|
|
185
|
+
`idleTimeoutMs`). A failed best-effort cleanup is reported through `onCleanupError` and never thrown.
|
|
186
|
+
- **Nothing is logged** by the client or the hooks, and request/response bodies are never placed in
|
|
187
|
+
errors or callbacks.
|
|
188
|
+
- **Conversation id.** `useBaileyChat` returns a read-only `conversationId`. It comes from the
|
|
189
|
+
`conversationId` option (a seed), from `storage`, or from the `X-Conversation-Id` response header
|
|
190
|
+
the transport reports via `onConversationId`. `newChat()` clears it. The library does not create
|
|
191
|
+
ids. Once an id is held it is sent as the `x-chat-id` header, and **only the newest message** goes in
|
|
192
|
+
`input` because the server prepends its stored history and does not deduplicate. With no id held
|
|
193
|
+
(for example the first turn, before the server reports one) the full transcript is sent, exactly
|
|
194
|
+
as before. Pass `sendFullTranscript: true` to always send the full transcript **and never the id**
|
|
195
|
+
(stateless requests; escape hatch, e.g. where the server does not persist; the library cannot
|
|
196
|
+
detect that itself). The id is withheld because with one the server would prepend its stored
|
|
197
|
+
history to the full transcript and duplicate it in the model context.
|
|
198
|
+
- **`conversationVia` (opt-in, default `'header'`).** `createBaileyClient({ conversationVia: 'body' })`
|
|
199
|
+
sends the held id as the `conversation` field of the request body instead of `x-chat-id` (never
|
|
200
|
+
both). See the `conversationVia` JSDoc for when it is safe to switch. Independently of that option,
|
|
201
|
+
`useBaileyChat` also adopts `response.conversation.id` from `response.completed` / `response.created`
|
|
202
|
+
events when present (non-empty string only), ahead of the `X-Conversation-Id` header value.
|
|
203
|
+
- **The id is persisted in sessionStorage by default (per tab).** A reload resumes the conversation
|
|
204
|
+
(the messages themselves are loaded with `loadConversation`). Pass `storage: false` to disable
|
|
205
|
+
persistence, or your own `BaileyConversationStorage` to store it elsewhere. A storage that throws
|
|
206
|
+
(Safari private mode, blocked storage) never reaches your render: chat continues with the id in
|
|
207
|
+
memory, `persistence` becomes `'degraded'` (`'ok'` normally, `'off'` for `storage: false`) and
|
|
208
|
+
`onStorageError(error, operation)` is called. A `sub` that is provided but blank is a host error
|
|
209
|
+
reported the same way (operation `'key'`); the id then stays in memory and the shared key is not used.
|
|
210
|
+
Append your own surface name to `storageKey` so surfaces do not share a conversation. Selecting an
|
|
211
|
+
old conversation (`loadConversation`) always replaces the stored id. Hosts should clear the stored
|
|
212
|
+
id on logout through the same `storage`; the library has no logout hook.
|
|
213
|
+
- **`sub` keys and validates the stored id (optional).** Pass the signed-in user's `sub` and the stored
|
|
214
|
+
id lives under `${storageKey}:${sub}` and is rejected (and removed) on read unless it starts with
|
|
215
|
+
`<sub>:`, so a different user in the same browser session never inherits the previous user's id. A
|
|
216
|
+
seed and any id reported by the transport are validated the same way. `sub` is used for nothing
|
|
217
|
+
else: **the library never creates a conversation id**, it only reads the one the server reports
|
|
218
|
+
(`X-Conversation-Id`) or one you seed. Without `sub` the stored id is held unvalidated; after a
|
|
219
|
+
shared-browser user switch the server rejects the foreign id and the library adopts the
|
|
220
|
+
server-reported one, so the worst case is a lost first-turn context.
|
|
221
|
+
- **Lost server context is surfaced.** When a turn that sent a held id comes back with no id, or a
|
|
222
|
+
different one, the server did not continue that conversation (persistence off, an id it does not
|
|
223
|
+
know, or history it could not load). The hook drops the held id and its stored value, sets
|
|
224
|
+
`contextLost: true` until the next turn, and calls `onContextLoss({ sentId, reportedId })`; the next
|
|
225
|
+
turn sends the full transcript. A turn that fails outright (4xx/5xx) is reported as an error as
|
|
226
|
+
before. A history load that fails while the server still reports the same id is not detectable
|
|
227
|
+
yet (it needs a server signal).
|
|
228
|
+
- **Persistence off on the server.** Stable conversation ids only help where the server persists
|
|
229
|
+
conversations (the conversation routes return 503 when persistence is off). The library does not
|
|
230
|
+
probe for this: enable stable ids (pass a `client` and keep the default storage) only in environments
|
|
231
|
+
where persistence is on, and otherwise set `sendFullTranscript: true` so every request carries the
|
|
232
|
+
full transcript and no conversation id.
|
|
233
|
+
- Existing hosts that pass none of these options behave as before until a transport reports a
|
|
234
|
+
conversation id: a custom `BaileyChatTransport` keeps working (`conversationId` and
|
|
235
|
+
`onConversationId` are optional transport params, and `conversationId` is only passed once an id is
|
|
236
|
+
held), and none of the hosts' transports report one today. Only a transport that does (such as this
|
|
237
|
+
client) makes the hook hold, persist and send an id.
|
|
238
|
+
- **Loading a conversation.** Pass `client` to `useBaileyChat` to get `loadConversation({ conversationId })`
|
|
239
|
+
(newest 100 messages), `loadEarlier()` / `hasEarlier` (older pages) and `historyStatus`. Hidden rows
|
|
240
|
+
are dropped, each assistant message gets `serverMessageId`, and a stored rating is mapped to
|
|
241
|
+
`message.feedback`. Loading holds the id, so the next turn sends only the newest message and the
|
|
242
|
+
loaded history is not re-sent. Failures reject with `BaileyApiError`, leave the messages intact and
|
|
243
|
+
set `historyStatus` to `'error'`; a response that arrives after `newChat()` or another load is
|
|
244
|
+
discarded. `mapTranscript` and `mergeEarlierPage` are exported for hosts that fetch transcripts
|
|
245
|
+
themselves. `loadMessages` still never changes the held id.
|
|
246
|
+
- **Feedback.** `createBaileyFeedbackHandlers({ client, getConversationId })` returns `onSubmit` / `onClear`
|
|
247
|
+
for `BaileyFeedbackButtons`:
|
|
248
|
+
|
|
249
|
+
```tsx
|
|
250
|
+
const handlers = createBaileyFeedbackHandlers({ client, getConversationId: () => chat.conversationId });
|
|
251
|
+
// inside renderMessageActions:
|
|
252
|
+
<BaileyFeedbackButtons messageId={m.serverMessageId} initialRating={m.feedback?.rating} {...handlers} />
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
Any rejection (including a 404, or no conversation id held) is a failure the buttons show; nothing is
|
|
256
|
+
retried automatically. The comment goes only into the request body.
|
|
257
|
+
- Not included yet: automatic resume on mount. A restored id does not load the transcript; call
|
|
258
|
+
`loadConversation` yourself (the history drawer below does this when the user picks a row).
|
|
259
|
+
|
|
260
|
+
## Conversation history drawer
|
|
261
|
+
|
|
262
|
+
An opt-in, built-in history UI: pass a Bailey client to the panel and the "Chat history" item in the
|
|
263
|
+
"..." menu opens a drawer inside the panel (no dialog dependency, nothing fetched until it opens).
|
|
264
|
+
|
|
265
|
+
```tsx
|
|
266
|
+
const client = createBaileyClient({ baseUrl, getHeaders });
|
|
267
|
+
const chat = useBaileyChat({ transport: client, client, model: 'bailey-default', instructions: '...' });
|
|
268
|
+
|
|
269
|
+
<BaileyChatPanel chat={chat} history={{ client }} locale="en" />
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
- Rows show the title (or "Untitled conversation"), a relative time, and the message count (hidden in
|
|
273
|
+
narrow panels); the active conversation is marked with a tint, an accent bar and the word "Current".
|
|
274
|
+
Selecting a row calls `chat.loadConversation` and closes the drawer. Delete asks for an inline
|
|
275
|
+
confirmation (no undo window); deleting the active conversation resets the chat. "Load more" follows
|
|
276
|
+
the server's cursor.
|
|
277
|
+
- Errors show fixed copy (a separate "session expired" message for a 401/403 on the list call, the
|
|
278
|
+
connection message otherwise) with Retry; error text and titles never reach logs or `title` attributes.
|
|
279
|
+
- `onHistoryClick` is the host override: when both `history` and `onHistoryClick` are given, your callback
|
|
280
|
+
runs and the drawer never mounts. A host that passes neither renders exactly as before.
|
|
281
|
+
- Every string is a `labels` key (`historyTitle`, `historyEmptyTitle`, `historySessionExpiredTitle`, and so
|
|
282
|
+
on, all with English defaults). The session-expired wording is a default for copy review. Relative times
|
|
283
|
+
use `Intl.RelativeTimeFormat` with `locale` (default `navigator.language`): a future time shows "now",
|
|
284
|
+
and a time without an offset is read as UTC.
|
|
285
|
+
|
|
145
286
|
## Resuming a conversation
|
|
146
287
|
|
|
147
288
|
`useBaileyChat` also returns `loadMessages(messages)`, for restoring a transcript a host fetched
|
|
@@ -167,7 +308,8 @@ be mid-stream.
|
|
|
167
308
|
|
|
168
309
|
`loadMessages` only replaces `useBaileyChat`'s own state — a host still owns fetching the
|
|
169
310
|
transcript and its own loading/error UI, and owns conversation identity (which id is active,
|
|
170
|
-
where it's persisted)
|
|
311
|
+
where it's persisted) outside this package, unless it opts in to the id handling described in
|
|
312
|
+
[Using the Bailey client](#using-the-bailey-client).
|
|
171
313
|
|
|
172
314
|
### Known limitations
|
|
173
315
|
|
|
@@ -236,6 +378,43 @@ Pass `privacyNote` to render a short compliance/privacy disclosure as a footer b
|
|
|
236
378
|
|
|
237
379
|
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
380
|
|
|
381
|
+
## Collecting feedback
|
|
382
|
+
|
|
383
|
+
`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.
|
|
384
|
+
|
|
385
|
+
```tsx
|
|
386
|
+
<BaileyChatPanel
|
|
387
|
+
chat={chat}
|
|
388
|
+
renderMessageActions={(message) =>
|
|
389
|
+
message.serverMessageId ? (
|
|
390
|
+
<BaileyFeedbackButtons
|
|
391
|
+
messageId={message.serverMessageId}
|
|
392
|
+
onSubmit={({ messageId, rating, reasonCodes, comment }) =>
|
|
393
|
+
submitFeedback({ messageId, rating, reasonCodes, comment })
|
|
394
|
+
}
|
|
395
|
+
onClear={({ messageId }) => clearFeedback({ messageId })}
|
|
396
|
+
/>
|
|
397
|
+
) : null
|
|
398
|
+
}
|
|
399
|
+
/>
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
Pass a stable function for `renderMessageActions`. The control renders nothing when `messageId` is empty.
|
|
403
|
+
|
|
404
|
+
**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.
|
|
405
|
+
|
|
406
|
+
**Behavior.**
|
|
407
|
+
|
|
408
|
+
- 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 })`.
|
|
409
|
+
- At most one request is in flight per `messageId`; edits made meanwhile are coalesced to the latest state and sent afterwards.
|
|
410
|
+
- A rejected promise means failure: the optimistic selection reverts, `labels.feedbackErrorText` is shown and the draft is kept.
|
|
411
|
+
- 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`.
|
|
412
|
+
- `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.
|
|
413
|
+
- 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.
|
|
414
|
+
- Strings are overridable through the `labels` prop (`BaileyFeedbackLabels`), merged shallowly with English defaults, like `BaileyChatPanel`'s labels.
|
|
415
|
+
|
|
416
|
+
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.
|
|
417
|
+
|
|
239
418
|
## Localization
|
|
240
419
|
|
|
241
420
|
Every visible string in `BaileyChatPanel` defaults to English. Pass a `labels` prop with
|