@bitbaum/chatkit 0.17.1 → 0.18.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.
Files changed (46) hide show
  1. package/README.md +152 -0
  2. package/dist/conversations.d.ts +99 -0
  3. package/dist/conversations.d.ts.map +1 -0
  4. package/dist/conversations.js +146 -0
  5. package/dist/conversations.js.map +1 -0
  6. package/dist/index.d.ts +2 -0
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +2 -0
  9. package/dist/index.js.map +1 -1
  10. package/dist/react/ChatApp.d.ts +123 -0
  11. package/dist/react/ChatApp.d.ts.map +1 -0
  12. package/dist/react/ChatApp.js +215 -0
  13. package/dist/react/ChatApp.js.map +1 -0
  14. package/dist/react/ChatHistory.d.ts +28 -0
  15. package/dist/react/ChatHistory.d.ts.map +1 -0
  16. package/dist/react/ChatHistory.js +58 -0
  17. package/dist/react/ChatHistory.js.map +1 -0
  18. package/dist/react/Composer.d.ts +4 -1
  19. package/dist/react/Composer.d.ts.map +1 -1
  20. package/dist/react/Composer.js +3 -3
  21. package/dist/react/Composer.js.map +1 -1
  22. package/dist/react/Thread.d.ts +4 -0
  23. package/dist/react/Thread.d.ts.map +1 -1
  24. package/dist/react/Thread.js +2 -2
  25. package/dist/react/Thread.js.map +1 -1
  26. package/dist/react/hooks.d.ts.map +1 -1
  27. package/dist/react/hooks.js +11 -0
  28. package/dist/react/hooks.js.map +1 -1
  29. package/dist/react/icons.d.ts +7 -0
  30. package/dist/react/icons.d.ts.map +1 -1
  31. package/dist/react/icons.js +8 -0
  32. package/dist/react/icons.js.map +1 -1
  33. package/dist/react/index.d.ts +6 -0
  34. package/dist/react/index.d.ts.map +1 -1
  35. package/dist/react/index.js +3 -0
  36. package/dist/react/index.js.map +1 -1
  37. package/dist/react/use-chat-app.d.ts +81 -0
  38. package/dist/react/use-chat-app.d.ts.map +1 -0
  39. package/dist/react/use-chat-app.js +380 -0
  40. package/dist/react/use-chat-app.js.map +1 -0
  41. package/dist/stores.d.ts +63 -0
  42. package/dist/stores.d.ts.map +1 -0
  43. package/dist/stores.js +291 -0
  44. package/dist/stores.js.map +1 -0
  45. package/package.json +3 -3
  46. package/styles.css +409 -0
package/README.md CHANGED
@@ -82,6 +82,155 @@ The reading column is `--ck-measure` (48rem) wide with `--ck-gutter` (1rem)
82
82
  to the edge; the page behind it is `--ck-page`. `demo/screen.html` is the
83
83
  reference: look at it at 390px before changing any of this.
84
84
 
85
+ ### The whole app — `ChatApp`
86
+
87
+ `ChatScreen`, `Composer` and `ChatThread` are the pieces. Until 0.18 every
88
+ product built the app around them itself — the chat list, New chat, Temporary
89
+ chat, titles, rename and delete, what a reload reopens — and each did it a
90
+ little differently (`docs/fleet-chat-matrix.md` has the table). `ChatApp` is
91
+ that app, once: what a person sees in the Claude or ChatGPT phone app, set by
92
+ this package and changed only through props.
93
+
94
+ ```tsx
95
+ import { ChatApp } from "@bitbaum/chatkit/react";
96
+ import { httpConversationStore, localConversationStore } from "@bitbaum/chatkit";
97
+
98
+ const store = signedIn
99
+ ? httpConversationStore({ baseUrl: "/api/conversations" })
100
+ : localConversationStore({ key: "heidi-chat" });
101
+
102
+ <ChatApp
103
+ name="Heidi"
104
+ store={store}
105
+ transport={async ({ text, attachments, history, signal, update }) => {
106
+ const answer = await streamFromMyApi({ text, attachments, history, signal, onText: (t) => update({ text: t }) });
107
+ return answer; // a string, or { content, replies?, citations?, thinking? }
108
+ }}
109
+ composer={{ voice: { transcribeUrl: "/api/transcribe" } }}
110
+ starters={["Explain this word", "Translate a message"]}
111
+ />;
112
+ ```
113
+
114
+ What it owns, so no product writes it again:
115
+
116
+ - **The header**: frosted pills — Chats (a phone; from `railQuery`, default
117
+ 64rem, the list is a rail beside the chat instead) · the chat's title ·
118
+ New chat · "…" with Temporary chat, Share this chat (with `onShare`),
119
+ Rename and Delete. `headerStart` / `headerEnd` take the product's own
120
+ controls (a model picker, a back link).
121
+ - **The chat list**: a full-height drawer on a phone (a native `<dialog>`:
122
+ Escape, the focus trap and focus restore come from the browser; it opens on
123
+ its close button, never on the search box, so no keyboard jumps up), a rail
124
+ on a wide screen. New chat, a search box (every word, accents ignored — or
125
+ your `search`, e.g. searchkit), Today / Yesterday / Earlier, and rows with
126
+ ONE anatomy: title, date (or "Answering…"), one "…" that opens Rename ·
127
+ Delete as two equal cells; Delete asks once, in the row. An empty list and a
128
+ failed load each say so, the second with Try again.
129
+ - **New chat and Temporary chat.** A temporary chat is never written to the
130
+ store and ends when it is left; the transport gets `temporary: true` and
131
+ `conversationId: null`, so a server that persists turns itself knows not to.
132
+ - **Titles**: the first message names a chat (`deriveTitle`, Heidi's rule);
133
+ `makeTitle({ text, answer })` renames it after the first answer if you have
134
+ something better.
135
+ - **Reload**: the open chat comes back (`rememberKey`, device storage — never
136
+ a temporary one), unless it was deleted elsewhere. A product that keeps the
137
+ chat in its URL passes `conversationId` and `onConversationChange` instead.
138
+ - **Turns**: one per conversation, and a turn keeps running when the person
139
+ opens another chat (its row says "Answering…"). Stop keeps what had arrived.
140
+ A failed turn shows Retry. A message sent while an answer runs is queued
141
+ above the composer, with its own ×, and goes when the answer lands.
142
+ - **The composer**: attachments ON (Camera · Photos · Files, thumbnails above
143
+ the text, each with its ×, shown in the sent turn too), the microphone,
144
+ suggested replies, and the screen's visual-viewport sizing (`fill`).
145
+
146
+ **The transport** is the product's one job: `(turn) => Promise<answer>`.
147
+ `turn` is `{ conversationId, temporary, text, attachments, history, signal,
148
+ update }`; call `update({ text, status, steps })` as the answer streams (the
149
+ whole text each time); throw for a failed turn.
150
+
151
+ **The store** is an adapter — six methods, `ConversationStore`:
152
+ `list() · get(id) · create({ title }) · append(id, message) · rename(id,
153
+ title) · remove(id)`. Two come with the package:
154
+
155
+ - `localConversationStore({ key })` — the device. Versioned (`<key>:v1`),
156
+ one key per chat; never throws: storage that is missing, blocked (private
157
+ mode) or full degrades to memory for the page, and a full quota drops the
158
+ OLDEST chats first. Pictures are kept by name, not bytes.
159
+ - `httpConversationStore({ baseUrl })` — the product's server, over
160
+ Heidi's shape: `GET {base}` → `{ conversations }`, `POST {base}` →
161
+ `{ conversation }`, `GET {base}/{id}` → `{ conversation, messages }` (404 =
162
+ gone), `POST {base}/{id}/messages` `{ message }`, `PATCH {base}/{id}`
163
+ `{ title }`, `DELETE {base}/{id}`. `serverWritesTurns: true` when the
164
+ product's chat route already stores each turn, so nothing is written twice.
165
+
166
+ Signed out on the device, signed in on the server: pass the store for who is
167
+ there. `copyConversations(local, server)` moves the device's chats into the
168
+ account on sign-in.
169
+
170
+ `demo/app.html` is the reference — look at it at 390px and 1440px.
171
+ `test/browser-app.mjs` drives it at both widths: send, the drawer (focus in
172
+ and back out), New chat, a temporary chat that leaves no row, a reload that
173
+ reopens the chat, a picture staged above the text and removed, delete, the
174
+ queue and Stop — and checks for sideways scroll, 44px targets and cut words
175
+ at every step.
176
+
177
+ #### Moving a product onto ChatApp
178
+
179
+ Per product, from the matrix: what goes, what stays. What stays is always
180
+ the same two things — the transport and the words.
181
+
182
+ - **Loki** — goes: `ThreadRail.tsx`, the history half of
183
+ `WorkspaceDrawers.tsx` and `WorkspaceChrome.tsx`, `ThreadTitle.tsx`,
184
+ `hooks/use-temporary-chat.ts`, `hooks/use-new-chat.ts`,
185
+ `hooks/use-send-queue.ts` + `QueuedMessages.tsx`, the history-pin storage.
186
+ Stays: `use-loki-stream` as the transport (its `ephemeral` flag from
187
+ `turn.temporary`), `ModelPicker` and the project chip in `composer.tools`,
188
+ the dispatch and handoff cards in `thread.renderFooter`,
189
+ `httpConversationStore({ baseUrl: "/api/conversations", serverWritesTurns:
190
+ true })`, `?c=` through `conversationId`. Not in ChatApp yet, so ask for it
191
+ here first: pinned chats, the "needs me first" line and by-project view,
192
+ hands-free voice conversation.
193
+ - **OrangeCat** — goes: `ConversationRail.tsx`, `hooks/useConversations.ts`
194
+ (incl. its temporary chat), `hooks/useChatHistory.ts`, the header row of
195
+ `ChatHeader.tsx`. Stays: the Cat's stream as the transport,
196
+ `ComposerAddMenu` (the person's own things) in `composer.tools` with
197
+ `heldAttachments`, tool chips and pending actions in `thread.renderFooter`,
198
+ the quota meter in `headerEnd`. Its conversation routes are not the REST
199
+ shape above: a six-method store over `API_ROUTES.CAT`, or align the routes.
200
+ Gains: reopening the chat after a reload, rename, search.
201
+ - **Heidi** — goes: `conversation-list.tsx`, `new-chat-button.tsx`, the
202
+ header and drawer state in `workspace.tsx`, the storage half of
203
+ `use-conversation.ts`, the signed-out draft restore (`use-draft`) →
204
+ `localConversationStore`. Stays: `transports.ts` (its `Transport` maps onto
205
+ this one: `onText` → `update`, a `status: "error"` result → a throw), the composer labels and picture limits, the answer view
206
+ and word pick in `thread.renderFooter`, the model sheet in `headerEnd`.
207
+ Store: `httpConversationStore({ baseUrl: "/api/conversations",
208
+ serverWritesTurns: true })` signed in. Gains: a temporary chat.
209
+ - **Substrata** — goes: the `ChatScreen` + `Composer` + `ChatThread`
210
+ assembly in `ResearchChat.tsx`, `lib/ask-thread.ts` (the tab handoff →
211
+ `conversationId` + the device store). Stays: `useChatSession` as the
212
+ transport, `AskTools` / `ComposerBeside` in `composer.tools` and
213
+ `footnote`, `chartRenderers`, `AnswerFooter`. Decide: attachments are ON in
214
+ ChatApp; `/api/chat` takes no files today, so either keep
215
+ `composer={{ attach: false }}` or accept pictures there.
216
+ - **Diplodoctor** — goes: the assembly in `app/[locale]/ask/page.tsx`.
217
+ Stays: the redacting `ask()` as the transport, the labels. Gains: a chat
218
+ list and reload. Needs: a transcription route (voice is off today) and an
219
+ API that takes files before `attach` is turned on.
220
+ - **Causius** — stays as it is for now. A matter is a case with documents,
221
+ participants and steps, not a chat; its list is the matters page.
222
+ `ChatThread` + `IntakeComposer` remain the right pieces.
223
+ - **evig** — goes: the header and Clear in `HirnChat.tsx` and
224
+ `HirnChatPanel.tsx`. Stays: `/api/hirn/chat` as the transport, the action
225
+ cards in `thread.renderFooter`, `useChatLabels`. Store: the Hirn history
226
+ route is per session, not the REST shape — a six-method store, or the
227
+ device store for the panel.
228
+ - **Solon** — goes: the header, "New conversation" and the React-state
229
+ history in `governance-chat.tsx` and `law-chat.tsx`. Stays: the SSE read as
230
+ the transport, the starters, `renderLink`. Gains: chats that survive a
231
+ reload (the footnote promises "stays in this tab" and today nothing does).
232
+ Signed out keeps its own "Sign in to ask Solon" screen instead of ChatApp.
233
+
85
234
  ### The microphone
86
235
 
87
236
  `voice` is on by default. Give it a server leg so it works everywhere:
@@ -440,6 +589,9 @@ on `Composer`, `labels` on `ChatThread`.
440
589
  9. Nothing overflows at 320–390px.
441
590
  10. In `ChatScreen`, the conversation fills the display and opens at the
442
591
  latest message, with its end above the composer, not behind it.
592
+ 11. In `ChatApp`, the same app everywhere: one chat list with one row
593
+ anatomy, New chat, Temporary chat that writes nothing, the open chat
594
+ back after a reload, attachments on, a queue behind a running answer.
443
595
 
444
596
  `pnpm run verify` proves all of it: unit and render tests, then
445
597
  `test/browser.mjs` drives Chromium through the demo at desktop and 390px, light
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Conversations: the list a chat app keeps, and the decisions about it that
3
+ * every product used to make on its own — what a chat is called, how the
4
+ * list is grouped and searched, which chat a reload reopens. Pure, so each
5
+ * rule is tested without a DOM. `ChatApp` (`@bitbaum/chatkit/react`) is the
6
+ * screen built on them; `stores.ts` holds where they are kept.
7
+ */
8
+ /** One row of the chat list. `updatedAt` is an ISO date (or ms since epoch). */
9
+ export type ConversationSummary = {
10
+ id: string;
11
+ title: string;
12
+ updatedAt: string | number;
13
+ };
14
+ /** A file that went with a message, as the thread shows it afterwards. */
15
+ export type MessageAttachment = {
16
+ kind: "image" | "text";
17
+ name: string;
18
+ /** A picture to draw (a data: or blob: URL). Not kept by the device store. */
19
+ previewUrl?: string;
20
+ };
21
+ /**
22
+ * A message as a store keeps it. Structurally the thread's `ChatMessageData`:
23
+ * whatever else a product puts on a message (citations, thinking, a speaker)
24
+ * travels through untouched.
25
+ */
26
+ export type StoredMessage = {
27
+ id: string;
28
+ role: "user" | "assistant";
29
+ content: string;
30
+ attachments?: readonly MessageAttachment[];
31
+ [field: string]: unknown;
32
+ };
33
+ export type Conversation = ConversationSummary & {
34
+ messages: StoredMessage[];
35
+ };
36
+ /**
37
+ * Where conversations live. The app picks: `localConversationStore` keeps them
38
+ * on the device (signed out), `httpConversationStore` on the product's server
39
+ * (signed in), or the product writes its own — six methods.
40
+ */
41
+ export interface ConversationStore {
42
+ /** Every conversation, newest first. */
43
+ list(): Promise<ConversationSummary[]>;
44
+ /** One conversation with its messages, or null when it is gone. */
45
+ get(id: string): Promise<Conversation | null>;
46
+ create(init?: {
47
+ title?: string;
48
+ }): Promise<ConversationSummary>;
49
+ append(id: string, message: StoredMessage): Promise<void>;
50
+ rename(id: string, title: string): Promise<void>;
51
+ remove(id: string): Promise<void>;
52
+ }
53
+ /** An id that is unique enough for a client-made message or conversation,
54
+ * also where `crypto.randomUUID` is missing (plain http, old WebViews). */
55
+ export declare function newId(prefix?: string): string;
56
+ /** The longest title derived from a message, in characters. */
57
+ export declare const TITLE_MAX = 48;
58
+ /**
59
+ * What a chat is called before anyone names it: the first thing the person
60
+ * said, as one line, without the markdown, cut at a word with an ellipsis.
61
+ * `null` when there are no words (a message of only an attachment) — the
62
+ * list then shows its "New chat" label rather than an empty row. The rule
63
+ * is Heidi's `lib/domain/chat/title.ts`, (heidi repo), now the package's.
64
+ */
65
+ export declare function deriveTitle(text: string, max?: number): string | null;
66
+ /** Newest first; a row with an unreadable date sinks to the bottom. */
67
+ export declare function sortConversations<T extends ConversationSummary>(list: readonly T[]): T[];
68
+ export type ConversationGroupKey = "today" | "yesterday" | "earlier";
69
+ export type ConversationGroup<T extends ConversationSummary = ConversationSummary> = {
70
+ key: ConversationGroupKey;
71
+ items: T[];
72
+ };
73
+ /** Which heading a row sits under, by the reader's own calendar day. */
74
+ export declare function conversationGroupOf(at: string | number, now?: number): ConversationGroupKey;
75
+ /** The list under Today / Yesterday / Earlier, newest first, empty groups left out. */
76
+ export declare function groupConversations<T extends ConversationSummary>(list: readonly T[], now?: number): ConversationGroup<T>[];
77
+ /**
78
+ * The search box over the list: every word typed must appear in the title,
79
+ * case and accents ignored ("zurich" finds "Zürich"). Order is kept. A
80
+ * product that ranks with `@bitbaum/searchkit` passes its own to `ChatApp`'s
81
+ * `search` — chatkit itself takes no dependency.
82
+ */
83
+ export declare function filterConversations<T extends ConversationSummary>(list: readonly T[], query: string): T[];
84
+ /**
85
+ * The date on a row, the way the phone apps write it: the time today, the
86
+ * weekday within the week, the date after that (with the year once it is not
87
+ * this year's). In the reader's locale through `Intl`.
88
+ */
89
+ export declare function formatConversationDate(at: string | number, now?: number, locale?: string): string;
90
+ /**
91
+ * Which chat a reload opens: the one asked for (a link, the URL), else the
92
+ * one that was open, but only while it still exists — a chat deleted on
93
+ * another device opens a new chat, never an error page.
94
+ */
95
+ export declare function pickRestored(list: readonly ConversationSummary[], wanted: string | null | undefined, remembered: string | null | undefined): string | null;
96
+ /** Copy every conversation from one store into another — the signed-out
97
+ * chats into the account when someone signs in. Returns how many moved. */
98
+ export declare function copyConversations(from: ConversationStore, to: ConversationStore): Promise<number>;
99
+ //# sourceMappingURL=conversations.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"conversations.d.ts","sourceRoot":"","sources":["../src/conversations.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Conversations: the list a chat app keeps, and the decisions about it that
3
+ * every product used to make on its own — what a chat is called, how the
4
+ * list is grouped and searched, which chat a reload reopens. Pure, so each
5
+ * rule is tested without a DOM. `ChatApp` (`@bitbaum/chatkit/react`) is the
6
+ * screen built on them; `stores.ts` holds where they are kept.
7
+ */
8
+ /** An id that is unique enough for a client-made message or conversation,
9
+ * also where `crypto.randomUUID` is missing (plain http, old WebViews). */
10
+ export function newId(prefix = "") {
11
+ const c = globalThis.crypto;
12
+ const core = typeof c?.randomUUID === "function"
13
+ ? c.randomUUID()
14
+ : `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
15
+ return prefix ? `${prefix}-${core}` : core;
16
+ }
17
+ /** The longest title derived from a message, in characters. */
18
+ export const TITLE_MAX = 48;
19
+ /**
20
+ * What a chat is called before anyone names it: the first thing the person
21
+ * said, as one line, without the markdown, cut at a word with an ellipsis.
22
+ * `null` when there are no words (a message of only an attachment) — the
23
+ * list then shows its "New chat" label rather than an empty row. The rule
24
+ * is Heidi's `lib/domain/chat/title.ts`, (heidi repo), now the package's.
25
+ */
26
+ export function deriveTitle(text, max = TITLE_MAX) {
27
+ const line = text
28
+ .replace(/```[\s\S]*?(```|$)/g, " ")
29
+ .replace(/!\[[^\]]*\]\([^)]*\)/g, " ")
30
+ .replace(/\[([^\]]*)\]\([^)]*\)/g, "$1")
31
+ .replace(/[`*_~#>|]+/g, " ")
32
+ .replace(/\s+/g, " ")
33
+ .trim();
34
+ if (!line)
35
+ return null;
36
+ if (line.length <= max)
37
+ return line;
38
+ const cut = line.slice(0, max - 1);
39
+ const space = cut.lastIndexOf(" ");
40
+ const head = space > max * 0.5 ? cut.slice(0, space) : cut;
41
+ return `${head.replace(/[\s,.;:–—-]+$/, "")}…`;
42
+ }
43
+ function timeOf(at) {
44
+ const t = typeof at === "number" ? at : Date.parse(at);
45
+ return Number.isFinite(t) ? t : 0;
46
+ }
47
+ /** Newest first; a row with an unreadable date sinks to the bottom. */
48
+ export function sortConversations(list) {
49
+ return [...list].sort((a, b) => timeOf(b.updatedAt) - timeOf(a.updatedAt));
50
+ }
51
+ function startOfDay(ms) {
52
+ const d = new Date(ms);
53
+ d.setHours(0, 0, 0, 0);
54
+ return d.getTime();
55
+ }
56
+ /** Which heading a row sits under, by the reader's own calendar day. */
57
+ export function conversationGroupOf(at, now = Date.now()) {
58
+ const t = timeOf(at);
59
+ const today = startOfDay(now);
60
+ if (t >= today)
61
+ return "today";
62
+ // The day before by the calendar, not "24 hours ago": a chat from 23:50
63
+ // read at 00:10 is yesterday's.
64
+ const yesterday = startOfDay(today - 1);
65
+ if (t >= yesterday)
66
+ return "yesterday";
67
+ return "earlier";
68
+ }
69
+ /** The list under Today / Yesterday / Earlier, newest first, empty groups left out. */
70
+ export function groupConversations(list, now = Date.now()) {
71
+ const order = ["today", "yesterday", "earlier"];
72
+ const by = new Map(order.map((k) => [k, []]));
73
+ for (const c of sortConversations(list))
74
+ by.get(conversationGroupOf(c.updatedAt, now)).push(c);
75
+ return order.filter((k) => by.get(k).length > 0).map((key) => ({ key, items: by.get(key) }));
76
+ }
77
+ function fold(s) {
78
+ return s.normalize("NFD").replace(/\p{M}/gu, "").toLowerCase();
79
+ }
80
+ /**
81
+ * The search box over the list: every word typed must appear in the title,
82
+ * case and accents ignored ("zurich" finds "Zürich"). Order is kept. A
83
+ * product that ranks with `@bitbaum/searchkit` passes its own to `ChatApp`'s
84
+ * `search` — chatkit itself takes no dependency.
85
+ */
86
+ export function filterConversations(list, query) {
87
+ const words = fold(query).split(/\s+/).filter(Boolean);
88
+ if (words.length === 0)
89
+ return [...list];
90
+ return list.filter((c) => {
91
+ const title = fold(c.title);
92
+ return words.every((w) => title.includes(w));
93
+ });
94
+ }
95
+ /**
96
+ * The date on a row, the way the phone apps write it: the time today, the
97
+ * weekday within the week, the date after that (with the year once it is not
98
+ * this year's). In the reader's locale through `Intl`.
99
+ */
100
+ export function formatConversationDate(at, now = Date.now(), locale) {
101
+ const t = timeOf(at);
102
+ if (!t)
103
+ return "";
104
+ const d = new Date(t);
105
+ const today = startOfDay(now);
106
+ if (t >= today)
107
+ return d.toLocaleTimeString(locale, { hour: "2-digit", minute: "2-digit" });
108
+ if (t >= today - 6 * 86_400_000)
109
+ return d.toLocaleDateString(locale, { weekday: "short" });
110
+ const sameYear = d.getFullYear() === new Date(now).getFullYear();
111
+ return d.toLocaleDateString(locale, {
112
+ day: "numeric",
113
+ month: "short",
114
+ ...(sameYear ? {} : { year: "numeric" }),
115
+ });
116
+ }
117
+ /**
118
+ * Which chat a reload opens: the one asked for (a link, the URL), else the
119
+ * one that was open, but only while it still exists — a chat deleted on
120
+ * another device opens a new chat, never an error page.
121
+ */
122
+ export function pickRestored(list, wanted, remembered) {
123
+ const ids = new Set(list.map((c) => c.id));
124
+ if (wanted && ids.has(wanted))
125
+ return wanted;
126
+ if (remembered && ids.has(remembered))
127
+ return remembered;
128
+ return null;
129
+ }
130
+ /** Copy every conversation from one store into another — the signed-out
131
+ * chats into the account when someone signs in. Returns how many moved. */
132
+ export async function copyConversations(from, to) {
133
+ let moved = 0;
134
+ // Oldest first, so the newest chat is also the newest in the target.
135
+ for (const summary of [...(await from.list())].reverse()) {
136
+ const full = await from.get(summary.id);
137
+ if (!full || full.messages.length === 0)
138
+ continue;
139
+ const created = await to.create({ title: full.title });
140
+ for (const m of full.messages)
141
+ await to.append(created.id, m);
142
+ moved++;
143
+ }
144
+ return moved;
145
+ }
146
+ //# sourceMappingURL=conversations.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"conversations.js","sourceRoot":"","sources":["../src/conversations.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAgDH;4EAC4E;AAC5E,MAAM,UAAU,KAAK,CAAC,MAAM,GAAG,EAAE;IAC/B,MAAM,CAAC,GAAI,UAAyD,CAAC,MAAM,CAAC;IAC5E,MAAM,IAAI,GACR,OAAO,CAAC,EAAE,UAAU,KAAK,UAAU;QACjC,CAAC,CAAC,CAAC,CAAC,UAAU,EAAE;QAChB,CAAC,CAAC,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC;IAC9E,OAAO,MAAM,CAAC,CAAC,CAAC,GAAG,MAAM,IAAI,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;AAC7C,CAAC;AAED,+DAA+D;AAC/D,MAAM,CAAC,MAAM,SAAS,GAAG,EAAE,CAAC;AAE5B;;;;;;GAMG;AACH,MAAM,UAAU,WAAW,CAAC,IAAY,EAAE,GAAG,GAAG,SAAS;IACvD,MAAM,IAAI,GAAG,IAAI;SACd,OAAO,CAAC,qBAAqB,EAAE,GAAG,CAAC;SACnC,OAAO,CAAC,uBAAuB,EAAE,GAAG,CAAC;SACrC,OAAO,CAAC,wBAAwB,EAAE,IAAI,CAAC;SACvC,OAAO,CAAC,aAAa,EAAE,GAAG,CAAC;SAC3B,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC;SACpB,IAAI,EAAE,CAAC;IACV,IAAI,CAAC,IAAI;QAAE,OAAO,IAAI,CAAC;IACvB,IAAI,IAAI,CAAC,MAAM,IAAI,GAAG;QAAE,OAAO,IAAI,CAAC;IACpC,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,GAAG,CAAC,CAAC,CAAC;IACnC,MAAM,KAAK,GAAG,GAAG,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;IACnC,MAAM,IAAI,GAAG,KAAK,GAAG,GAAG,GAAG,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC;IAC3D,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,eAAe,EAAE,EAAE,CAAC,GAAG,CAAC;AACjD,CAAC;AAED,SAAS,MAAM,CAAC,EAAmB;IACjC,MAAM,CAAC,GAAG,OAAO,EAAE,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;IACvD,OAAO,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AACpC,CAAC;AAED,uEAAuE;AACvE,MAAM,UAAU,iBAAiB,CAAgC,IAAkB;IACjF,OAAO,CAAC,GAAG,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC;AAC7E,CAAC;AAQD,SAAS,UAAU,CAAC,EAAU;IAC5B,MAAM,CAAC,GAAG,IAAI,IAAI,CAAC,EAAE,CAAC,CAAC;IACvB,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;IACvB,OAAO,CAAC,CAAC,OAAO,EAAE,CAAC;AACrB,CAAC;AAED,wEAAwE;AACxE,MAAM,UAAU,mBAAmB,CACjC,EAAmB,EACnB,MAAc,IAAI,CAAC,GAAG,EAAE;IAExB,MAAM,CAAC,GAAG,MAAM,CAAC,EAAE,CAAC,CAAC;IACrB,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,CAAC,CAAC;IAC9B,IAAI,CAAC,IAAI,KAAK;QAAE,OAAO,OAAO,CAAC;IAC/B,wEAAwE;IACxE,gCAAgC;IAChC,MAAM,SAAS,GAAG,UAAU,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC;IACxC,IAAI,CAAC,IAAI,SAAS;QAAE,OAAO,WAAW,CAAC;IACvC,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,uFAAuF;AACvF,MAAM,UAAU,kBAAkB,CAChC,IAAkB,EAClB,MAAc,IAAI,CAAC,GAAG,EAAE;IAExB,MAAM,KAAK,GAA2B,CAAC,OAAO,EAAE,WAAW,EAAE,SAAS,CAAC,CAAC;IACxE,MAAM,EAAE,GAAG,IAAI,GAAG,CAA4B,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC;IACzE,KAAK,MAAM,CAAC,IAAI,iBAAiB,CAAC,IAAI,CAAC;QAAE,EAAE,CAAC,GAAG,CAAC,mBAAmB,CAAC,CAAC,CAAC,SAAS,EAAE,GAAG,CAAC,CAAE,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAChG,OAAO,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,CAAE,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,EAAE,KAAK,EAAE,EAAE,CAAC,GAAG,CAAC,GAAG,CAAE,EAAE,CAAC,CAAC,CAAC;AACjG,CAAC;AAED,SAAS,IAAI,CAAC,CAAS;IACrB,OAAO,CAAC,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC;AACjE,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,mBAAmB,CACjC,IAAkB,EAClB,KAAa;IAEb,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IACvD,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,CAAC,GAAG,IAAI,CAAC,CAAC;IACzC,OAAO,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE;QACvB,MAAM,KAAK,GAAG,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;QAC5B,OAAO,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC;IAC/C,CAAC,CAAC,CAAC;AACL,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,sBAAsB,CACpC,EAAmB,EACnB,MAAc,IAAI,CAAC,GAAG,EAAE,EACxB,MAAe;IAEf,MAAM,CAAC,GAAG,MAAM,CAAC,EAAE,CAAC,CAAC;IACrB,IAAI,CAAC,CAAC;QAAE,OAAO,EAAE,CAAC;IAClB,MAAM,CAAC,GAAG,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC;IACtB,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,CAAC,CAAC;IAC9B,IAAI,CAAC,IAAI,KAAK;QAAE,OAAO,CAAC,CAAC,kBAAkB,CAAC,MAAM,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC,CAAC;IAC5F,IAAI,CAAC,IAAI,KAAK,GAAG,CAAC,GAAG,UAAU;QAAE,OAAO,CAAC,CAAC,kBAAkB,CAAC,MAAM,EAAE,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,CAAC;IAC3F,MAAM,QAAQ,GAAG,CAAC,CAAC,WAAW,EAAE,KAAK,IAAI,IAAI,CAAC,GAAG,CAAC,CAAC,WAAW,EAAE,CAAC;IACjE,OAAO,CAAC,CAAC,kBAAkB,CAAC,MAAM,EAAE;QAClC,GAAG,EAAE,SAAS;QACd,KAAK,EAAE,OAAO;QACd,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC;KACzC,CAAC,CAAC;AACL,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,YAAY,CAC1B,IAAoC,EACpC,MAAiC,EACjC,UAAqC;IAErC,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAC3C,IAAI,MAAM,IAAI,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC;QAAE,OAAO,MAAM,CAAC;IAC7C,IAAI,UAAU,IAAI,GAAG,CAAC,GAAG,CAAC,UAAU,CAAC;QAAE,OAAO,UAAU,CAAC;IACzD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;4EAC4E;AAC5E,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,IAAuB,EACvB,EAAqB;IAErB,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,qEAAqE;IACrE,KAAK,MAAM,OAAO,IAAI,CAAC,GAAG,CAAC,MAAM,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC;QACzD,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;QACxC,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,KAAK,CAAC;YAAE,SAAS;QAClD,MAAM,OAAO,GAAG,MAAM,EAAE,CAAC,MAAM,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC;QACvD,KAAK,MAAM,CAAC,IAAI,IAAI,CAAC,QAAQ;YAAE,MAAM,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC;QAC9D,KAAK,EAAE,CAAC;IACV,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC"}
package/dist/index.d.ts CHANGED
@@ -13,4 +13,6 @@ export * from "./replies.js";
13
13
  export * from "./speech.js";
14
14
  export * from "./rich.js";
15
15
  export * from "./review.js";
16
+ export * from "./conversations.js";
17
+ export * from "./stores.js";
16
18
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,cAAc,eAAe,CAAC;AAC9B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC;AAC5B,cAAc,kBAAkB,CAAC;AACjC,cAAc,YAAY,CAAC;AAC3B,cAAc,cAAc,CAAC;AAC7B,cAAc,aAAa,CAAC;AAC5B,cAAc,WAAW,CAAC;AAC1B,cAAc,aAAa,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,cAAc,eAAe,CAAC;AAC9B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC;AAC5B,cAAc,kBAAkB,CAAC;AACjC,cAAc,YAAY,CAAC;AAC3B,cAAc,cAAc,CAAC;AAC7B,cAAc,aAAa,CAAC;AAC5B,cAAc,WAAW,CAAC;AAC1B,cAAc,aAAa,CAAC;AAC5B,cAAc,oBAAoB,CAAC;AACnC,cAAc,aAAa,CAAC"}
package/dist/index.js CHANGED
@@ -13,4 +13,6 @@ export * from "./replies.js";
13
13
  export * from "./speech.js";
14
14
  export * from "./rich.js";
15
15
  export * from "./review.js";
16
+ export * from "./conversations.js";
17
+ export * from "./stores.js";
16
18
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,cAAc,eAAe,CAAC;AAC9B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC;AAC5B,cAAc,kBAAkB,CAAC;AACjC,cAAc,YAAY,CAAC;AAC3B,cAAc,cAAc,CAAC;AAC7B,cAAc,aAAa,CAAC;AAC5B,cAAc,WAAW,CAAC;AAC1B,cAAc,aAAa,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,cAAc,eAAe,CAAC;AAC9B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC;AAC5B,cAAc,kBAAkB,CAAC;AACjC,cAAc,YAAY,CAAC;AAC3B,cAAc,cAAc,CAAC;AAC7B,cAAc,aAAa,CAAC;AAC5B,cAAc,WAAW,CAAC;AAC1B,cAAc,aAAa,CAAC;AAC5B,cAAc,oBAAoB,CAAC;AACnC,cAAc,aAAa,CAAC"}
@@ -0,0 +1,123 @@
1
+ import { type ReactNode } from "react";
2
+ import type { ConversationStore, ConversationSummary } from "../conversations.js";
3
+ import type { Speaker } from "../speech.js";
4
+ import type { AnswerFeedback } from "../review.js";
5
+ import { type ChatMessageData, type Starter, type ThreadLabels } from "./Thread.js";
6
+ import { type ComposerProps } from "./Composer.js";
7
+ import type { RenderLink } from "./Markdown.js";
8
+ import type { RichRenderers } from "./renderers.js";
9
+ import { type ChatAppProblem, type ChatTransport } from "./use-chat-app.js";
10
+ export type ChatAppLabels = {
11
+ chats: string;
12
+ openChats: string;
13
+ closeChats: string;
14
+ newChat: string;
15
+ /** The title of a chat nobody has named and that has no words yet. */
16
+ untitled: string;
17
+ more: string;
18
+ temporary: string;
19
+ /** Under the title while a chat is temporary. */
20
+ temporaryNote: string;
21
+ share: string;
22
+ rename: string;
23
+ renameField: string;
24
+ delete: string;
25
+ deleteAsk: (title: string) => string;
26
+ save: string;
27
+ cancel: string;
28
+ search: string;
29
+ today: string;
30
+ yesterday: string;
31
+ earlier: string;
32
+ noChats: string;
33
+ noMatch: (query: string) => string;
34
+ loadingChats: string;
35
+ listFailed: string;
36
+ tryAgain: string;
37
+ answering: string;
38
+ chatActions: (title: string) => string;
39
+ queued: string;
40
+ unqueue: (text: string) => string;
41
+ dismiss: string;
42
+ problems: Record<ChatAppProblem, string>;
43
+ };
44
+ export declare const DEFAULT_CHAT_APP_LABELS: ChatAppLabels;
45
+ export type ChatAppProps = {
46
+ /** Send a turn and resolve with the answer (stream through `turn.update`). */
47
+ transport: ChatTransport;
48
+ /** Where chats live. Default: on this device (`localConversationStore`). */
49
+ store?: ConversationStore;
50
+ /** The composer's placeholder. */
51
+ placeholder?: string;
52
+ /** Over a new chat, where a chat's title goes (the product's name). */
53
+ name?: string;
54
+ /** Name a chat after its first answer. Without it, the first message names it. */
55
+ makeTitle?: (first: {
56
+ text: string;
57
+ answer: string;
58
+ }) => string | null | Promise<string | null>;
59
+ /** Starters on an empty chat; or your own `empty`. */
60
+ starters?: readonly Starter[];
61
+ startersTitle?: ReactNode;
62
+ empty?: ReactNode;
63
+ /** Composer options: `voice` (give it `transcribeUrl`), `attach` (on by
64
+ * default; `false` only when the product's API takes no files), `tools`,
65
+ * `labels`… Sending, Stop and the queue are ChatApp's. */
66
+ composer?: Partial<Omit<ComposerProps, "onSend" | "sending" | "onStop" | "queue">>;
67
+ /** Thread options, passed to ChatThread. */
68
+ thread?: {
69
+ renderLink?: RenderLink;
70
+ renderFooter?: (m: ChatMessageData) => ReactNode;
71
+ renderers?: RichRenderers;
72
+ speaker?: Speaker | null;
73
+ share?: {
74
+ title?: string;
75
+ url?: string;
76
+ } | false;
77
+ onFeedback?: (feedback: AnswerFeedback & {
78
+ conversationId: string | null;
79
+ }) => void;
80
+ labels?: Partial<ThreadLabels>;
81
+ };
82
+ /** One quiet line under the composer. */
83
+ footnote?: ReactNode;
84
+ /** "viewport" (default) follows the visual viewport, above the phone keyboard. */
85
+ fill?: "viewport" | "parent";
86
+ /** Offer Temporary chat. Default true. */
87
+ temporaryChats?: boolean;
88
+ /** Offer "Share this chat" in the header's menu. */
89
+ onShare?: (chat: {
90
+ id: string;
91
+ title: string;
92
+ messages: ChatMessageData[];
93
+ }) => void;
94
+ /** The open chat, when the product keeps it in its URL; `null` = a new chat. */
95
+ conversationId?: string | null;
96
+ onConversationChange?: (id: string | null) => void;
97
+ /** Device key that remembers the open chat across a reload; `null` = never. */
98
+ rememberKey?: string | null;
99
+ /** Your own list search (e.g. `@bitbaum/searchkit`); default: every word in the title. */
100
+ search?: (list: ConversationSummary[], query: string) => ConversationSummary[];
101
+ /** Product controls in the header: before the title, and before New chat. */
102
+ headerStart?: ReactNode;
103
+ headerEnd?: ReactNode;
104
+ /** Dates in the list. Default: the browser's. */
105
+ locale?: string;
106
+ /** When the chat list stops being a drawer and stays open beside the chat. */
107
+ railQuery?: string;
108
+ labels?: Partial<ChatAppLabels>;
109
+ className?: string;
110
+ };
111
+ /**
112
+ * The whole chat app, the way the Claude, ChatGPT and Grok phone apps are
113
+ * laid out: the conversation edge to edge, a frosted header (Chats · title ·
114
+ * New chat · More), the chat list in a drawer on a phone and a rail beside
115
+ * the chat from a wide screen, and the composer floating at the bottom with
116
+ * the microphone, Camera · Photos · Files and the queue.
117
+ *
118
+ * A product gives it two things — a `transport` that answers a turn, and a
119
+ * `store` that keeps chats (or nothing: the device then keeps them) — plus
120
+ * its words. Everything a person sees and does is the same in every product.
121
+ */
122
+ export declare function ChatApp({ transport, store: givenStore, placeholder, name, makeTitle, starters, startersTitle, empty, composer, thread, footnote, fill, temporaryChats, onShare, conversationId, onConversationChange, rememberKey, search, headerStart, headerEnd, locale, railQuery, labels: labelOverrides, className, }: ChatAppProps): import("react").JSX.Element;
123
+ //# sourceMappingURL=ChatApp.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ChatApp.d.ts","sourceRoot":"","sources":["../../src/react/ChatApp.tsx"],"names":[],"mappings":"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"}