@bitbaum/chatkit 0.18.1 → 0.19.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/README.md +161 -0
- package/dist/react/AttachMenu.d.ts +18 -1
- package/dist/react/AttachMenu.d.ts.map +1 -1
- package/dist/react/AttachMenu.js +8 -4
- package/dist/react/AttachMenu.js.map +1 -1
- package/dist/react/ChatApp.d.ts +79 -5
- package/dist/react/ChatApp.d.ts.map +1 -1
- package/dist/react/ChatApp.js +113 -7
- package/dist/react/ChatApp.js.map +1 -1
- package/dist/react/ChatHistory.d.ts +7 -1
- package/dist/react/ChatHistory.d.ts.map +1 -1
- package/dist/react/ChatHistory.js +2 -2
- package/dist/react/ChatHistory.js.map +1 -1
- package/dist/react/Composer.d.ts +20 -3
- package/dist/react/Composer.d.ts.map +1 -1
- package/dist/react/Composer.js +19 -4
- package/dist/react/Composer.js.map +1 -1
- package/dist/react/Thread.d.ts +48 -2
- package/dist/react/Thread.d.ts.map +1 -1
- package/dist/react/Thread.js +13 -6
- package/dist/react/Thread.js.map +1 -1
- package/dist/react/attach.d.ts +1 -1
- package/dist/react/attach.d.ts.map +1 -1
- package/dist/react/index.d.ts +10 -10
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +4 -4
- package/dist/react/index.js.map +1 -1
- package/dist/react/use-attachments.d.ts +17 -0
- package/dist/react/use-attachments.d.ts.map +1 -1
- package/dist/react/use-attachments.js +89 -72
- package/dist/react/use-attachments.js.map +1 -1
- package/dist/react/use-chat-app.d.ts +53 -3
- package/dist/react/use-chat-app.d.ts.map +1 -1
- package/dist/react/use-chat-app.js +126 -15
- package/dist/react/use-chat-app.js.map +1 -1
- package/package.json +1 -1
- package/styles.css +64 -1
package/README.md
CHANGED
|
@@ -174,11 +174,172 @@ reopens the chat, a picture staged above the text and removed, delete, the
|
|
|
174
174
|
queue and Stop — and checks for sideways scroll, 44px targets and cut words
|
|
175
175
|
at every step.
|
|
176
176
|
|
|
177
|
+
#### What a product adds to it (0.19)
|
|
178
|
+
|
|
179
|
+
The first four products to adopt ChatApp (Heidi, OrangeCat, Solon,
|
|
180
|
+
Substrata) each listed what it could not do. These close those gaps; every
|
|
181
|
+
one is optional, and 0.18 code keeps working unchanged.
|
|
182
|
+
|
|
183
|
+
**Send from the page — the handle.** `<ChatApp ref={chat}>` gives
|
|
184
|
+
`ChatAppHandle`: `send(text, attachments?, { newChat?, chips? })`,
|
|
185
|
+
`fill(text, files?)`, `focus()`, `newChat()`, `openConversation(id)`.
|
|
186
|
+
`send` behaves exactly like the composer's Send (queued behind a running
|
|
187
|
+
answer, a chat created when none is open); `File`s are read like the
|
|
188
|
+
paperclip's, and if one cannot be attached nothing is sent — the words and
|
|
189
|
+
files go into the box with the reason. Never click `.ck-send` from outside.
|
|
190
|
+
|
|
191
|
+
```tsx
|
|
192
|
+
const chat = useRef<ChatAppHandle>(null);
|
|
193
|
+
<ChatApp ref={chat} transport={transport} />;
|
|
194
|
+
<button onClick={() => chat.current?.send("Check this figure", [], { newChat: true })}>Check this</button>;
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
**A message the page arrives with.** `initialMessage` is sent once, in a new
|
|
198
|
+
chat, after the list has loaded (StrictMode's double mount sends it once).
|
|
199
|
+
`send: false` only fills the box. Clear it from your URL or state yourself.
|
|
200
|
+
|
|
201
|
+
```tsx
|
|
202
|
+
<ChatApp transport={transport}
|
|
203
|
+
initialMessage={handoff ? { text: handoff.text, attachments: handoff.files } : null} />
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
**`?q=` links.** `deepLinkParam="q"` reads the parameter into a new chat's
|
|
207
|
+
box and takes it off the address. A link never sends for someone: only with
|
|
208
|
+
`deepLinkAutoSend` too.
|
|
209
|
+
|
|
210
|
+
```tsx
|
|
211
|
+
<ChatApp transport={transport} deepLinkParam="q" />
|
|
212
|
+
// /chat?q=How%20do%20invoices%20expire → the box says it; the person sends.
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
**Why a turn failed, per turn.** Throw `ChatTurnError(message, { kind })`
|
|
216
|
+
from the transport: that bubble shows `message` and keeps `failure.kind`
|
|
217
|
+
(for a footer to link the fix). Two failures in one chat keep their own
|
|
218
|
+
words. Any other error shows the `failed` label, never its raw message —
|
|
219
|
+
unless `failureMessage(error)` maps it.
|
|
220
|
+
|
|
221
|
+
```tsx
|
|
222
|
+
import { ChatTurnError } from "@bitbaum/chatkit/react";
|
|
223
|
+
if (res.status === 503) throw new ChatTurnError("The model is not set up here.", { kind: "not-configured" });
|
|
224
|
+
<ChatApp transport={transport} failureMessage={(e) => (navigator.onLine ? null : "You are offline.")} />;
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
**Retry sends the pictures again.** The files of a failed turn stay with it
|
|
228
|
+
until its answer lands; Retry re-sends the words and the files. Nothing to
|
|
229
|
+
configure.
|
|
230
|
+
|
|
231
|
+
**Stop and the half answer.** Stop keeps what had arrived (as the apps do).
|
|
232
|
+
`stopKeepsPartial={false}` drops it for every turn; a transport decides per
|
|
233
|
+
turn with `turn.stopKeepsPartial(keep)` — `false` while the text is unchecked.
|
|
234
|
+
|
|
235
|
+
```tsx
|
|
236
|
+
transport={async (turn) => {
|
|
237
|
+
turn.stopKeepsPartial(false); // the gate has not checked this text yet
|
|
238
|
+
/* … stream … */ turn.stopKeepsPartial(true); return answer; }}
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
**Starters with a language.** A starter is `string | { label, prompt?, lang? }
|
|
242
|
+
| { text, label?, lang? }`; `lang` goes on the button.
|
|
243
|
+
|
|
244
|
+
```tsx
|
|
245
|
+
<ChatApp transport={transport}
|
|
246
|
+
starters={["Explain this word", { text: "Grüezi mitenand", lang: "gsw" }]} />
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
**The chat list's own line.** `historySlot` sits under the search box, above
|
|
250
|
+
the chats; `historyEmpty` replaces "No chats yet…".
|
|
251
|
+
|
|
252
|
+
```tsx
|
|
253
|
+
<ChatApp transport={transport}
|
|
254
|
+
historySlot={signedOut && <a href="/login">Only in this browser · Sign in to keep it</a>}
|
|
255
|
+
historyEmpty={<p>What you ask is kept here.</p>} />
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
**Product rows in the attach sheet.** `composer.attachExtras` — `{ id,
|
|
259
|
+
label, detail?, icon?, onPick }[]` — are rows under Camera · Photos · Files,
|
|
260
|
+
one anatomy (icon, label, second line), in the same sheet. With `attach:
|
|
261
|
+
false` the paperclip opens a sheet of only these rows. (`AttachMenu rows`
|
|
262
|
+
takes the same list.)
|
|
263
|
+
|
|
264
|
+
```tsx
|
|
265
|
+
<ChatApp transport={transport}
|
|
266
|
+
composer={{ attachExtras: [{ id: "things", label: "Your things", onPick: openThings }],
|
|
267
|
+
heldAttachments: held.length }} />
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
**The product's things on the sent message.** `chipsFor({ text, attachments
|
|
271
|
+
})` returns `MessageChip[]` (`{ label, href?, kind? }`) shown on the person's
|
|
272
|
+
message the moment it is sent, and kept with it (`ChatMessageData.chips`).
|
|
273
|
+
`send(…, { chips })` does the same from the handle.
|
|
274
|
+
|
|
275
|
+
```tsx
|
|
276
|
+
<ChatApp transport={transport}
|
|
277
|
+
chipsFor={() => held.map((r) => ({ label: r.title, href: r.url }))} />
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
**A temporary chat ended.** `onTemporaryEnd(chatId)` fires once when a
|
|
281
|
+
temporary chat is left (New chat, another chat, Temporary off), the page is
|
|
282
|
+
hidden, or ChatApp unmounts. `turn.chatId` is the same id, so a server row
|
|
283
|
+
kept for the chat can be keyed by it and deleted here.
|
|
284
|
+
|
|
285
|
+
```tsx
|
|
286
|
+
<ChatApp transport={(turn) => myApi.send({ ...turn, hiddenRowKey: turn.chatId })}
|
|
287
|
+
onTemporaryEnd={(chatId) => myApi.deleteHiddenRow(chatId)} />
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
**Footers that know the latest answer, and can send.** `thread.renderFooter(m,
|
|
291
|
+
{ isLatest, index, send, fill })`: put "what next" only under the latest,
|
|
292
|
+
and let a chip whose label differs from its sentence send in one tap.
|
|
293
|
+
|
|
294
|
+
```tsx
|
|
295
|
+
thread={{ renderFooter: (m, { isLatest, send }) =>
|
|
296
|
+
isLatest && <button className="ck-reply" onClick={() => send("Make it shorter.")}>Shorter</button> }}
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
**After the last turn.** `thread.after` is drawn inside the thread, after the
|
|
300
|
+
last turn (a pending action, a picker), not over the composer.
|
|
301
|
+
|
|
302
|
+
`demo/app.html?lab=1` is the page these are checked on: StrictMode around
|
|
303
|
+
ChatApp, the handle on `window.ckChat`. `test/browser-app.mjs` drives it at
|
|
304
|
+
390px and 1440px.
|
|
305
|
+
|
|
177
306
|
#### Moving a product onto ChatApp
|
|
178
307
|
|
|
179
308
|
Per product, from the matrix: what goes, what stays. What stays is always
|
|
180
309
|
the same two things — the transport and the words.
|
|
181
310
|
|
|
311
|
+
**Workarounds 0.19 makes deletable** (each was written in an adoption PR
|
|
312
|
+
because ChatApp 0.18 could not do it — bitbaum/heidi#226, bitbaum/orangecat#1447,
|
|
313
|
+
bitbaum/solon#309, bitbaum/substrata#262):
|
|
314
|
+
|
|
315
|
+
- **Substrata** — `lib/chat/drive-composer.ts` (types into the composer and
|
|
316
|
+
presses Send) goes: `components/portal/chat/useAutoAsk.ts` and
|
|
317
|
+
`lib/chat/handoff.ts` hand their words and pictures to `initialMessage` (or
|
|
318
|
+
`ref.send(text, files, { newChat: true })`), and `/chat?q=` becomes
|
|
319
|
+
`deepLinkParam="q"`. Retry now re-sends the pictures.
|
|
320
|
+
- **OrangeCat** — in `src/components/ai-chat/ModernChatPanel/index.tsx`, the
|
|
321
|
+
bridge that sets the composer value and clicks `button.ck-send` goes (`?q=`
|
|
322
|
+
and `askCat` → the handle), and so does the ephemeral-row cleanup hung on
|
|
323
|
+
the empty state mounting and `onConversationChange` (→ `onTemporaryEnd`,
|
|
324
|
+
keyed by `turn.chatId` in `hooks/useCatTransport.ts`). "Your things" moves
|
|
325
|
+
from a second button into the sheet (`composer.attachExtras`), its staged
|
|
326
|
+
refs show on the sent bubble (`chipsFor`), `CatAnswerFooter` shows the
|
|
327
|
+
create links and upgrade nudge only under `isLatest`, and pending actions
|
|
328
|
+
move from `composer.above` to `thread.after`. `TurnFailure` reads the
|
|
329
|
+
turn's own `failure.kind` (throw `ChatTurnError`).
|
|
330
|
+
- **Heidi** — in `app/[locale]/_components/chat/workspace.tsx`, the
|
|
331
|
+
latest-failure state passed as `thread.labels.failed` goes (throw
|
|
332
|
+
`ChatTurnError` from `chat-app.ts`'s transport mapping), and so does
|
|
333
|
+
`turn.update({ text: "" })` before a throw on Stop (→
|
|
334
|
+
`turn.stopKeepsPartial(false)` until the variety gate has checked the
|
|
335
|
+
text). The move and learn chips and the word pick send in one tap again
|
|
336
|
+
through `renderFooter`'s `send`; the dialect starter gets `lang`; the
|
|
337
|
+
signed-out "Sign in to keep it" line moves from `startersTitle` to
|
|
338
|
+
`historySlot`.
|
|
339
|
+
- **Solon** — nothing to delete. The per-answer "Answered by <model>" fits
|
|
340
|
+
`thread.renderFooter` (it existed in 0.18; it now also gets `isLatest`).
|
|
341
|
+
ChatApp's own words still come only in English: pass `labels`.
|
|
342
|
+
|
|
182
343
|
- **Loki** — goes: `ThreadRail.tsx`, the history half of
|
|
183
344
|
`WorkspaceDrawers.tsx` and `WorkspaceChrome.tsx`, `ThreadTitle.tsx`,
|
|
184
345
|
`hooks/use-temporary-chat.ts`, `hooks/use-new-chat.ts`,
|
|
@@ -9,6 +9,19 @@ export type AttachMenuLabels = {
|
|
|
9
9
|
sources: Record<AttachSource, string>;
|
|
10
10
|
};
|
|
11
11
|
export declare const DEFAULT_ATTACH_MENU_LABELS: AttachMenuLabels;
|
|
12
|
+
/**
|
|
13
|
+
* One of the app's own rows in the attach sheet, under Camera · Photos ·
|
|
14
|
+
* Files: an icon, what it adds (`label`), an optional second line. Every row
|
|
15
|
+
* has this one anatomy, so a product's "Your things" reads as part of the
|
|
16
|
+
* same sheet, not a second menu. Picking a row closes the sheet first.
|
|
17
|
+
*/
|
|
18
|
+
export type AttachExtra = {
|
|
19
|
+
id: string;
|
|
20
|
+
label: string;
|
|
21
|
+
detail?: string;
|
|
22
|
+
icon?: ReactNode;
|
|
23
|
+
onPick: () => void;
|
|
24
|
+
};
|
|
12
25
|
/**
|
|
13
26
|
* The paperclip, and what it opens on a phone: a sheet with Camera, Photos and
|
|
14
27
|
* Files — one input each. See `ATTACH_SOURCE_INPUT` for why three inputs and
|
|
@@ -27,7 +40,7 @@ export declare const DEFAULT_ATTACH_MENU_LABELS: AttachMenuLabels;
|
|
|
27
40
|
* they sit under Camera · Photos · Files in the same sheet, on every pointer,
|
|
28
41
|
* since a mouse cannot reach them through a file dialog.
|
|
29
42
|
*/
|
|
30
|
-
export declare function AttachMenu({ onFiles, disabled, labels, sources, trigger, extra, onOpenChange, }: {
|
|
43
|
+
export declare function AttachMenu({ onFiles, disabled, labels, sources, trigger, extra, rows, onOpenChange, }: {
|
|
31
44
|
onFiles: (files: FileList | null) => void;
|
|
32
45
|
disabled?: boolean;
|
|
33
46
|
labels?: AttachMenuLabels;
|
|
@@ -41,6 +54,10 @@ export declare function AttachMenu({ onFiles, disabled, labels, sources, trigger
|
|
|
41
54
|
* sheet opens on every pointer. A row that should close the sheet calls
|
|
42
55
|
* the `close` it is given. */
|
|
43
56
|
extra?: (close: () => void) => ReactNode;
|
|
57
|
+
/** The app's rows as data — drawn with one anatomy (see `AttachExtra`).
|
|
58
|
+
* With them the sheet opens on every pointer; with no `sources` it holds
|
|
59
|
+
* only them. */
|
|
60
|
+
rows?: readonly AttachExtra[];
|
|
44
61
|
onOpenChange?: (open: boolean) => void;
|
|
45
62
|
}): import("react").JSX.Element;
|
|
46
63
|
//# sourceMappingURL=AttachMenu.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AttachMenu.d.ts","sourceRoot":"","sources":["../../src/react/AttachMenu.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAoB,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AACzD,OAAO,EAAuB,KAAK,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAI3E,MAAM,MAAM,gBAAgB,GAAG;IAC7B,4BAA4B;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,yBAAyB;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC,YAAY,EAAE,MAAM,CAAC,CAAC;CACvC,CAAC;AAEF,eAAO,MAAM,0BAA0B,EAAE,gBAKxC,CAAC;AASF;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,UAAU,CAAC,EACzB,OAAO,EACP,QAAgB,EAChB,MAAmC,EACnC,OAAiB,EACjB,OAAO,EACP,KAAK,EACL,YAAY,GACb,EAAE;IACD,OAAO,EAAE,CAAC,KAAK,EAAE,QAAQ,GAAG,IAAI,KAAK,IAAI,CAAC;IAC1C,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,MAAM,CAAC,EAAE,gBAAgB,CAAC;IAC1B;8EAC0E;IAC1E,OAAO,CAAC,EAAE,SAAS,YAAY,EAAE,CAAC;IAClC;yEACqE;IACrE,OAAO,CAAC,EAAE,SAAS,CAAC;IACpB;;mCAE+B;IAC/B,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,IAAI,KAAK,SAAS,CAAC;IACzC,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;CACxC,+
|
|
1
|
+
{"version":3,"file":"AttachMenu.d.ts","sourceRoot":"","sources":["../../src/react/AttachMenu.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAoB,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AACzD,OAAO,EAAuB,KAAK,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAI3E,MAAM,MAAM,gBAAgB,GAAG;IAC7B,4BAA4B;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,yBAAyB;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC,YAAY,EAAE,MAAM,CAAC,CAAC;CACvC,CAAC;AAEF,eAAO,MAAM,0BAA0B,EAAE,gBAKxC,CAAC;AAEF;;;;;GAKG;AACH,MAAM,MAAM,WAAW,GAAG;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,MAAM,EAAE,MAAM,IAAI,CAAC;CACpB,CAAC;AASF;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,UAAU,CAAC,EACzB,OAAO,EACP,QAAgB,EAChB,MAAmC,EACnC,OAAiB,EACjB,OAAO,EACP,KAAK,EACL,IAAI,EACJ,YAAY,GACb,EAAE;IACD,OAAO,EAAE,CAAC,KAAK,EAAE,QAAQ,GAAG,IAAI,KAAK,IAAI,CAAC;IAC1C,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,MAAM,CAAC,EAAE,gBAAgB,CAAC;IAC1B;8EAC0E;IAC1E,OAAO,CAAC,EAAE,SAAS,YAAY,EAAE,CAAC;IAClC;yEACqE;IACrE,OAAO,CAAC,EAAE,SAAS,CAAC;IACpB;;mCAE+B;IAC/B,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,IAAI,KAAK,SAAS,CAAC;IACzC;;qBAEiB;IACjB,IAAI,CAAC,EAAE,SAAS,WAAW,EAAE,CAAC;IAC9B,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;CACxC,+BAkGA"}
|
package/dist/react/AttachMenu.js
CHANGED
|
@@ -34,7 +34,7 @@ const ICONS = {
|
|
|
34
34
|
* they sit under Camera · Photos · Files in the same sheet, on every pointer,
|
|
35
35
|
* since a mouse cannot reach them through a file dialog.
|
|
36
36
|
*/
|
|
37
|
-
export function AttachMenu({ onFiles, disabled = false, labels = DEFAULT_ATTACH_MENU_LABELS, sources = SOURCES, trigger, extra, onOpenChange, }) {
|
|
37
|
+
export function AttachMenu({ onFiles, disabled = false, labels = DEFAULT_ATTACH_MENU_LABELS, sources = SOURCES, trigger, extra, rows, onOpenChange, }) {
|
|
38
38
|
const inputs = useRef({});
|
|
39
39
|
const [sheetOpen, setSheetOpen] = useState(false);
|
|
40
40
|
const setOpen = (next) => {
|
|
@@ -46,8 +46,9 @@ export function AttachMenu({ onFiles, disabled = false, labels = DEFAULT_ATTACH_
|
|
|
46
46
|
close();
|
|
47
47
|
inputs.current[source]?.click();
|
|
48
48
|
};
|
|
49
|
+
const hasRows = Boolean(extra) || (rows?.length ?? 0) > 0;
|
|
49
50
|
const open = () => {
|
|
50
|
-
if (
|
|
51
|
+
if (hasRows || (sources.length > 1 && prefersSheet()))
|
|
51
52
|
setOpen(true);
|
|
52
53
|
// A mouse gets the broadest picker offered: Files when it is one of the
|
|
53
54
|
// sources, else the last one (Photos, for an image uploader).
|
|
@@ -59,9 +60,12 @@ export function AttachMenu({ onFiles, disabled = false, labels = DEFAULT_ATTACH_
|
|
|
59
60
|
}, type: "file", hidden: true, multiple: source !== "camera", "data-ck-source": source, ...ATTACH_SOURCE_INPUT[source], onChange: (e) => {
|
|
60
61
|
onFiles(e.target.files);
|
|
61
62
|
e.target.value = "";
|
|
62
|
-
} }, source))), _jsx("button", { type: "button", className: trigger ? "ck-attach-trigger" : "ck-icon-btn", onClick: open, disabled: disabled, "aria-label": labels.attach, "aria-haspopup": "dialog", title: labels.attach, children: trigger ?? _jsx(IconPaperclip, {}) }), _jsxs(Sheet, { open: sheetOpen, onClose: close, title: labels.title, closeLabel: labels.close, children: [_jsx("div", { className: "ck-attach-sources", children: sources.map((source) => {
|
|
63
|
+
} }, source))), _jsx("button", { type: "button", className: trigger ? "ck-attach-trigger" : "ck-icon-btn", onClick: open, disabled: disabled, "aria-label": labels.attach, "aria-haspopup": "dialog", title: labels.attach, children: trigger ?? _jsx(IconPaperclip, {}) }), _jsxs(Sheet, { open: sheetOpen, onClose: close, title: labels.title, closeLabel: labels.close, children: [sources.length > 0 && (_jsx("div", { className: "ck-attach-sources", children: sources.map((source) => {
|
|
63
64
|
const Icon = ICONS[source];
|
|
64
65
|
return (_jsxs("button", { type: "button", className: "ck-attach-source", "data-ck-source": source, onClick: () => pick(source), children: [_jsx(Icon, {}), _jsx("span", { children: labels.sources[source] })] }, source));
|
|
65
|
-
}) }),
|
|
66
|
+
}) })), hasRows && (_jsxs("div", { className: sources.length > 0 ? "ck-sheet-extra" : "ck-sheet-extra ck-sheet-only", children: [rows?.map((row) => (_jsxs("button", { type: "button", className: "ck-attach-extra", "data-ck-extra": row.id, onClick: () => {
|
|
67
|
+
close();
|
|
68
|
+
row.onPick();
|
|
69
|
+
}, children: [_jsx("span", { className: "ck-attach-extra-icon", "aria-hidden": true, children: row.icon ?? _jsx(IconPaperclip, {}) }), _jsxs("span", { className: "ck-attach-extra-text", children: [_jsx("span", { className: "ck-truncate", children: row.label }), row.detail && _jsx("small", { className: "ck-truncate", children: row.detail })] })] }, row.id))), extra?.(close)] }))] })] }));
|
|
66
70
|
}
|
|
67
71
|
//# sourceMappingURL=AttachMenu.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AttachMenu.js","sourceRoot":"","sources":["../../src/react/AttachMenu.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAEb,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAkB,MAAM,OAAO,CAAC;AACzD,OAAO,EAAE,mBAAmB,EAAqB,MAAM,mBAAmB,CAAC;AAC3E,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAC9E,OAAO,EAAE,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAWjD,MAAM,CAAC,MAAM,0BAA0B,GAAqB;IAC1D,MAAM,EAAE,6BAA6B;IACrC,KAAK,EAAE,gBAAgB;IACvB,KAAK,EAAE,OAAO;IACd,OAAO,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,KAAK,EAAE,OAAO,EAAE;CAChE,CAAC;
|
|
1
|
+
{"version":3,"file":"AttachMenu.js","sourceRoot":"","sources":["../../src/react/AttachMenu.tsx"],"names":[],"mappings":"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"}
|
package/dist/react/ChatApp.d.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { type ReactNode } from "react";
|
|
2
|
+
import type { Attachment } from "../attachments.js";
|
|
2
3
|
import type { ConversationStore, ConversationSummary } from "../conversations.js";
|
|
3
4
|
import type { Speaker } from "../speech.js";
|
|
4
5
|
import type { AnswerFeedback } from "../review.js";
|
|
5
|
-
import { type ChatMessageData, type Starter, type ThreadLabels } from "./Thread.js";
|
|
6
|
+
import { type ChatMessageData, type FooterInfo, type MessageChip, type Starter, type ThreadLabels } from "./Thread.js";
|
|
6
7
|
import { type ComposerProps } from "./Composer.js";
|
|
7
8
|
import type { RenderLink } from "./Markdown.js";
|
|
8
9
|
import type { RichRenderers } from "./renderers.js";
|
|
9
|
-
import { type ChatAppProblem, type ChatTransport } from "./use-chat-app.js";
|
|
10
|
+
import { type ChatAppProblem, type ChatTransport, type SendOptions } from "./use-chat-app.js";
|
|
10
11
|
export type ChatAppLabels = {
|
|
11
12
|
chats: string;
|
|
12
13
|
openChats: string;
|
|
@@ -42,6 +43,42 @@ export type ChatAppLabels = {
|
|
|
42
43
|
problems: Record<ChatAppProblem, string>;
|
|
43
44
|
};
|
|
44
45
|
export declare const DEFAULT_CHAT_APP_LABELS: ChatAppLabels;
|
|
46
|
+
/**
|
|
47
|
+
* What a page can do to the chat it holds (`<ChatApp ref={chat}>`): send for
|
|
48
|
+
* the person ("Check this", `/chat?q=`, a message handed over from a home
|
|
49
|
+
* box), put words in the box for them to finish, and move between chats.
|
|
50
|
+
*/
|
|
51
|
+
export type ChatAppHandle = {
|
|
52
|
+
/**
|
|
53
|
+
* Send now, exactly as if typed and sent: queued behind a running answer,
|
|
54
|
+
* a new chat created when none is open (or always, with `newChat`). Files
|
|
55
|
+
* are read like the paperclip's (limits, shrinking); if one cannot be
|
|
56
|
+
* attached, nothing is sent — the words and files go into the box with the
|
|
57
|
+
* reason, for the person to decide. Resolves whether it was sent.
|
|
58
|
+
*/
|
|
59
|
+
send: (text: string, attachments?: readonly (Attachment | File)[], options?: SendOptions) => Promise<boolean>;
|
|
60
|
+
/** Put words (and files) in the box and the cursor after them; nothing is sent. */
|
|
61
|
+
fill: (text: string, files?: readonly File[]) => void;
|
|
62
|
+
focus: () => void;
|
|
63
|
+
newChat: () => void;
|
|
64
|
+
openConversation: (id: string) => void;
|
|
65
|
+
};
|
|
66
|
+
/** A message the page arrives with (see `initialMessage`). */
|
|
67
|
+
export type InitialMessage = {
|
|
68
|
+
text: string;
|
|
69
|
+
/** Pictures and files with it: `File`s as picked, or wire attachments. */
|
|
70
|
+
attachments?: readonly (Attachment | File)[];
|
|
71
|
+
chips?: readonly MessageChip[];
|
|
72
|
+
/** Send it (the default), or only put it in the box (`false`). */
|
|
73
|
+
send?: boolean;
|
|
74
|
+
};
|
|
75
|
+
/** What `thread.renderFooter` gets beside the message. */
|
|
76
|
+
export type ChatAppFooterInfo = FooterInfo & {
|
|
77
|
+
/** Send for the person — a chip whose label differs from what it says. */
|
|
78
|
+
send: (text: string, options?: SendOptions) => void;
|
|
79
|
+
/** Put words in the box for the person to finish. */
|
|
80
|
+
fill: (text: string) => void;
|
|
81
|
+
};
|
|
45
82
|
export type ChatAppProps = {
|
|
46
83
|
/** Send a turn and resolve with the answer (stream through `turn.update`). */
|
|
47
84
|
transport: ChatTransport;
|
|
@@ -63,11 +100,13 @@ export type ChatAppProps = {
|
|
|
63
100
|
/** Composer options: `voice` (give it `transcribeUrl`), `attach` (on by
|
|
64
101
|
* default; `false` only when the product's API takes no files), `tools`,
|
|
65
102
|
* `labels`… Sending, Stop and the queue are ChatApp's. */
|
|
66
|
-
composer?: Partial<Omit<ComposerProps, "onSend" | "sending" | "onStop" | "queue">>;
|
|
103
|
+
composer?: Partial<Omit<ComposerProps, "onSend" | "sending" | "onStop" | "queue" | "handleRef">>;
|
|
67
104
|
/** Thread options, passed to ChatThread. */
|
|
68
105
|
thread?: {
|
|
69
106
|
renderLink?: RenderLink;
|
|
70
|
-
renderFooter?: (m: ChatMessageData) => ReactNode;
|
|
107
|
+
renderFooter?: (m: ChatMessageData, info: ChatAppFooterInfo) => ReactNode;
|
|
108
|
+
/** After the last turn, inside the thread (a pending action, a picker). */
|
|
109
|
+
after?: ReactNode;
|
|
71
110
|
renderers?: RichRenderers;
|
|
72
111
|
speaker?: Speaker | null;
|
|
73
112
|
share?: {
|
|
@@ -107,7 +146,42 @@ export type ChatAppProps = {
|
|
|
107
146
|
railQuery?: string;
|
|
108
147
|
labels?: Partial<ChatAppLabels>;
|
|
109
148
|
className?: string;
|
|
149
|
+
/** A message to start with, in a NEW chat, once the list has loaded — the
|
|
150
|
+
* home box handing its words and pictures to `/chat`. Used once per mount
|
|
151
|
+
* (StrictMode's double mount included); clear it from your URL or state
|
|
152
|
+
* yourself so a reload does not send it again. */
|
|
153
|
+
initialMessage?: InitialMessage | null;
|
|
154
|
+
/** Read this query parameter (e.g. `"q"`) into a new chat's box, then take
|
|
155
|
+
* it off the address. The words are only put in the box: a link can never
|
|
156
|
+
* send for someone unless `deepLinkAutoSend` is set too. */
|
|
157
|
+
deepLinkParam?: string;
|
|
158
|
+
/** Send the `deepLinkParam` words instead of filling the box. Off by default. */
|
|
159
|
+
deepLinkAutoSend?: boolean;
|
|
160
|
+
/** Stop keeps the half answer that had arrived. Default true; a transport
|
|
161
|
+
* can decide per turn with `turn.stopKeepsPartial(keep)`. */
|
|
162
|
+
stopKeepsPartial?: boolean;
|
|
163
|
+
/** The words for a failed turn whose error is not a `ChatTurnError`
|
|
164
|
+
* (return null for the thread's `failed` label). */
|
|
165
|
+
failureMessage?: (error: unknown) => string | null | undefined;
|
|
166
|
+
/** A temporary chat ended (left, page hidden, unmounted): delete what the
|
|
167
|
+
* server kept for it. Gets `turn.chatId`; called once per chat. */
|
|
168
|
+
onTemporaryEnd?: (chatId: string) => void;
|
|
169
|
+
/** The product's things shown on the person's message when it is sent
|
|
170
|
+
* from the box (the ones staged with `heldAttachments`). */
|
|
171
|
+
chipsFor?: (turn: {
|
|
172
|
+
text: string;
|
|
173
|
+
attachments: Attachment[];
|
|
174
|
+
}) => readonly MessageChip[] | null | undefined;
|
|
175
|
+
/** In the chat list, under the search box: the product's own line. */
|
|
176
|
+
historySlot?: ReactNode;
|
|
177
|
+
/** In the chat list, instead of "No chats yet…" when there are none. */
|
|
178
|
+
historyEmpty?: ReactNode;
|
|
110
179
|
};
|
|
180
|
+
/** The words a `?q=`-style link carries, and the address without them. Pure. */
|
|
181
|
+
export declare function takeDeepLink(href: string, param: string): {
|
|
182
|
+
text: string;
|
|
183
|
+
href: string;
|
|
184
|
+
} | null;
|
|
111
185
|
/**
|
|
112
186
|
* The whole chat app, the way the Claude, ChatGPT and Grok phone apps are
|
|
113
187
|
* laid out: the conversation edge to edge, a frosted header (Chats · title ·
|
|
@@ -119,5 +193,5 @@ export type ChatAppProps = {
|
|
|
119
193
|
* `store` that keeps chats (or nothing: the device then keeps them) — plus
|
|
120
194
|
* its words. Everything a person sees and does is the same in every product.
|
|
121
195
|
*/
|
|
122
|
-
export declare
|
|
196
|
+
export declare const ChatApp: import("react").ForwardRefExoticComponent<ChatAppProps & import("react").RefAttributes<ChatAppHandle>>;
|
|
123
197
|
//# sourceMappingURL=ChatApp.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ChatApp.d.ts","sourceRoot":"","sources":["../../src/react/ChatApp.tsx"],"names":[],"mappings":"AAEA,OAAO,
|
|
1
|
+
{"version":3,"file":"ChatApp.d.ts","sourceRoot":"","sources":["../../src/react/ChatApp.tsx"],"names":[],"mappings":"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"}
|
package/dist/react/ChatApp.js
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
-
import { useEffect, useMemo, useRef, useState } from "react";
|
|
3
|
+
import { forwardRef, useEffect, useImperativeHandle, useMemo, useRef, useState, } from "react";
|
|
4
4
|
import { localConversationStore } from "../stores.js";
|
|
5
5
|
import { ChatScreen } from "./Screen.js";
|
|
6
6
|
import { ChatStarters, ChatThread, } from "./Thread.js";
|
|
7
7
|
import { Composer } from "./Composer.js";
|
|
8
|
+
import { readAttachmentFile } from "./use-attachments.js";
|
|
8
9
|
import { ChatHistory } from "./ChatHistory.js";
|
|
9
10
|
import { useViewportHeight } from "./hooks.js";
|
|
10
11
|
import { IconGhost, IconMenu, IconMore, IconNewChat, IconX } from "./icons.js";
|
|
11
|
-
import { useChatApp } from "./use-chat-app.js";
|
|
12
|
+
import { useChatApp, } from "./use-chat-app.js";
|
|
12
13
|
export const DEFAULT_CHAT_APP_LABELS = {
|
|
13
14
|
chats: "Chats",
|
|
14
15
|
openChats: "Open your chats",
|
|
@@ -45,6 +46,23 @@ export const DEFAULT_CHAT_APP_LABELS = {
|
|
|
45
46
|
open: "That chat did not open. Try again.",
|
|
46
47
|
},
|
|
47
48
|
};
|
|
49
|
+
/** The words a `?q=`-style link carries, and the address without them. Pure. */
|
|
50
|
+
export function takeDeepLink(href, param) {
|
|
51
|
+
let url;
|
|
52
|
+
try {
|
|
53
|
+
url = new URL(href);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
const raw = url.searchParams.get(param);
|
|
59
|
+
if (raw === null)
|
|
60
|
+
return null;
|
|
61
|
+
url.searchParams.delete(param);
|
|
62
|
+
const text = raw.trim().slice(0, DEEP_LINK_MAX);
|
|
63
|
+
return { text, href: url.toString() };
|
|
64
|
+
}
|
|
65
|
+
const DEEP_LINK_MAX = 4000;
|
|
48
66
|
function useMediaQuery(query) {
|
|
49
67
|
const [match, setMatch] = useState(false);
|
|
50
68
|
useEffect(() => {
|
|
@@ -70,13 +88,23 @@ let fallbackStore = null;
|
|
|
70
88
|
* `store` that keeps chats (or nothing: the device then keeps them) — plus
|
|
71
89
|
* its words. Everything a person sees and does is the same in every product.
|
|
72
90
|
*/
|
|
73
|
-
export function ChatApp({ transport, store: givenStore, placeholder = "Ask anything", name = "", makeTitle, starters, startersTitle, empty, composer, thread, footnote, fill = "viewport", temporaryChats = true, onShare, conversationId, onConversationChange, rememberKey = "ck-chat:open", search, headerStart, headerEnd, locale, railQuery = "(min-width: 64rem)", labels: labelOverrides, className, }) {
|
|
91
|
+
export const ChatApp = forwardRef(function ChatApp({ transport, store: givenStore, placeholder = "Ask anything", name = "", makeTitle, starters, startersTitle, empty, composer, thread, footnote, fill = "viewport", temporaryChats = true, onShare, conversationId, onConversationChange, rememberKey = "ck-chat:open", search, headerStart, headerEnd, locale, railQuery = "(min-width: 64rem)", labels: labelOverrides, className, initialMessage, deepLinkParam, deepLinkAutoSend = false, stopKeepsPartial, failureMessage, onTemporaryEnd, chipsFor, historySlot, historyEmpty, }, ref) {
|
|
74
92
|
const labels = {
|
|
75
93
|
...DEFAULT_CHAT_APP_LABELS,
|
|
76
94
|
...labelOverrides,
|
|
77
95
|
problems: { ...DEFAULT_CHAT_APP_LABELS.problems, ...labelOverrides?.problems },
|
|
78
96
|
};
|
|
79
97
|
const store = useMemo(() => givenStore ?? (fallbackStore ??= localConversationStore()), [givenStore]);
|
|
98
|
+
// What the page arrived with, read once: a message handed over, or a
|
|
99
|
+
// `?q=` link. Either starts a new chat, so the reload's chat is not reopened.
|
|
100
|
+
const [arrival] = useState(() => {
|
|
101
|
+
if (initialMessage)
|
|
102
|
+
return initialMessage;
|
|
103
|
+
if (!deepLinkParam || typeof window === "undefined")
|
|
104
|
+
return null;
|
|
105
|
+
const link = takeDeepLink(window.location.href, deepLinkParam);
|
|
106
|
+
return link?.text ? { text: link.text, send: deepLinkAutoSend } : null;
|
|
107
|
+
});
|
|
80
108
|
const app = useChatApp({
|
|
81
109
|
store,
|
|
82
110
|
transport,
|
|
@@ -84,7 +112,67 @@ export function ChatApp({ transport, store: givenStore, placeholder = "Ask anyth
|
|
|
84
112
|
conversationId,
|
|
85
113
|
onConversationChange,
|
|
86
114
|
rememberKey,
|
|
115
|
+
stopKeepsPartial,
|
|
116
|
+
failureMessage,
|
|
117
|
+
onTemporaryEnd,
|
|
118
|
+
restore: !arrival,
|
|
87
119
|
});
|
|
120
|
+
const composerRef = useRef(null);
|
|
121
|
+
const attachLimits = typeof composer?.attach === "object" ? composer.attach : {};
|
|
122
|
+
const fillBox = (text, files) => {
|
|
123
|
+
const box = composerRef.current;
|
|
124
|
+
if (!box)
|
|
125
|
+
return;
|
|
126
|
+
box.setText(text);
|
|
127
|
+
if (files && files.length > 0)
|
|
128
|
+
box.addFiles([...files]);
|
|
129
|
+
box.focus();
|
|
130
|
+
};
|
|
131
|
+
const sendFor = async (text, given = [], options) => {
|
|
132
|
+
await app.whenReady();
|
|
133
|
+
const files = given.filter((a) => typeof File !== "undefined" && a instanceof File);
|
|
134
|
+
const wire = given.filter((a) => !(typeof File !== "undefined" && a instanceof File));
|
|
135
|
+
const read = await Promise.all(files.map((f) => readAttachmentFile(f, attachLimits, composer?.labels?.attachNotes)));
|
|
136
|
+
if (read.some((r) => r.note !== undefined)) {
|
|
137
|
+
// One file cannot go: send nothing behind the person's back. The box
|
|
138
|
+
// gets the words and the files, and says why (the composer's own note).
|
|
139
|
+
if (options?.newChat)
|
|
140
|
+
app.newChat();
|
|
141
|
+
fillBox(text, files);
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
const attachments = [...wire, ...read.map((r) => stripPreview(r.attachment))];
|
|
145
|
+
return app.send(text, attachments, options);
|
|
146
|
+
};
|
|
147
|
+
const handle = {
|
|
148
|
+
send: sendFor,
|
|
149
|
+
fill: fillBox,
|
|
150
|
+
focus: () => composerRef.current?.focus(),
|
|
151
|
+
newChat: app.newChat,
|
|
152
|
+
openConversation: app.open,
|
|
153
|
+
};
|
|
154
|
+
useImperativeHandle(ref, () => handle);
|
|
155
|
+
// The arrival, once — StrictMode's rehearsal mount included (`taken` is
|
|
156
|
+
// kept across it), and only after the list is in so nothing moves it.
|
|
157
|
+
const taken = useRef(false);
|
|
158
|
+
useEffect(() => {
|
|
159
|
+
if (!app.ready || !arrival || taken.current)
|
|
160
|
+
return;
|
|
161
|
+
taken.current = true;
|
|
162
|
+
if (deepLinkParam && !initialMessage && typeof window !== "undefined") {
|
|
163
|
+
const link = takeDeepLink(window.location.href, deepLinkParam);
|
|
164
|
+
if (link)
|
|
165
|
+
window.history.replaceState(window.history.state, "", link.href);
|
|
166
|
+
}
|
|
167
|
+
if (arrival.send === false) {
|
|
168
|
+
app.newChat();
|
|
169
|
+
fillBox(arrival.text, (arrival.attachments ?? []).filter((a) => typeof File !== "undefined" && a instanceof File));
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
void sendFor(arrival.text, arrival.attachments, { newChat: true, chips: arrival.chips });
|
|
173
|
+
}
|
|
174
|
+
// `arrival` is fixed at mount; the rest is read when it fires.
|
|
175
|
+
}, [app.ready]);
|
|
88
176
|
const rootRef = useRef(null);
|
|
89
177
|
useViewportHeight(rootRef, fill === "viewport");
|
|
90
178
|
const wide = useMediaQuery(railQuery);
|
|
@@ -122,7 +210,7 @@ export function ChatApp({ transport, store: givenStore, placeholder = "Ask anyth
|
|
|
122
210
|
}, onNew: () => {
|
|
123
211
|
setDrawerOpen(false);
|
|
124
212
|
app.newChat();
|
|
125
|
-
}, onRename: (id, t) => void app.rename(id, t), onRemove: (id) => void app.remove(id), onRetry: app.reloadList, onClose: wide ? undefined : () => setDrawerOpen(false), search: search, locale: locale, labels: labels }));
|
|
213
|
+
}, onRename: (id, t) => void app.rename(id, t), onRemove: (id) => void app.remove(id), onRetry: app.reloadList, onClose: wide ? undefined : () => setDrawerOpen(false), search: search, locale: locale, labels: labels, slot: historySlot, empty: historyEmpty }));
|
|
126
214
|
const [renaming, setRenaming] = useState(false);
|
|
127
215
|
const [deleting, setDeleting] = useState(false);
|
|
128
216
|
const menu = [];
|
|
@@ -155,8 +243,12 @@ export function ChatApp({ transport, store: givenStore, placeholder = "Ask anyth
|
|
|
155
243
|
setDeleting(false);
|
|
156
244
|
void app.remove(current.id);
|
|
157
245
|
}, children: labels.delete }), _jsx("button", { type: "button", onClick: () => setDeleting(false), children: labels.cancel })] })] })) : null;
|
|
158
|
-
const dock = (_jsxs(_Fragment, { children: [above, app.problem && (_jsxs("p", { className: "ck-problem", role: "status", children: [_jsx("span", { children: labels.problems[app.problem] }), _jsx("button", { type: "button", className: "ck-problem-x", onClick: app.clearProblem, "aria-label": labels.dismiss, children: _jsx(IconX, {}) })] })), app.queued.length > 0 && (_jsx("ul", { className: "ck-app-queue", "aria-label": labels.queued, children: app.queued.map((q) => (_jsxs("li", { children: [_jsxs("span", { className: "ck-app-queue-text", children: [_jsx("span", { className: "ck-truncate", children: q.text }), _jsx("small", { children: labels.queued })] }), _jsx("button", { type: "button", className: "ck-icon-btn", onClick: () => app.unqueue(q.id), "aria-label": labels.unqueue(q.text), title: labels.unqueue(q.text), children: _jsx(IconX, {}) })] }, q.id))) })), _jsx(Composer, { placeholder: placeholder, attach: true, stagedPosition: "above", ...composer,
|
|
246
|
+
const dock = (_jsxs(_Fragment, { children: [above, app.problem && (_jsxs("p", { className: "ck-problem", role: "status", children: [_jsx("span", { children: labels.problems[app.problem] }), _jsx("button", { type: "button", className: "ck-problem-x", onClick: app.clearProblem, "aria-label": labels.dismiss, children: _jsx(IconX, {}) })] })), app.queued.length > 0 && (_jsx("ul", { className: "ck-app-queue", "aria-label": labels.queued, children: app.queued.map((q) => (_jsxs("li", { children: [_jsxs("span", { className: "ck-app-queue-text", children: [_jsx("span", { className: "ck-truncate", children: q.text }), _jsx("small", { children: labels.queued })] }), _jsx("button", { type: "button", className: "ck-icon-btn", onClick: () => app.unqueue(q.id), "aria-label": labels.unqueue(q.text), title: labels.unqueue(q.text), children: _jsx(IconX, {}) })] }, q.id))) })), _jsx(Composer, { placeholder: placeholder, attach: true, stagedPosition: "above", ...composer, handleRef: composerRef, onSend: (text, attachments) => {
|
|
247
|
+
const chips = chipsFor?.({ text, attachments });
|
|
248
|
+
return app.send(text, attachments, chips ? { chips } : undefined);
|
|
249
|
+
}, sending: app.busy, onStop: app.stop, queue: true })] }));
|
|
159
250
|
const lastFailed = app.messages[app.messages.length - 1]?.failed;
|
|
251
|
+
const renderFooter = thread?.renderFooter;
|
|
160
252
|
const onFeedback = thread?.onFeedback;
|
|
161
253
|
const cls = ["ck-app"];
|
|
162
254
|
if (fill === "viewport")
|
|
@@ -168,8 +260,22 @@ export function ChatApp({ transport, store: givenStore, placeholder = "Ask anyth
|
|
|
168
260
|
return (_jsxs("div", { ref: rootRef, className: cls.join(" "), children: [wide ? (_jsx("aside", { className: "ck-app-rail", children: history })) : (_jsx("dialog", { ref: drawerRef, className: "ck-app-drawer", "aria-label": labels.chats, onClose: () => setDrawerOpen(false), onClick: (e) => {
|
|
169
261
|
if (e.target === e.currentTarget)
|
|
170
262
|
setDrawerOpen(false);
|
|
171
|
-
}, children: drawerOpen && _jsx("div", { className: "ck-app-drawer-panel", children: history }) })), _jsx(ChatScreen, { fill: "parent", className: "ck-app-screen", header: header, composer: dock, footnote: footnote, renderers: thread?.renderers, children: _jsx(ChatThread, { messages: app.messages, loading: app.loading, live: app.live, stopped: app.stopped, onStop: app.stop, onRetry: lastFailed || app.stopped ? app.retry : undefined, onReply: (text) => void app.send(text), renderLink: thread?.renderLink, renderFooter:
|
|
172
|
-
|
|
263
|
+
}, children: drawerOpen && _jsx("div", { className: "ck-app-drawer-panel", children: history }) })), _jsx(ChatScreen, { fill: "parent", className: "ck-app-screen", header: header, composer: dock, footnote: footnote, renderers: thread?.renderers, children: _jsx(ChatThread, { messages: app.messages, loading: app.loading, live: app.live, stopped: app.stopped, onStop: app.stop, onRetry: lastFailed || app.stopped ? app.retry : undefined, onReply: (text) => void app.send(text), renderLink: thread?.renderLink, renderFooter: renderFooter
|
|
264
|
+
? (m, info) => renderFooter(m, {
|
|
265
|
+
...info,
|
|
266
|
+
send: (text, options) => void app.send(text, [], options),
|
|
267
|
+
fill: (text) => fillBox(text),
|
|
268
|
+
})
|
|
269
|
+
: undefined, speaker: thread?.speaker, share: thread?.share, onFeedback: onFeedback ? (f) => onFeedback({ ...f, conversationId: app.stored }) : undefined, labels: thread?.labels, empty: empty ??
|
|
270
|
+
(starters && starters.length > 0 ? (_jsx(ChatStarters, { starters: starters, title: startersTitle, onPick: (text) => void app.send(text) })) : (_jsx("div", { className: "ck-starters", children: startersTitle && _jsx("div", { className: "ck-starters-title", children: startersTitle }) }))), children: thread?.after }, app.activeId ?? (app.temporary ? "temporary" : "new")) })] }));
|
|
271
|
+
});
|
|
272
|
+
/** The wire shape only: a preview URL never leaves the page. */
|
|
273
|
+
function stripPreview(a) {
|
|
274
|
+
if (!("previewUrl" in a))
|
|
275
|
+
return a;
|
|
276
|
+
const { previewUrl: _preview, ...wire } = a;
|
|
277
|
+
void _preview;
|
|
278
|
+
return wire;
|
|
173
279
|
}
|
|
174
280
|
/** The header's "…": a small menu under the button. Escape and a tap
|
|
175
281
|
* outside close it and give focus back to the button; arrows move. */
|