@bitbaum/chatkit 0.18.1 → 0.20.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 +200 -0
- package/bin/chatkit-copy-pdf-assets.mjs +38 -0
- package/dist/pdf.d.ts +96 -0
- package/dist/pdf.d.ts.map +1 -0
- package/dist/pdf.js +212 -0
- package/dist/pdf.js.map +1 -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 +13 -4
- package/styles.css +64 -1
|
@@ -39,72 +39,18 @@ export function useAttachments(limits = {}, notes = {}) {
|
|
|
39
39
|
return prev;
|
|
40
40
|
return [...prev, item];
|
|
41
41
|
}), [maxFiles]);
|
|
42
|
-
const
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
shrink(file, maxImageEdge, maxImageBytes)
|
|
53
|
-
.then((dataUrl) => {
|
|
54
|
-
if (!dataUrl) {
|
|
55
|
-
tooLarge();
|
|
56
|
-
return;
|
|
57
|
-
}
|
|
58
|
-
const previewUrl = URL.createObjectURL(file);
|
|
59
|
-
previews.current.add(previewUrl);
|
|
60
|
-
add({
|
|
61
|
-
kind: "image",
|
|
62
|
-
name: name.replace(/\.(png|gif|webp|jpe?g)$/i, "") + ".jpg",
|
|
63
|
-
mimeType: "image/jpeg",
|
|
64
|
-
dataBase64: stripDataUrlBase64(dataUrl),
|
|
65
|
-
previewUrl,
|
|
66
|
-
});
|
|
67
|
-
})
|
|
68
|
-
.catch(() => setNote(say.current.unreadable(name)));
|
|
69
|
-
return;
|
|
70
|
-
}
|
|
71
|
-
if (file.size > maxImageBytes) {
|
|
72
|
-
tooLarge();
|
|
73
|
-
return;
|
|
74
|
-
}
|
|
75
|
-
const reader = new FileReader();
|
|
76
|
-
reader.onload = () => {
|
|
77
|
-
const previewUrl = URL.createObjectURL(file);
|
|
78
|
-
previews.current.add(previewUrl);
|
|
79
|
-
add({
|
|
80
|
-
kind: "image",
|
|
81
|
-
name,
|
|
82
|
-
mimeType: file.type,
|
|
83
|
-
dataBase64: stripDataUrlBase64(String(reader.result ?? "")),
|
|
84
|
-
previewUrl,
|
|
85
|
-
});
|
|
86
|
-
};
|
|
87
|
-
reader.onerror = () => setNote(say.current.unreadable(name));
|
|
88
|
-
reader.readAsDataURL(file);
|
|
89
|
-
}, [add, maxImageBytes, maxImageEdge]);
|
|
90
|
-
const stageText = useCallback((file) => {
|
|
91
|
-
if (!isTextFile(file.name, file.type)) {
|
|
92
|
-
setNote(say.current.wrongType(file.name));
|
|
93
|
-
return;
|
|
94
|
-
}
|
|
95
|
-
if (file.size > maxTextChars) {
|
|
96
|
-
setNote(say.current.textTooLarge(file.name, Math.round(maxTextChars / 1000)));
|
|
97
|
-
return;
|
|
98
|
-
}
|
|
99
|
-
const reader = new FileReader();
|
|
100
|
-
reader.onload = () => add({
|
|
101
|
-
kind: "text",
|
|
102
|
-
name: file.name,
|
|
103
|
-
content: String(reader.result ?? "").slice(0, maxTextChars),
|
|
42
|
+
const stage = useCallback((file) => {
|
|
43
|
+
const limits = { maxFiles, maxImageBytes, maxImageEdge, maxTextChars };
|
|
44
|
+
void readAttachmentFile(file, limits, say.current, { preview: true }).then((read) => {
|
|
45
|
+
if (read.note !== undefined) {
|
|
46
|
+
setNote(read.note);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (read.attachment.previewUrl)
|
|
50
|
+
previews.current.add(read.attachment.previewUrl);
|
|
51
|
+
add(read.attachment);
|
|
104
52
|
});
|
|
105
|
-
|
|
106
|
-
reader.readAsText(file);
|
|
107
|
-
}, [add, maxTextChars]);
|
|
53
|
+
}, [add, maxFiles, maxImageBytes, maxImageEdge, maxTextChars]);
|
|
108
54
|
const addFiles = useCallback((files) => {
|
|
109
55
|
if (!files)
|
|
110
56
|
return;
|
|
@@ -115,12 +61,9 @@ export function useAttachments(limits = {}, notes = {}) {
|
|
|
115
61
|
return;
|
|
116
62
|
}
|
|
117
63
|
for (const file of Array.from(files).slice(0, room)) {
|
|
118
|
-
|
|
119
|
-
stageImage(file);
|
|
120
|
-
else
|
|
121
|
-
stageText(file);
|
|
64
|
+
stage(file);
|
|
122
65
|
}
|
|
123
|
-
}, [maxFiles,
|
|
66
|
+
}, [maxFiles, stage]);
|
|
124
67
|
const addFromPaste = useCallback((e) => {
|
|
125
68
|
const items = e.clipboardData?.items;
|
|
126
69
|
if (!items)
|
|
@@ -132,10 +75,10 @@ export function useAttachments(limits = {}, notes = {}) {
|
|
|
132
75
|
for (const item of images) {
|
|
133
76
|
const file = item.getAsFile();
|
|
134
77
|
if (file)
|
|
135
|
-
|
|
78
|
+
stage(file);
|
|
136
79
|
}
|
|
137
80
|
return true;
|
|
138
|
-
}, [
|
|
81
|
+
}, [stage]);
|
|
139
82
|
const revoke = (a) => {
|
|
140
83
|
if (!a.previewUrl)
|
|
141
84
|
return;
|
|
@@ -171,4 +114,78 @@ export function useAttachments(limits = {}, notes = {}) {
|
|
|
171
114
|
limits: lim,
|
|
172
115
|
};
|
|
173
116
|
}
|
|
117
|
+
/**
|
|
118
|
+
* One file read the way the composer stages it: an image shrunk to
|
|
119
|
+
* `maxImageEdge` and judged by its size after that, a text file read up to
|
|
120
|
+
* `maxTextChars`, anything else refused — with the reason as a sentence from
|
|
121
|
+
* `notes`, never a throw. `preview: true` adds an object URL the caller must
|
|
122
|
+
* revoke. `useAttachments` and `ChatApp`'s `send` both read files through it.
|
|
123
|
+
*/
|
|
124
|
+
export async function readAttachmentFile(file, limits = {}, notes = {}, { preview = false } = {}) {
|
|
125
|
+
const { maxImageBytes, maxTextChars, maxImageEdge } = { ...DEFAULT_ATTACHMENT_LIMITS, ...limits };
|
|
126
|
+
const say = { ...DEFAULT_ATTACHMENT_NOTES, ...notes };
|
|
127
|
+
const previewOf = () => (preview ? URL.createObjectURL(file) : undefined);
|
|
128
|
+
if (isImageMime(file.type)) {
|
|
129
|
+
const name = file.name || "screenshot.png";
|
|
130
|
+
const tooLarge = { note: say.imageTooLarge(name, Math.round(maxImageBytes / 1_000_000)) };
|
|
131
|
+
try {
|
|
132
|
+
if (maxImageEdge > 0) {
|
|
133
|
+
// Shrink first, then judge the size: a 4 MB screenshot is fine once it
|
|
134
|
+
// is the 300 KB it needed to be.
|
|
135
|
+
const dataUrl = await shrink(file, maxImageEdge, maxImageBytes);
|
|
136
|
+
if (!dataUrl)
|
|
137
|
+
return tooLarge;
|
|
138
|
+
return {
|
|
139
|
+
attachment: {
|
|
140
|
+
kind: "image",
|
|
141
|
+
name: name.replace(/\.(png|gif|webp|jpe?g)$/i, "") + ".jpg",
|
|
142
|
+
mimeType: "image/jpeg",
|
|
143
|
+
dataBase64: stripDataUrlBase64(dataUrl),
|
|
144
|
+
previewUrl: previewOf(),
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
if (file.size > maxImageBytes)
|
|
149
|
+
return tooLarge;
|
|
150
|
+
const dataUrl = await readAs(file, "dataUrl");
|
|
151
|
+
return {
|
|
152
|
+
attachment: {
|
|
153
|
+
kind: "image",
|
|
154
|
+
name,
|
|
155
|
+
mimeType: file.type,
|
|
156
|
+
dataBase64: stripDataUrlBase64(dataUrl),
|
|
157
|
+
previewUrl: previewOf(),
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
return { note: say.unreadable(name) };
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
if (!isTextFile(file.name, file.type))
|
|
166
|
+
return { note: say.wrongType(file.name) };
|
|
167
|
+
if (file.size > maxTextChars) {
|
|
168
|
+
return { note: say.textTooLarge(file.name, Math.round(maxTextChars / 1000)) };
|
|
169
|
+
}
|
|
170
|
+
try {
|
|
171
|
+
const content = await readAs(file, "text");
|
|
172
|
+
return {
|
|
173
|
+
attachment: { kind: "text", name: file.name, content: content.slice(0, maxTextChars) },
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
return { note: say.unreadable(file.name) };
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
function readAs(file, as) {
|
|
181
|
+
return new Promise((resolve, reject) => {
|
|
182
|
+
const reader = new FileReader();
|
|
183
|
+
reader.onload = () => resolve(String(reader.result ?? ""));
|
|
184
|
+
reader.onerror = () => reject(reader.error);
|
|
185
|
+
if (as === "text")
|
|
186
|
+
reader.readAsText(file);
|
|
187
|
+
else
|
|
188
|
+
reader.readAsDataURL(file);
|
|
189
|
+
});
|
|
190
|
+
}
|
|
174
191
|
//# sourceMappingURL=use-attachments.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-attachments.js","sourceRoot":"","sources":["../../src/react/use-attachments.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAuB,MAAM,OAAO,CAAC;AACtF,OAAO,EAAE,WAAW,IAAI,MAAM,EAAE,MAAM,aAAa,CAAC;AACpD,OAAO,EACL,yBAAyB,EACzB,wBAAwB,EACxB,aAAa,EACb,WAAW,EACX,UAAU,EACV,kBAAkB,EAClB,MAAM,GAKP,MAAM,mBAAmB,CAAC;AAmB3B;;;;;GAKG;AACH,MAAM,UAAU,cAAc,CAC5B,SAAoC,EAAE,EACtC,QAAkC,EAAE;IAEpC,MAAM,GAAG,GAAqB,EAAE,GAAG,yBAAyB,EAAE,GAAG,MAAM,EAAE,CAAC;IAC1E,MAAM,EAAE,QAAQ,EAAE,aAAa,EAAE,YAAY,EAAE,YAAY,EAAE,GAAG,GAAG,CAAC;IACpE,0EAA0E;IAC1E,wBAAwB;IACxB,MAAM,GAAG,GAAG,MAAM,CAAkB,wBAAwB,CAAC,CAAC;IAC9D,SAAS,CAAC,GAAG,EAAE;QACb,GAAG,CAAC,OAAO,GAAG,EAAE,GAAG,wBAAwB,EAAE,GAAG,KAAK,EAAE,CAAC;IAC1D,CAAC,CAAC,CAAC;IACH,MAAM,CAAC,WAAW,EAAE,cAAc,CAAC,GAAG,QAAQ,CAAqB,EAAE,CAAC,CAAC;IACvE,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,QAAQ,CAAgB,IAAI,CAAC,CAAC;IACtD,MAAM,QAAQ,GAAG,MAAM,CAAc,IAAI,GAAG,EAAE,CAAC,CAAC;IAChD,MAAM,KAAK,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;IAExB,SAAS,CAAC,GAAG,EAAE;QACb,KAAK,CAAC,OAAO,GAAG,WAAW,CAAC,MAAM,CAAC;IACrC,CAAC,EAAE,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC,CAAC;IAEzB,SAAS,CAAC,GAAG,EAAE;QACb,MAAM,IAAI,GAAG,QAAQ,CAAC,OAAO,CAAC;QAC9B,OAAO,GAAG,EAAE;YACV,KAAK,MAAM,GAAG,IAAI,IAAI;gBAAE,GAAG,CAAC,eAAe,CAAC,GAAG,CAAC,CAAC;YACjD,IAAI,CAAC,KAAK,EAAE,CAAC;QACf,CAAC,CAAC;IACJ,CAAC,EAAE,EAAE,CAAC,CAAC;IAEP,MAAM,GAAG,GAAG,WAAW,CACrB,CAAC,IAAsB,EAAE,EAAE,CACzB,cAAc,CAAC,CAAC,IAAI,EAAE,EAAE;QACtB,IAAI,IAAI,CAAC,MAAM,IAAI,QAAQ;YAAE,OAAO,IAAI,CAAC;QACzC,IAAI,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,aAAa,CAAC,CAAC,CAAC,KAAK,aAAa,CAAC,IAAI,CAAC,CAAC;YAAE,OAAO,IAAI,CAAC;QAC5E,OAAO,CAAC,GAAG,IAAI,EAAE,IAAI,CAAC,CAAC;IACzB,CAAC,CAAC,EACJ,CAAC,QAAQ,CAAC,CACX,CAAC;IAEF,MAAM,
|
|
1
|
+
{"version":3,"file":"use-attachments.js","sourceRoot":"","sources":["../../src/react/use-attachments.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import type { Attachment } from "../attachments.js";
|
|
2
2
|
import { type ConversationStore, type ConversationSummary } from "../conversations.js";
|
|
3
|
-
import type { ChatMessageData, LiveTurn } from "./Thread.js";
|
|
3
|
+
import type { ChatFailure, ChatMessageData, LiveTurn, MessageChip } from "./Thread.js";
|
|
4
4
|
/** One turn, as ChatApp hands it to the product. */
|
|
5
5
|
export type ChatTurn = {
|
|
6
6
|
/** The stored conversation, or null in a temporary chat (nothing is kept). */
|
|
7
7
|
conversationId: string | null;
|
|
8
|
+
/** This chat's id on the page: the stored id, or — in a temporary chat — a
|
|
9
|
+
* page-only id, the one `onTemporaryEnd` names when the chat ends. A
|
|
10
|
+
* product that keeps a hidden server row for a temporary chat keys it here. */
|
|
11
|
+
chatId: string;
|
|
8
12
|
temporary: boolean;
|
|
9
13
|
/** What the person wrote (or the composer's attachment-only words). */
|
|
10
14
|
text: string;
|
|
@@ -16,6 +20,38 @@ export type ChatTurn = {
|
|
|
16
20
|
/** What has arrived so far — the WHOLE text each time, a status line before
|
|
17
21
|
* the first word, the steps taken. Call it as often as you like. */
|
|
18
22
|
update: (live: LiveTurn) => void;
|
|
23
|
+
/** Whether Stop keeps what has arrived for THIS turn, overriding ChatApp's
|
|
24
|
+
* `stopKeepsPartial` — `false` while the text so far is unchecked (Heidi's
|
|
25
|
+
* variety gate), `true` once it is. Call it any time before the turn ends. */
|
|
26
|
+
stopKeepsPartial: (keep: boolean) => void;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Throw this from a transport to say why a turn failed in the reader's
|
|
30
|
+
* words: the failed bubble shows `message`, and `kind` (the product's own
|
|
31
|
+
* code, e.g. "quota" or "no-vision") travels with that message as
|
|
32
|
+
* `failure.kind` for a footer to link the fix. Any other error shows the
|
|
33
|
+
* thread's `failed` label — a raw `TypeError: Failed to fetch` is never
|
|
34
|
+
* shown to a reader — unless ChatApp's `failureMessage` maps it.
|
|
35
|
+
*/
|
|
36
|
+
export declare class ChatTurnError extends Error {
|
|
37
|
+
readonly kind?: string;
|
|
38
|
+
constructor(message: string, options?: {
|
|
39
|
+
kind?: string;
|
|
40
|
+
cause?: unknown;
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* What a failed turn keeps: a `ChatTurnError`'s words and kind, a plain
|
|
45
|
+
* `{ message, kind }` object as thrown, or what `map` makes of anything
|
|
46
|
+
* else. Never an ordinary Error's message — that is for the console.
|
|
47
|
+
*/
|
|
48
|
+
export declare function chatTurnFailure(error: unknown, map?: (error: unknown) => string | null | undefined): ChatFailure;
|
|
49
|
+
/** How a page sends for the person (`ChatAppHandle.send`). */
|
|
50
|
+
export type SendOptions = {
|
|
51
|
+
/** Start a new chat for it instead of adding to the open one. */
|
|
52
|
+
newChat?: boolean;
|
|
53
|
+
/** The product's own things shown on the person's message (see `MessageChip`). */
|
|
54
|
+
chips?: readonly MessageChip[];
|
|
19
55
|
};
|
|
20
56
|
/** The answer: its words, or the whole message (replies, citations, thinking…). */
|
|
21
57
|
export type ChatTurnResult = string | (Omit<ChatMessageData, "id" | "role"> & {
|
|
@@ -32,6 +68,7 @@ type Queued = {
|
|
|
32
68
|
conversation: string;
|
|
33
69
|
text: string;
|
|
34
70
|
attachments: Attachment[];
|
|
71
|
+
chips?: readonly MessageChip[];
|
|
35
72
|
};
|
|
36
73
|
export declare const isTemporaryId: (id: string | null | undefined) => boolean;
|
|
37
74
|
export type UseChatAppOptions = {
|
|
@@ -44,15 +81,28 @@ export type UseChatAppOptions = {
|
|
|
44
81
|
conversationId?: string | null;
|
|
45
82
|
onConversationChange?: (id: string | null) => void;
|
|
46
83
|
rememberKey: string | null;
|
|
84
|
+
/** Stop keeps the half answer that had arrived. Default true. */
|
|
85
|
+
stopKeepsPartial?: boolean;
|
|
86
|
+
/** The words for a failed turn whose error is not a `ChatTurnError`. */
|
|
87
|
+
failureMessage?: (error: unknown) => string | null | undefined;
|
|
88
|
+
/** A temporary chat ended: left for a new or another chat, the page hidden
|
|
89
|
+
* or ChatApp unmounted. Called once per chat, with `turn.chatId`. */
|
|
90
|
+
onTemporaryEnd?: (chatId: string) => void;
|
|
91
|
+
/** Reopen the chat from before the reload. False when the page arrives
|
|
92
|
+
* with a message to send (it starts a new chat). Default true. */
|
|
93
|
+
restore?: boolean;
|
|
47
94
|
};
|
|
48
95
|
/**
|
|
49
96
|
* Everything ChatApp does that is not drawing: the list, the open chat, new
|
|
50
97
|
* and temporary chats, turns (one per conversation, each running on when the
|
|
51
98
|
* person opens another chat), the queue, titles and the reopen-on-reload.
|
|
52
99
|
*/
|
|
53
|
-
export declare function useChatApp({ store, transport, makeTitle, conversationId, onConversationChange, rememberKey, }: UseChatAppOptions): {
|
|
100
|
+
export declare function useChatApp({ store, transport, makeTitle, conversationId, onConversationChange, rememberKey, stopKeepsPartial, failureMessage, onTemporaryEnd, restore, }: UseChatAppOptions): {
|
|
54
101
|
list: ConversationSummary[];
|
|
55
102
|
listState: "error" | "loading" | "ready";
|
|
103
|
+
/** The list has loaded (or failed) and the reopened chat is chosen. */
|
|
104
|
+
ready: boolean;
|
|
105
|
+
whenReady: () => Promise<void>;
|
|
56
106
|
reloadList: () => void;
|
|
57
107
|
activeId: string | null;
|
|
58
108
|
stored: string | null;
|
|
@@ -67,7 +117,7 @@ export declare function useChatApp({ store, transport, makeTitle, conversationId
|
|
|
67
117
|
unqueue: (qid: string) => void;
|
|
68
118
|
problem: ChatAppProblem | null;
|
|
69
119
|
clearProblem: () => void;
|
|
70
|
-
send: (text: string, attachments?: Attachment[]) => Promise<boolean>;
|
|
120
|
+
send: (text: string, attachments?: Attachment[], options?: SendOptions) => Promise<boolean>;
|
|
71
121
|
stop: () => void;
|
|
72
122
|
retry: () => void;
|
|
73
123
|
open: (id: string) => void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-chat-app.d.ts","sourceRoot":"","sources":["../../src/react/use-chat-app.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EAIL,KAAK,iBAAiB,EACtB,KAAK,mBAAmB,EAEzB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAAK,EAAE,eAAe,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"use-chat-app.d.ts","sourceRoot":"","sources":["../../src/react/use-chat-app.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,6 +1,45 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { useEffect, useRef, useState } from "react";
|
|
3
3
|
import { deriveTitle, newId, pickRestored, } from "../conversations.js";
|
|
4
|
+
/**
|
|
5
|
+
* Throw this from a transport to say why a turn failed in the reader's
|
|
6
|
+
* words: the failed bubble shows `message`, and `kind` (the product's own
|
|
7
|
+
* code, e.g. "quota" or "no-vision") travels with that message as
|
|
8
|
+
* `failure.kind` for a footer to link the fix. Any other error shows the
|
|
9
|
+
* thread's `failed` label — a raw `TypeError: Failed to fetch` is never
|
|
10
|
+
* shown to a reader — unless ChatApp's `failureMessage` maps it.
|
|
11
|
+
*/
|
|
12
|
+
export class ChatTurnError extends Error {
|
|
13
|
+
kind;
|
|
14
|
+
constructor(message, options = {}) {
|
|
15
|
+
super(message, options.cause === undefined ? undefined : { cause: options.cause });
|
|
16
|
+
this.name = "ChatTurnError";
|
|
17
|
+
this.kind = options.kind;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* What a failed turn keeps: a `ChatTurnError`'s words and kind, a plain
|
|
22
|
+
* `{ message, kind }` object as thrown, or what `map` makes of anything
|
|
23
|
+
* else. Never an ordinary Error's message — that is for the console.
|
|
24
|
+
*/
|
|
25
|
+
export function chatTurnFailure(error, map) {
|
|
26
|
+
if (error instanceof ChatTurnError) {
|
|
27
|
+
return { message: error.message, ...(error.kind ? { kind: error.kind } : {}) };
|
|
28
|
+
}
|
|
29
|
+
const mapped = map?.(error);
|
|
30
|
+
if (mapped)
|
|
31
|
+
return { message: mapped };
|
|
32
|
+
if (error && typeof error === "object" && !(error instanceof Error)) {
|
|
33
|
+
const { message, kind } = error;
|
|
34
|
+
const out = {};
|
|
35
|
+
if (typeof message === "string" && message.trim())
|
|
36
|
+
out.message = message;
|
|
37
|
+
if (typeof kind === "string" && kind)
|
|
38
|
+
out.kind = kind;
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
return {};
|
|
42
|
+
}
|
|
4
43
|
const TEMP = "tmp";
|
|
5
44
|
export const isTemporaryId = (id) => !!id && id.startsWith(`${TEMP}-`);
|
|
6
45
|
function messageAttachments(list) {
|
|
@@ -38,7 +77,7 @@ function writeRemembered(key, id) {
|
|
|
38
77
|
* and temporary chats, turns (one per conversation, each running on when the
|
|
39
78
|
* person opens another chat), the queue, titles and the reopen-on-reload.
|
|
40
79
|
*/
|
|
41
|
-
export function useChatApp({ store, transport, makeTitle, conversationId, onConversationChange, rememberKey, }) {
|
|
80
|
+
export function useChatApp({ store, transport, makeTitle, conversationId, onConversationChange, rememberKey, stopKeepsPartial = true, failureMessage, onTemporaryEnd, restore = true, }) {
|
|
42
81
|
const [list, setList] = useState([]);
|
|
43
82
|
const [listState, setListState] = useState("loading");
|
|
44
83
|
const [activeId, setActiveId] = useState(null);
|
|
@@ -50,6 +89,7 @@ export function useChatApp({ store, transport, makeTitle, conversationId, onConv
|
|
|
50
89
|
const [queue, setQueue] = useState([]);
|
|
51
90
|
const [problem, setProblem] = useState(null);
|
|
52
91
|
const [reload, setReload] = useState(0);
|
|
92
|
+
const [ready, setReady] = useState(false);
|
|
53
93
|
// Async work reads the CURRENT state through these, never a stale closure.
|
|
54
94
|
const threadsRef = useRef(threads);
|
|
55
95
|
const queueRef = useRef(queue);
|
|
@@ -59,10 +99,32 @@ export function useChatApp({ store, transport, makeTitle, conversationId, onConv
|
|
|
59
99
|
const restored = useRef(false);
|
|
60
100
|
const transportRef = useRef(transport);
|
|
61
101
|
const titleRef = useRef(makeTitle);
|
|
102
|
+
const optionsRef = useRef({ stopKeepsPartial, failureMessage, onTemporaryEnd });
|
|
62
103
|
useEffect(() => {
|
|
63
104
|
transportRef.current = transport;
|
|
64
105
|
titleRef.current = makeTitle;
|
|
106
|
+
optionsRef.current = { stopKeepsPartial, failureMessage, onTemporaryEnd };
|
|
65
107
|
});
|
|
108
|
+
// The files of each person's message, by its id, kept until its answer
|
|
109
|
+
// lands: Retry after a failure sends the pictures again, not only the words.
|
|
110
|
+
const turnFiles = useRef(new Map());
|
|
111
|
+
const ended = useRef(new Set());
|
|
112
|
+
const readyWaiters = useRef([]);
|
|
113
|
+
const markReady = () => {
|
|
114
|
+
restored.current = true;
|
|
115
|
+
setReady(true);
|
|
116
|
+
for (const go of readyWaiters.current.splice(0))
|
|
117
|
+
go();
|
|
118
|
+
};
|
|
119
|
+
/** Resolves once the list has loaded and the reopened chat is chosen, so a
|
|
120
|
+
* message sent from outside never lands before the restore moves away. */
|
|
121
|
+
const whenReady = () => restored.current ? Promise.resolve() : new Promise((go) => readyWaiters.current.push(go));
|
|
122
|
+
const endTemporary = (id) => {
|
|
123
|
+
if (!isTemporaryId(id) || ended.current.has(id))
|
|
124
|
+
return;
|
|
125
|
+
ended.current.add(id);
|
|
126
|
+
optionsRef.current.onTemporaryEnd?.(id);
|
|
127
|
+
};
|
|
66
128
|
const setThreadsNow = (fn) => {
|
|
67
129
|
threadsRef.current = fn(threadsRef.current);
|
|
68
130
|
setThreads(threadsRef.current);
|
|
@@ -83,13 +145,28 @@ export function useChatApp({ store, transport, makeTitle, conversationId, onConv
|
|
|
83
145
|
if (!isTemporaryId(id))
|
|
84
146
|
return;
|
|
85
147
|
controllers.current.get(id)?.abort();
|
|
148
|
+
for (const m of threadsRef.current[id] ?? [])
|
|
149
|
+
turnFiles.current.delete(m.id);
|
|
86
150
|
setThreadsNow((t) => {
|
|
87
151
|
const next = { ...t };
|
|
88
152
|
delete next[id];
|
|
89
153
|
return next;
|
|
90
154
|
});
|
|
91
155
|
setQueueNow((q) => q.filter((item) => item.conversation !== id));
|
|
156
|
+
endTemporary(id);
|
|
92
157
|
};
|
|
158
|
+
// A temporary chat also ends when the page goes away or ChatApp unmounts.
|
|
159
|
+
// (StrictMode's rehearsal unmount finds no temporary chat: one only exists
|
|
160
|
+
// after a first message.)
|
|
161
|
+
useEffect(() => {
|
|
162
|
+
const hide = () => endTemporary(activeRef.current);
|
|
163
|
+
globalThis.addEventListener?.("pagehide", hide);
|
|
164
|
+
return () => {
|
|
165
|
+
globalThis.removeEventListener?.("pagehide", hide);
|
|
166
|
+
hide();
|
|
167
|
+
};
|
|
168
|
+
// Reads everything through refs.
|
|
169
|
+
}, []);
|
|
93
170
|
const open = (id) => {
|
|
94
171
|
if (id === activeRef.current)
|
|
95
172
|
return;
|
|
@@ -126,17 +203,21 @@ export function useChatApp({ store, transport, makeTitle, conversationId, onConv
|
|
|
126
203
|
setList(rows);
|
|
127
204
|
setListState("ready");
|
|
128
205
|
if (!restored.current) {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
206
|
+
const id = restore
|
|
207
|
+
? pickRestored(rows, conversationId, readRemembered(rememberKey))
|
|
208
|
+
: null;
|
|
209
|
+
// An outside send may already have opened a chat: never move off it.
|
|
210
|
+
if (id && activeRef.current === null)
|
|
132
211
|
open(id);
|
|
212
|
+
markReady();
|
|
133
213
|
}
|
|
134
214
|
})
|
|
135
215
|
.catch(() => {
|
|
136
216
|
if (!alive)
|
|
137
217
|
return;
|
|
138
|
-
restored.current = true;
|
|
139
218
|
setListState("error");
|
|
219
|
+
if (!restored.current)
|
|
220
|
+
markReady();
|
|
140
221
|
});
|
|
141
222
|
return () => {
|
|
142
223
|
alive = false;
|
|
@@ -181,9 +262,12 @@ export function useChatApp({ store, transport, makeTitle, conversationId, onConv
|
|
|
181
262
|
setLive((l) => ({ ...l, [id]: {} }));
|
|
182
263
|
let soFar = {};
|
|
183
264
|
let ok = false;
|
|
265
|
+
let keepPartial = null;
|
|
266
|
+
const asked = [...(threadsRef.current[id] ?? [])].reverse().find((m) => m.role === "user");
|
|
184
267
|
try {
|
|
185
268
|
const result = await transportRef.current({
|
|
186
269
|
conversationId: temp ? null : id,
|
|
270
|
+
chatId: id,
|
|
187
271
|
temporary: temp,
|
|
188
272
|
text,
|
|
189
273
|
attachments,
|
|
@@ -194,6 +278,9 @@ export function useChatApp({ store, transport, makeTitle, conversationId, onConv
|
|
|
194
278
|
if (!controller.signal.aborted)
|
|
195
279
|
setLive((l) => (id in l ? { ...l, [id]: soFar } : l));
|
|
196
280
|
},
|
|
281
|
+
stopKeepsPartial: (keep) => {
|
|
282
|
+
keepPartial = keep;
|
|
283
|
+
},
|
|
197
284
|
});
|
|
198
285
|
if (controller.signal.aborted)
|
|
199
286
|
throw new DOMException("stopped", "AbortError");
|
|
@@ -202,6 +289,8 @@ export function useChatApp({ store, transport, makeTitle, conversationId, onConv
|
|
|
202
289
|
push(id, message);
|
|
203
290
|
save(id, message);
|
|
204
291
|
ok = true;
|
|
292
|
+
if (asked)
|
|
293
|
+
turnFiles.current.delete(asked.id);
|
|
205
294
|
if (!temp && history.length === 0 && titleRef.current) {
|
|
206
295
|
void Promise.resolve(titleRef.current({ text, answer: answer.content }))
|
|
207
296
|
.then((title) => (title?.trim() ? rename(id, title.trim()) : undefined))
|
|
@@ -210,10 +299,14 @@ export function useChatApp({ store, transport, makeTitle, conversationId, onConv
|
|
|
210
299
|
});
|
|
211
300
|
}
|
|
212
301
|
}
|
|
213
|
-
catch {
|
|
302
|
+
catch (error) {
|
|
214
303
|
if (controller.signal.aborted) {
|
|
215
|
-
// Stopped: what had arrived is kept, as the apps keep a half answer
|
|
216
|
-
|
|
304
|
+
// Stopped: what had arrived is kept, as the apps keep a half answer —
|
|
305
|
+
// unless the product says this turn's text is not fit to keep.
|
|
306
|
+
const keep = keepPartial ?? optionsRef.current.stopKeepsPartial;
|
|
307
|
+
if (keep && soFar.text?.trim()) {
|
|
308
|
+
if (asked)
|
|
309
|
+
turnFiles.current.delete(asked.id);
|
|
217
310
|
const message = {
|
|
218
311
|
id: newId("m"),
|
|
219
312
|
role: "assistant",
|
|
@@ -226,7 +319,14 @@ export function useChatApp({ store, transport, makeTitle, conversationId, onConv
|
|
|
226
319
|
setStopped((s) => ({ ...s, [id]: true }));
|
|
227
320
|
}
|
|
228
321
|
else {
|
|
229
|
-
|
|
322
|
+
const failure = chatTurnFailure(error, optionsRef.current.failureMessage);
|
|
323
|
+
push(id, {
|
|
324
|
+
id: newId("m"),
|
|
325
|
+
role: "assistant",
|
|
326
|
+
content: failure.message ?? "",
|
|
327
|
+
failed: true,
|
|
328
|
+
...(failure.message || failure.kind ? { failure } : {}),
|
|
329
|
+
});
|
|
230
330
|
}
|
|
231
331
|
}
|
|
232
332
|
finally {
|
|
@@ -243,27 +343,33 @@ export function useChatApp({ store, transport, makeTitle, conversationId, onConv
|
|
|
243
343
|
const next = queueRef.current.find((q) => q.conversation === id);
|
|
244
344
|
if (next) {
|
|
245
345
|
setQueueNow((q) => q.filter((item) => item.id !== next.id));
|
|
246
|
-
void sendIn(id, next.text, next.attachments);
|
|
346
|
+
void sendIn(id, next.text, next.attachments, next.chips);
|
|
247
347
|
}
|
|
248
348
|
}
|
|
249
349
|
};
|
|
250
|
-
const sendIn = async (id, text, attachments) => {
|
|
350
|
+
const sendIn = async (id, text, attachments, chips) => {
|
|
251
351
|
const history = (threadsRef.current[id] ?? []).filter((m) => !m.failed);
|
|
252
352
|
const message = {
|
|
253
353
|
id: newId("m"),
|
|
254
354
|
role: "user",
|
|
255
355
|
content: text,
|
|
256
356
|
...(attachments.length ? { attachments: messageAttachments(attachments) } : {}),
|
|
357
|
+
...(chips && chips.length ? { chips } : {}),
|
|
257
358
|
};
|
|
359
|
+
if (attachments.length)
|
|
360
|
+
turnFiles.current.set(message.id, attachments);
|
|
258
361
|
push(id, message);
|
|
259
362
|
save(id, message);
|
|
260
363
|
await runTurn(id, text, attachments, history);
|
|
261
364
|
};
|
|
262
365
|
/** Composer's onSend. Resolves false to keep the draft (nothing was sent). */
|
|
263
|
-
const send = async (text, attachments = []) => {
|
|
366
|
+
const send = async (text, attachments = [], options = {}) => {
|
|
367
|
+
const { chips } = options;
|
|
368
|
+
if (options.newChat)
|
|
369
|
+
newChat();
|
|
264
370
|
let id = activeRef.current;
|
|
265
371
|
if (id && controllers.current.has(id)) {
|
|
266
|
-
setQueueNow((q) => [...q, { id: newId("q"), conversation: id, text, attachments }]);
|
|
372
|
+
setQueueNow((q) => [...q, { id: newId("q"), conversation: id, text, attachments, chips }]);
|
|
267
373
|
return true;
|
|
268
374
|
}
|
|
269
375
|
if (!id) {
|
|
@@ -284,7 +390,7 @@ export function useChatApp({ store, transport, makeTitle, conversationId, onConv
|
|
|
284
390
|
setThreadsNow((t) => ({ ...t, [id]: [] }));
|
|
285
391
|
select(id, temporaryRef.current);
|
|
286
392
|
}
|
|
287
|
-
void sendIn(id, text, attachments);
|
|
393
|
+
void sendIn(id, text, attachments, chips);
|
|
288
394
|
return true;
|
|
289
395
|
};
|
|
290
396
|
const stop = () => {
|
|
@@ -304,7 +410,7 @@ export function useChatApp({ store, transport, makeTitle, conversationId, onConv
|
|
|
304
410
|
return;
|
|
305
411
|
setThreadsNow((t) => ({ ...t, [id]: kept }));
|
|
306
412
|
const history = kept.slice(0, kept.indexOf(lastUser));
|
|
307
|
-
void runTurn(id, lastUser.content, [], history);
|
|
413
|
+
void runTurn(id, lastUser.content, turnFiles.current.get(lastUser.id) ?? [], history);
|
|
308
414
|
};
|
|
309
415
|
const newChat = () => {
|
|
310
416
|
leaveTemporary();
|
|
@@ -332,6 +438,8 @@ export function useChatApp({ store, transport, makeTitle, conversationId, onConv
|
|
|
332
438
|
};
|
|
333
439
|
const remove = async (id) => {
|
|
334
440
|
controllers.current.get(id)?.abort();
|
|
441
|
+
for (const m of threadsRef.current[id] ?? [])
|
|
442
|
+
turnFiles.current.delete(m.id);
|
|
335
443
|
setList((l) => l.filter((c) => c.id !== id));
|
|
336
444
|
setThreadsNow((t) => {
|
|
337
445
|
const next = { ...t };
|
|
@@ -353,6 +461,9 @@ export function useChatApp({ store, transport, makeTitle, conversationId, onConv
|
|
|
353
461
|
return {
|
|
354
462
|
list,
|
|
355
463
|
listState,
|
|
464
|
+
/** The list has loaded (or failed) and the reopened chat is chosen. */
|
|
465
|
+
ready,
|
|
466
|
+
whenReady,
|
|
356
467
|
reloadList: () => setReload((n) => n + 1),
|
|
357
468
|
activeId,
|
|
358
469
|
stored,
|