@x12i/ai 1.0.0 → 1.1.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 +515 -24
- package/dist/chunk-RP5SPJKM.js +1179 -0
- package/dist/chunk-RP5SPJKM.js.map +1 -0
- package/dist/index.d.ts +273 -72
- package/dist/index.js +510 -9
- package/dist/index.js.map +1 -1
- package/dist/react.d.ts +906 -0
- package/dist/react.js +3067 -0
- package/dist/react.js.map +1 -0
- package/dist/stream-C2tzoSII.d.ts +482 -0
- package/dist/styles.css +1225 -0
- package/package.json +48 -4
package/dist/react.js
ADDED
|
@@ -0,0 +1,3067 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import {
|
|
3
|
+
applyStreamEvent,
|
|
4
|
+
createAbortableStream,
|
|
5
|
+
createThreadState,
|
|
6
|
+
findToolCall,
|
|
7
|
+
formatBytes,
|
|
8
|
+
formatDuration,
|
|
9
|
+
formatTokenCount,
|
|
10
|
+
getBranchInfoMap,
|
|
11
|
+
getUrlHostname,
|
|
12
|
+
isBusy,
|
|
13
|
+
isExternalUrl,
|
|
14
|
+
isTerminalStreamEvent,
|
|
15
|
+
lastMessageWithRole,
|
|
16
|
+
lastUserMessage,
|
|
17
|
+
markdownToPlainText,
|
|
18
|
+
messageText,
|
|
19
|
+
parseCitations,
|
|
20
|
+
sanitizeImageUrl,
|
|
21
|
+
sanitizeUrl,
|
|
22
|
+
splitCompletedSentences,
|
|
23
|
+
splitMarkdownBlocks,
|
|
24
|
+
threadPendingApprovals,
|
|
25
|
+
threadReducer,
|
|
26
|
+
toThreadError,
|
|
27
|
+
totalTokens
|
|
28
|
+
} from "./chunk-RP5SPJKM.js";
|
|
29
|
+
|
|
30
|
+
// src/react/chat-thread.tsx
|
|
31
|
+
import { VisuallyHidden as VisuallyHidden10 } from "@x12i/ui";
|
|
32
|
+
import {
|
|
33
|
+
memo as memo2,
|
|
34
|
+
useEffect as useEffect6,
|
|
35
|
+
useMemo as useMemo5,
|
|
36
|
+
useRef as useRef11,
|
|
37
|
+
useState as useState7
|
|
38
|
+
} from "react";
|
|
39
|
+
|
|
40
|
+
// src/react/conversation.tsx
|
|
41
|
+
import { Button } from "@x12i/ui";
|
|
42
|
+
import { useImperativeHandle } from "react";
|
|
43
|
+
|
|
44
|
+
// src/react/internal/cx.ts
|
|
45
|
+
function cx(...values) {
|
|
46
|
+
const joined = values.filter(Boolean).join(" ");
|
|
47
|
+
return joined === "" ? void 0 : joined;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// src/react/internal/glyphs.tsx
|
|
51
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
52
|
+
function Glyph({ size = "sm", children, ...props }) {
|
|
53
|
+
return /* @__PURE__ */ jsx(
|
|
54
|
+
"svg",
|
|
55
|
+
{
|
|
56
|
+
"data-kit": "ai-glyph",
|
|
57
|
+
"data-size": size,
|
|
58
|
+
viewBox: "0 0 24 24",
|
|
59
|
+
fill: "none",
|
|
60
|
+
stroke: "currentColor",
|
|
61
|
+
strokeLinecap: "round",
|
|
62
|
+
strokeLinejoin: "round",
|
|
63
|
+
"aria-hidden": "true",
|
|
64
|
+
focusable: "false",
|
|
65
|
+
...props,
|
|
66
|
+
children
|
|
67
|
+
}
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
function SendGlyph(props) {
|
|
71
|
+
return /* @__PURE__ */ jsxs(Glyph, { ...props, children: [
|
|
72
|
+
/* @__PURE__ */ jsx("path", { d: "M12 19V5" }),
|
|
73
|
+
/* @__PURE__ */ jsx("path", { d: "m5 12 7-7 7 7" })
|
|
74
|
+
] });
|
|
75
|
+
}
|
|
76
|
+
function ArrowDownGlyph(props) {
|
|
77
|
+
return /* @__PURE__ */ jsxs(Glyph, { ...props, children: [
|
|
78
|
+
/* @__PURE__ */ jsx("path", { d: "M12 5v14" }),
|
|
79
|
+
/* @__PURE__ */ jsx("path", { d: "m19 12-7 7-7-7" })
|
|
80
|
+
] });
|
|
81
|
+
}
|
|
82
|
+
function StopGlyph(props) {
|
|
83
|
+
return /* @__PURE__ */ jsx(Glyph, { ...props, children: /* @__PURE__ */ jsx("rect", { x: "6", y: "6", width: "12", height: "12", rx: "2", fill: "currentColor", stroke: "none" }) });
|
|
84
|
+
}
|
|
85
|
+
function PaperclipGlyph(props) {
|
|
86
|
+
return /* @__PURE__ */ jsx(Glyph, { ...props, children: /* @__PURE__ */ jsx("path", { d: "m21.44 11.05-9.19 9.19a6 6 0 0 1-8.49-8.49l8.57-8.57A4 4 0 1 1 18 8.84l-8.59 8.57a2 2 0 0 1-2.83-2.83l8.49-8.48" }) });
|
|
87
|
+
}
|
|
88
|
+
function ThumbsUpGlyph({ filled, ...props }) {
|
|
89
|
+
return /* @__PURE__ */ jsxs(Glyph, { ...props, fill: filled ? "currentColor" : "none", children: [
|
|
90
|
+
/* @__PURE__ */ jsx("path", { d: "M7 10v12" }),
|
|
91
|
+
/* @__PURE__ */ jsx("path", { d: "M15 5.88 14 10h5.83a2 2 0 0 1 1.92 2.56l-2.33 8A2 2 0 0 1 17.5 22H4a2 2 0 0 1-2-2v-8a2 2 0 0 1 2-2h2.76a2 2 0 0 0 1.79-1.11L12 2a3.13 3.13 0 0 1 3 3.88Z" })
|
|
92
|
+
] });
|
|
93
|
+
}
|
|
94
|
+
function ThumbsDownGlyph({ filled, ...props }) {
|
|
95
|
+
return /* @__PURE__ */ jsxs(Glyph, { ...props, fill: filled ? "currentColor" : "none", children: [
|
|
96
|
+
/* @__PURE__ */ jsx("path", { d: "M17 14V2" }),
|
|
97
|
+
/* @__PURE__ */ jsx("path", { d: "M9 18.12 10 14H4.17a2 2 0 0 1-1.92-2.56l2.33-8A2 2 0 0 1 6.5 2H20a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2h-2.76a2 2 0 0 0-1.79 1.11L12 22a3.13 3.13 0 0 1-3-3.88Z" })
|
|
98
|
+
] });
|
|
99
|
+
}
|
|
100
|
+
function RefreshGlyph(props) {
|
|
101
|
+
return /* @__PURE__ */ jsxs(Glyph, { ...props, children: [
|
|
102
|
+
/* @__PURE__ */ jsx("path", { d: "M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" }),
|
|
103
|
+
/* @__PURE__ */ jsx("path", { d: "M3 3v5h5" })
|
|
104
|
+
] });
|
|
105
|
+
}
|
|
106
|
+
function SparklesGlyph(props) {
|
|
107
|
+
return /* @__PURE__ */ jsxs(Glyph, { ...props, children: [
|
|
108
|
+
/* @__PURE__ */ jsx("path", { d: "M12 3l1.9 5.1L19 10l-5.1 1.9L12 17l-1.9-5.1L5 10l5.1-1.9L12 3z" }),
|
|
109
|
+
/* @__PURE__ */ jsx("path", { d: "M19 15l.8 2.2L22 18l-2.2.8L19 21l-.8-2.2L16 18l2.2-.8L19 15z" })
|
|
110
|
+
] });
|
|
111
|
+
}
|
|
112
|
+
function WrenchGlyph(props) {
|
|
113
|
+
return /* @__PURE__ */ jsx(Glyph, { ...props, children: /* @__PURE__ */ jsx("path", { d: "M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z" }) });
|
|
114
|
+
}
|
|
115
|
+
function FileGlyph(props) {
|
|
116
|
+
return /* @__PURE__ */ jsxs(Glyph, { ...props, children: [
|
|
117
|
+
/* @__PURE__ */ jsx("path", { d: "M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z" }),
|
|
118
|
+
/* @__PURE__ */ jsx("path", { d: "M14 2v6h6" })
|
|
119
|
+
] });
|
|
120
|
+
}
|
|
121
|
+
function BranchPrevGlyph(props) {
|
|
122
|
+
return /* @__PURE__ */ jsx(Glyph, { ...props, children: /* @__PURE__ */ jsx("path", { d: "m15 18-6-6 6-6" }) });
|
|
123
|
+
}
|
|
124
|
+
function BranchNextGlyph(props) {
|
|
125
|
+
return /* @__PURE__ */ jsx(Glyph, { ...props, children: /* @__PURE__ */ jsx("path", { d: "m9 18 6-6-6-6" }) });
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// src/react/messages-context.tsx
|
|
129
|
+
import { createContext, useContext, useMemo } from "react";
|
|
130
|
+
|
|
131
|
+
// src/react/messages.ts
|
|
132
|
+
var defaultMessages = {
|
|
133
|
+
chat: "Chat",
|
|
134
|
+
conversation: "Conversation",
|
|
135
|
+
jumpToLatest: "Jump to latest",
|
|
136
|
+
emptyTitle: "How can I help?",
|
|
137
|
+
emptyDescription: "Ask a question or pick a suggestion to start.",
|
|
138
|
+
disclaimer: "AI-generated content can be wrong. Check important information.",
|
|
139
|
+
suggestions: "Suggested follow-ups",
|
|
140
|
+
roles: { user: "You", assistant: "Assistant", system: "System", tool: "Tool" },
|
|
141
|
+
aiGenerated: "AI",
|
|
142
|
+
aiGeneratedLabel: "AI-generated",
|
|
143
|
+
edited: "Edited",
|
|
144
|
+
stopped: "Response stopped",
|
|
145
|
+
failed: "Response failed",
|
|
146
|
+
responding: "Responding",
|
|
147
|
+
tokens: (count) => `${count} tokens`,
|
|
148
|
+
messageFrom: (role) => `Message from ${role}`,
|
|
149
|
+
promptLabel: "Message",
|
|
150
|
+
promptPlaceholder: "Type a message",
|
|
151
|
+
send: "Send message",
|
|
152
|
+
stop: "Stop generating",
|
|
153
|
+
attach: "Attach files",
|
|
154
|
+
attachments: "Attachments",
|
|
155
|
+
removeAttachment: (name) => `Remove ${name}`,
|
|
156
|
+
attachmentUploading: "Uploading",
|
|
157
|
+
attachmentFailed: "Upload failed",
|
|
158
|
+
dropFiles: "Drop files to attach",
|
|
159
|
+
characterCount: (count, max) => `${count} / ${max}`,
|
|
160
|
+
characterLimitReached: "Character limit reached",
|
|
161
|
+
commands: "Commands",
|
|
162
|
+
commandsAvailable: (count) => `${count} commands available`,
|
|
163
|
+
reasoning: "Reasoning",
|
|
164
|
+
thinking: "Thinking\u2026",
|
|
165
|
+
thought: "Thought",
|
|
166
|
+
thoughtFor: (duration) => `Thought for ${duration}`,
|
|
167
|
+
toolStates: {
|
|
168
|
+
queued: "Queued",
|
|
169
|
+
running: "Running",
|
|
170
|
+
"needs-approval": "Needs approval",
|
|
171
|
+
approved: "Approved",
|
|
172
|
+
denied: "Denied",
|
|
173
|
+
succeeded: "Done",
|
|
174
|
+
failed: "Failed"
|
|
175
|
+
},
|
|
176
|
+
toolCall: (name) => `Tool call: ${name}`,
|
|
177
|
+
toolInput: "Input",
|
|
178
|
+
toolOutput: "Output",
|
|
179
|
+
toolError: "Error",
|
|
180
|
+
toolAborted: "Stopped before it finished",
|
|
181
|
+
noInput: "No input",
|
|
182
|
+
noOutput: "No output",
|
|
183
|
+
approvalTitle: (toolName) => `Approve \u201C${toolName}\u201D?`,
|
|
184
|
+
approvalDescription: "The assistant wants to run this action. Review it, then approve or deny.",
|
|
185
|
+
approvalAction: "Action",
|
|
186
|
+
approve: "Approve",
|
|
187
|
+
deny: "Deny",
|
|
188
|
+
approveLabel: (toolName) => `Approve ${toolName}`,
|
|
189
|
+
denyLabel: (toolName) => `Deny ${toolName}`,
|
|
190
|
+
reasonLabel: "Reason (optional)",
|
|
191
|
+
reasonPlaceholder: "Why?",
|
|
192
|
+
sources: "Sources",
|
|
193
|
+
sourcesCount: (count) => count === 1 ? "1 source" : `${count} sources`,
|
|
194
|
+
citationLabel: (number, title) => `Source ${number}: ${title}`,
|
|
195
|
+
opensInNewTab: "opens in a new tab",
|
|
196
|
+
noLink: "No link available",
|
|
197
|
+
actions: "Message actions",
|
|
198
|
+
copy: "Copy",
|
|
199
|
+
copied: "Copied",
|
|
200
|
+
copyFailed: "Copy failed",
|
|
201
|
+
regenerate: "Regenerate response",
|
|
202
|
+
edit: "Edit message",
|
|
203
|
+
goodResponse: "Good response",
|
|
204
|
+
badResponse: "Bad response",
|
|
205
|
+
versionLabel: (index, count) => `Version ${index} of ${count}`,
|
|
206
|
+
previousVersion: "Previous version",
|
|
207
|
+
nextVersion: "Next version",
|
|
208
|
+
saveEdit: "Save and resend",
|
|
209
|
+
cancelEdit: "Cancel",
|
|
210
|
+
editLabel: "Edit your message",
|
|
211
|
+
attachmentsList: "Attachments",
|
|
212
|
+
download: (name) => `Download ${name}`,
|
|
213
|
+
thinkingIndicator: "Assistant is thinking",
|
|
214
|
+
errorKinds: {
|
|
215
|
+
network: {
|
|
216
|
+
title: "Connection problem",
|
|
217
|
+
description: "The request did not complete. Check your connection and try again."
|
|
218
|
+
},
|
|
219
|
+
"rate-limit": {
|
|
220
|
+
title: "Too many requests",
|
|
221
|
+
description: "The service asked us to slow down. Wait a moment, then try again."
|
|
222
|
+
},
|
|
223
|
+
moderation: {
|
|
224
|
+
title: "Response blocked",
|
|
225
|
+
description: "The service declined to answer this message. Edit it and try again."
|
|
226
|
+
},
|
|
227
|
+
auth: {
|
|
228
|
+
title: "Not signed in",
|
|
229
|
+
description: "The service rejected the credentials. Sign in again, then retry."
|
|
230
|
+
},
|
|
231
|
+
unknown: {
|
|
232
|
+
title: "Something went wrong",
|
|
233
|
+
description: "The response could not be completed."
|
|
234
|
+
}
|
|
235
|
+
},
|
|
236
|
+
retry: "Retry",
|
|
237
|
+
dismiss: "Dismiss",
|
|
238
|
+
editInstead: "Edit message",
|
|
239
|
+
announceReply: (text) => `Assistant replied: ${text}`,
|
|
240
|
+
announceStopped: "Response stopped",
|
|
241
|
+
announceApproval: (toolName) => `Approval needed for ${toolName}`,
|
|
242
|
+
copyCode: "Copy code",
|
|
243
|
+
codeCopied: "Code copied",
|
|
244
|
+
codeBlock: (language) => language === "" ? "Code" : `Code, ${language}`,
|
|
245
|
+
plainText: "Text",
|
|
246
|
+
table: "Table",
|
|
247
|
+
taskDone: "Completed",
|
|
248
|
+
taskTodo: "Not completed",
|
|
249
|
+
footnotes: "Footnotes",
|
|
250
|
+
footnoteBack: "Back to reference",
|
|
251
|
+
imageBlocked: "Image not shown",
|
|
252
|
+
copyJson: "Copy",
|
|
253
|
+
showMore: (hidden) => `Show ${hidden} more characters`,
|
|
254
|
+
showLess: "Show less"
|
|
255
|
+
};
|
|
256
|
+
function mergeMessages(base, ...overrides) {
|
|
257
|
+
let result = base;
|
|
258
|
+
for (const override of overrides) {
|
|
259
|
+
if (!override) continue;
|
|
260
|
+
const next = { ...result };
|
|
261
|
+
for (const [key, value] of Object.entries(override)) {
|
|
262
|
+
if (value === void 0) continue;
|
|
263
|
+
if (key === "roles" || key === "toolStates") {
|
|
264
|
+
next[key] = { ...result[key], ...value };
|
|
265
|
+
} else if (key === "errorKinds") {
|
|
266
|
+
const merged = { ...result.errorKinds };
|
|
267
|
+
for (const [kind, entry] of Object.entries(value)) {
|
|
268
|
+
merged[kind] = { ...result.errorKinds[kind], ...entry };
|
|
269
|
+
}
|
|
270
|
+
next[key] = merged;
|
|
271
|
+
} else {
|
|
272
|
+
next[key] = value;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
result = next;
|
|
276
|
+
}
|
|
277
|
+
return result;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// src/react/messages-context.tsx
|
|
281
|
+
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
282
|
+
var MessagesContext = createContext(defaultMessages);
|
|
283
|
+
function AiMessagesProvider({ messages, children }) {
|
|
284
|
+
const parent = useContext(MessagesContext);
|
|
285
|
+
const value = useMemo(() => mergeMessages(parent, messages), [parent, messages]);
|
|
286
|
+
return /* @__PURE__ */ jsx2(MessagesContext.Provider, { value, children });
|
|
287
|
+
}
|
|
288
|
+
function useAiMessages(override) {
|
|
289
|
+
const context = useContext(MessagesContext);
|
|
290
|
+
return useMemo(() => override ? mergeMessages(context, override) : context, [context, override]);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// src/react/use-stick-to-bottom.ts
|
|
294
|
+
import { useCallback as useCallback2, useEffect as useEffect2, useLayoutEffect as useLayoutEffect2, useRef as useRef2, useState as useState2 } from "react";
|
|
295
|
+
|
|
296
|
+
// src/react/internal/hooks.ts
|
|
297
|
+
import {
|
|
298
|
+
useCallback,
|
|
299
|
+
useEffect,
|
|
300
|
+
useLayoutEffect,
|
|
301
|
+
useRef,
|
|
302
|
+
useState,
|
|
303
|
+
useSyncExternalStore
|
|
304
|
+
} from "react";
|
|
305
|
+
function useEventCallback(fn) {
|
|
306
|
+
const ref = useRef(fn);
|
|
307
|
+
useLayoutEffect(() => {
|
|
308
|
+
ref.current = fn;
|
|
309
|
+
});
|
|
310
|
+
return useCallback((...args) => ref.current(...args), []);
|
|
311
|
+
}
|
|
312
|
+
function useControllableState(options) {
|
|
313
|
+
const { value, defaultValue, onChange } = options;
|
|
314
|
+
const [inner, setInner] = useState(defaultValue);
|
|
315
|
+
const controlled = value !== void 0;
|
|
316
|
+
const current = controlled ? value : inner;
|
|
317
|
+
const notify = useEventCallback((next) => onChange?.(next));
|
|
318
|
+
const set = useCallback(
|
|
319
|
+
(next) => {
|
|
320
|
+
if (!controlled) setInner(next);
|
|
321
|
+
notify(next);
|
|
322
|
+
},
|
|
323
|
+
[controlled, notify]
|
|
324
|
+
);
|
|
325
|
+
return [current, set];
|
|
326
|
+
}
|
|
327
|
+
var REDUCED_MOTION_QUERY = "(prefers-reduced-motion: reduce)";
|
|
328
|
+
function subscribeReducedMotion(onChange) {
|
|
329
|
+
if (typeof window === "undefined" || typeof window.matchMedia !== "function") return () => {
|
|
330
|
+
};
|
|
331
|
+
const query = window.matchMedia(REDUCED_MOTION_QUERY);
|
|
332
|
+
query.addEventListener?.("change", onChange);
|
|
333
|
+
return () => query.removeEventListener?.("change", onChange);
|
|
334
|
+
}
|
|
335
|
+
function getReducedMotion() {
|
|
336
|
+
if (typeof window === "undefined" || typeof window.matchMedia !== "function") return false;
|
|
337
|
+
return window.matchMedia(REDUCED_MOTION_QUERY).matches;
|
|
338
|
+
}
|
|
339
|
+
function usePrefersReducedMotion() {
|
|
340
|
+
return useSyncExternalStore(subscribeReducedMotion, getReducedMotion, () => false);
|
|
341
|
+
}
|
|
342
|
+
function useHorizontalOverflow(ref, content) {
|
|
343
|
+
const [overflowing, setOverflowing] = useState(false);
|
|
344
|
+
useLayoutEffect(() => {
|
|
345
|
+
const element = ref.current;
|
|
346
|
+
if (!element) return;
|
|
347
|
+
const measure = () => setOverflowing(element.scrollWidth > element.clientWidth);
|
|
348
|
+
measure();
|
|
349
|
+
if (typeof ResizeObserver === "undefined") return;
|
|
350
|
+
const observer = new ResizeObserver(measure);
|
|
351
|
+
observer.observe(element);
|
|
352
|
+
return () => observer.disconnect();
|
|
353
|
+
}, [ref, content]);
|
|
354
|
+
return overflowing;
|
|
355
|
+
}
|
|
356
|
+
function useTransientState(resting, delay = 2e3) {
|
|
357
|
+
const [value, setValue] = useState(resting);
|
|
358
|
+
const timer = useRef(void 0);
|
|
359
|
+
const set = useCallback(
|
|
360
|
+
(next) => {
|
|
361
|
+
setValue(next);
|
|
362
|
+
clearTimeout(timer.current);
|
|
363
|
+
if (next !== resting) {
|
|
364
|
+
timer.current = setTimeout(() => setValue(resting), delay);
|
|
365
|
+
}
|
|
366
|
+
},
|
|
367
|
+
[resting, delay]
|
|
368
|
+
);
|
|
369
|
+
useEffect(() => () => clearTimeout(timer.current), []);
|
|
370
|
+
return [value, set];
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// src/react/use-stick-to-bottom.ts
|
|
374
|
+
var INSTANT_SETTLE_MS = 120;
|
|
375
|
+
var SMOOTH_SETTLE_MS = 800;
|
|
376
|
+
function useStickToBottom({ threshold = 48, onAtBottomChange } = {}) {
|
|
377
|
+
const scrollerRef = useRef2(null);
|
|
378
|
+
const contentRef = useRef2(null);
|
|
379
|
+
const following = useRef2(true);
|
|
380
|
+
const ours = useRef2(false);
|
|
381
|
+
const settleTimer = useRef2(void 0);
|
|
382
|
+
const lastAtBottom = useRef2(true);
|
|
383
|
+
const [atBottom, setAtBottomState] = useState2(true);
|
|
384
|
+
const reducedMotion = usePrefersReducedMotion();
|
|
385
|
+
const notify = useEventCallback((value) => onAtBottomChange?.(value));
|
|
386
|
+
const setAtBottom = useCallback2(
|
|
387
|
+
(value) => {
|
|
388
|
+
if (lastAtBottom.current === value) return;
|
|
389
|
+
lastAtBottom.current = value;
|
|
390
|
+
setAtBottomState(value);
|
|
391
|
+
notify(value);
|
|
392
|
+
},
|
|
393
|
+
[notify]
|
|
394
|
+
);
|
|
395
|
+
const isNearBottom = useCallback2(
|
|
396
|
+
(element) => element.scrollHeight - element.scrollTop - element.clientHeight <= threshold,
|
|
397
|
+
[threshold]
|
|
398
|
+
);
|
|
399
|
+
const scrollToBottom = useCallback2(
|
|
400
|
+
(behavior = "auto") => {
|
|
401
|
+
const element = scrollerRef.current;
|
|
402
|
+
if (!element) return;
|
|
403
|
+
const effective = reducedMotion ? "auto" : behavior;
|
|
404
|
+
following.current = true;
|
|
405
|
+
ours.current = true;
|
|
406
|
+
clearTimeout(settleTimer.current);
|
|
407
|
+
settleTimer.current = setTimeout(
|
|
408
|
+
() => {
|
|
409
|
+
ours.current = false;
|
|
410
|
+
},
|
|
411
|
+
effective === "smooth" ? SMOOTH_SETTLE_MS : INSTANT_SETTLE_MS
|
|
412
|
+
);
|
|
413
|
+
element.scrollTo({ top: element.scrollHeight, behavior: effective });
|
|
414
|
+
setAtBottom(true);
|
|
415
|
+
},
|
|
416
|
+
[reducedMotion, setAtBottom]
|
|
417
|
+
);
|
|
418
|
+
useLayoutEffect2(() => {
|
|
419
|
+
scrollToBottom("auto");
|
|
420
|
+
}, []);
|
|
421
|
+
useEffect2(() => {
|
|
422
|
+
const scroller = scrollerRef.current;
|
|
423
|
+
if (!scroller) return;
|
|
424
|
+
const release = () => {
|
|
425
|
+
ours.current = false;
|
|
426
|
+
clearTimeout(settleTimer.current);
|
|
427
|
+
};
|
|
428
|
+
const handleScroll = () => {
|
|
429
|
+
const near = isNearBottom(scroller);
|
|
430
|
+
if (ours.current) {
|
|
431
|
+
if (near) {
|
|
432
|
+
ours.current = false;
|
|
433
|
+
clearTimeout(settleTimer.current);
|
|
434
|
+
following.current = true;
|
|
435
|
+
setAtBottom(true);
|
|
436
|
+
}
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
following.current = near;
|
|
440
|
+
setAtBottom(near);
|
|
441
|
+
};
|
|
442
|
+
const handleWheel = (event) => {
|
|
443
|
+
if (event.deltaY < 0) {
|
|
444
|
+
release();
|
|
445
|
+
if (scroller.scrollHeight > scroller.clientHeight) following.current = false;
|
|
446
|
+
}
|
|
447
|
+
};
|
|
448
|
+
const handleKey = (event) => {
|
|
449
|
+
if (event.target !== scroller) return;
|
|
450
|
+
if (event.key === "PageUp" || event.key === "ArrowUp" || event.key === "Home") {
|
|
451
|
+
release();
|
|
452
|
+
if (scroller.scrollHeight > scroller.clientHeight) following.current = false;
|
|
453
|
+
}
|
|
454
|
+
};
|
|
455
|
+
scroller.addEventListener("scroll", handleScroll, { passive: true });
|
|
456
|
+
scroller.addEventListener("wheel", handleWheel, { passive: true });
|
|
457
|
+
scroller.addEventListener("touchmove", release, { passive: true });
|
|
458
|
+
scroller.addEventListener("keydown", handleKey);
|
|
459
|
+
let observer;
|
|
460
|
+
if (typeof ResizeObserver !== "undefined") {
|
|
461
|
+
observer = new ResizeObserver(() => {
|
|
462
|
+
if (following.current) scrollToBottom("auto");
|
|
463
|
+
});
|
|
464
|
+
observer.observe(scroller);
|
|
465
|
+
if (contentRef.current) observer.observe(contentRef.current);
|
|
466
|
+
}
|
|
467
|
+
return () => {
|
|
468
|
+
scroller.removeEventListener("scroll", handleScroll);
|
|
469
|
+
scroller.removeEventListener("wheel", handleWheel);
|
|
470
|
+
scroller.removeEventListener("touchmove", release);
|
|
471
|
+
scroller.removeEventListener("keydown", handleKey);
|
|
472
|
+
observer?.disconnect();
|
|
473
|
+
clearTimeout(settleTimer.current);
|
|
474
|
+
};
|
|
475
|
+
}, [isNearBottom, scrollToBottom, setAtBottom]);
|
|
476
|
+
return { scrollerRef, contentRef, atBottom, scrollToBottom };
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// src/react/conversation.tsx
|
|
480
|
+
import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
481
|
+
function Conversation({
|
|
482
|
+
children,
|
|
483
|
+
stickThreshold,
|
|
484
|
+
onAtBottomChange,
|
|
485
|
+
label,
|
|
486
|
+
handleRef,
|
|
487
|
+
messages: override,
|
|
488
|
+
className,
|
|
489
|
+
ref,
|
|
490
|
+
...props
|
|
491
|
+
}) {
|
|
492
|
+
const messages = useAiMessages(override);
|
|
493
|
+
const { scrollerRef, contentRef, atBottom, scrollToBottom } = useStickToBottom({
|
|
494
|
+
threshold: stickThreshold,
|
|
495
|
+
onAtBottomChange
|
|
496
|
+
});
|
|
497
|
+
useImperativeHandle(handleRef, () => ({ scrollToBottom }), [scrollToBottom]);
|
|
498
|
+
return /* @__PURE__ */ jsxs2("div", { ...props, ref, "data-kit": "ai-conversation", className: cx(className), children: [
|
|
499
|
+
/* @__PURE__ */ jsx3(
|
|
500
|
+
"div",
|
|
501
|
+
{
|
|
502
|
+
ref: scrollerRef,
|
|
503
|
+
"data-kit": "ai-conversation-scroller",
|
|
504
|
+
role: "log",
|
|
505
|
+
"aria-live": "polite",
|
|
506
|
+
"aria-relevant": "additions",
|
|
507
|
+
"aria-label": label ?? messages.conversation,
|
|
508
|
+
tabIndex: 0,
|
|
509
|
+
children: /* @__PURE__ */ jsx3("div", { ref: contentRef, "data-kit": "ai-conversation-content", children })
|
|
510
|
+
}
|
|
511
|
+
),
|
|
512
|
+
atBottom ? null : /* @__PURE__ */ jsx3("div", { "data-kit": "ai-jump-to-latest", children: /* @__PURE__ */ jsxs2(
|
|
513
|
+
Button,
|
|
514
|
+
{
|
|
515
|
+
variant: "outline",
|
|
516
|
+
tone: "neutral",
|
|
517
|
+
size: "sm",
|
|
518
|
+
onClick: () => {
|
|
519
|
+
scrollToBottom("smooth");
|
|
520
|
+
scrollerRef.current?.focus({ preventScroll: true });
|
|
521
|
+
},
|
|
522
|
+
children: [
|
|
523
|
+
/* @__PURE__ */ jsx3(ArrowDownGlyph, { size: "sm" }),
|
|
524
|
+
messages.jumpToLatest
|
|
525
|
+
]
|
|
526
|
+
}
|
|
527
|
+
) })
|
|
528
|
+
] });
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// src/react/empty-thread.tsx
|
|
532
|
+
import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
533
|
+
function EmptyThread({ title, description, icon, children, messages: override, className, ref, ...props }) {
|
|
534
|
+
const messages = useAiMessages(override);
|
|
535
|
+
return /* @__PURE__ */ jsxs3("div", { ...props, ref, "data-kit": "ai-empty-thread", className: cx(className), children: [
|
|
536
|
+
/* @__PURE__ */ jsx4("span", { "data-kit": "ai-empty-icon", "aria-hidden": "true", children: icon ?? /* @__PURE__ */ jsx4(SparklesGlyph, { size: "lg" }) }),
|
|
537
|
+
/* @__PURE__ */ jsx4("p", { "data-kit": "ai-empty-title", dir: "auto", children: title ?? messages.emptyTitle }),
|
|
538
|
+
/* @__PURE__ */ jsx4("p", { "data-kit": "ai-empty-description", dir: "auto", children: description ?? messages.emptyDescription }),
|
|
539
|
+
children
|
|
540
|
+
] });
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// src/react/error-banner.tsx
|
|
544
|
+
import { Icon } from "@x12i/icons";
|
|
545
|
+
import { Button as Button2, IconButton } from "@x12i/ui";
|
|
546
|
+
import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
547
|
+
function ErrorBanner({
|
|
548
|
+
error,
|
|
549
|
+
onRetry,
|
|
550
|
+
onDismiss,
|
|
551
|
+
onEdit,
|
|
552
|
+
showDetail = true,
|
|
553
|
+
messages: override,
|
|
554
|
+
className,
|
|
555
|
+
ref,
|
|
556
|
+
...props
|
|
557
|
+
}) {
|
|
558
|
+
const messages = useAiMessages(override);
|
|
559
|
+
const kind = error.kind ?? "unknown";
|
|
560
|
+
const copy = messages.errorKinds[kind];
|
|
561
|
+
const canRetry = onRetry !== void 0 && error.retryable !== false;
|
|
562
|
+
const canEdit = onEdit !== void 0 && kind === "moderation";
|
|
563
|
+
return /* @__PURE__ */ jsxs4("div", { ...props, ref, role: "alert", "data-kit": "ai-error-banner", "data-kind": kind, className: cx(className), children: [
|
|
564
|
+
/* @__PURE__ */ jsx5("span", { "data-kit": "ai-error-icon", children: /* @__PURE__ */ jsx5(Icon, { name: "alert-circle", size: 20 }) }),
|
|
565
|
+
/* @__PURE__ */ jsxs4("div", { "data-kit": "ai-error-body", children: [
|
|
566
|
+
/* @__PURE__ */ jsx5("p", { "data-kit": "ai-error-title", children: copy.title }),
|
|
567
|
+
/* @__PURE__ */ jsx5("p", { "data-kit": "ai-error-description", children: copy.description }),
|
|
568
|
+
showDetail && error.message !== "" ? /* @__PURE__ */ jsx5("p", { "data-kit": "ai-error-detail", dir: "auto", children: error.message }) : null,
|
|
569
|
+
canRetry || canEdit ? /* @__PURE__ */ jsxs4("div", { "data-kit": "ai-error-actions", children: [
|
|
570
|
+
canRetry ? /* @__PURE__ */ jsx5(Button2, { variant: "outline", tone: "neutral", size: "sm", onClick: onRetry, children: messages.retry }) : null,
|
|
571
|
+
canEdit ? /* @__PURE__ */ jsx5(Button2, { variant: "outline", tone: "neutral", size: "sm", onClick: onEdit, children: messages.editInstead }) : null
|
|
572
|
+
] }) : null
|
|
573
|
+
] }),
|
|
574
|
+
onDismiss ? /* @__PURE__ */ jsx5(IconButton, { variant: "ghost", tone: "neutral", size: "sm", label: messages.dismiss, onClick: onDismiss, children: /* @__PURE__ */ jsx5(Icon, { name: "close", size: 16 }) }) : null
|
|
575
|
+
] });
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// src/react/message.tsx
|
|
579
|
+
import { Badge as Badge3, Button as Button5, VisuallyHidden as VisuallyHidden7 } from "@x12i/ui";
|
|
580
|
+
import {
|
|
581
|
+
useEffect as useEffect5,
|
|
582
|
+
useMemo as useMemo4,
|
|
583
|
+
useRef as useRef9,
|
|
584
|
+
useState as useState5
|
|
585
|
+
} from "react";
|
|
586
|
+
|
|
587
|
+
// src/react/attachments.tsx
|
|
588
|
+
import { VisuallyHidden } from "@x12i/ui";
|
|
589
|
+
import { Fragment, jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
590
|
+
function isImage(file) {
|
|
591
|
+
return file.mediaType.toLowerCase().startsWith("image/") && file.mediaType.toLowerCase() !== "image/svg+xml";
|
|
592
|
+
}
|
|
593
|
+
function Attachments({ files, locale, messages: override, className, ref, ...props }) {
|
|
594
|
+
const messages = useAiMessages(override);
|
|
595
|
+
if (files.length === 0) return null;
|
|
596
|
+
return /* @__PURE__ */ jsx6("ul", { ...props, ref, role: "list", "aria-label": messages.attachmentsList, "data-kit": "ai-attachments", className: cx(className), children: files.map((file, position) => {
|
|
597
|
+
const image = isImage(file);
|
|
598
|
+
const url = image ? sanitizeImageUrl(file.url, { allowDataImages: true, allowBlob: true }) : sanitizeUrl(file.url, { protocols: ["http:", "https:", "blob:"] });
|
|
599
|
+
const size = file.size === void 0 ? "" : formatBytes(file.size, locale);
|
|
600
|
+
return /* @__PURE__ */ jsx6("li", { "data-kit": "ai-attachment", "data-kind": image ? "image" : "file", children: image && url ? /* @__PURE__ */ jsxs5("a", { href: url, target: "_blank", rel: "noopener noreferrer", children: [
|
|
601
|
+
/* @__PURE__ */ jsx6("img", { src: url, alt: file.name, loading: "lazy", decoding: "async", referrerPolicy: "no-referrer" }),
|
|
602
|
+
/* @__PURE__ */ jsxs5(VisuallyHidden, { children: [
|
|
603
|
+
" (",
|
|
604
|
+
messages.opensInNewTab,
|
|
605
|
+
")"
|
|
606
|
+
] })
|
|
607
|
+
] }) : /* @__PURE__ */ jsx6(Fragment, { children: url ? /* @__PURE__ */ jsxs5("a", { href: url, download: file.name, "aria-label": messages.download(file.name), "data-kit": "ai-attachment-link", children: [
|
|
608
|
+
/* @__PURE__ */ jsx6(FileGlyph, { size: "sm" }),
|
|
609
|
+
/* @__PURE__ */ jsx6("span", { "data-kit": "ai-attachment-name", dir: "auto", children: file.name }),
|
|
610
|
+
size === "" ? null : /* @__PURE__ */ jsx6("span", { "data-kit": "ai-attachment-size", children: size })
|
|
611
|
+
] }) : /* @__PURE__ */ jsxs5("span", { "data-kit": "ai-attachment-link", "data-inert": "", children: [
|
|
612
|
+
/* @__PURE__ */ jsx6(FileGlyph, { size: "sm" }),
|
|
613
|
+
/* @__PURE__ */ jsx6("span", { "data-kit": "ai-attachment-name", dir: "auto", children: file.name }),
|
|
614
|
+
size === "" ? null : /* @__PURE__ */ jsx6("span", { "data-kit": "ai-attachment-size", children: size })
|
|
615
|
+
] }) }) }, file.id ?? `${file.url}-${position}`);
|
|
616
|
+
}) });
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// src/react/internal/json-view.tsx
|
|
620
|
+
import { Button as Button3 } from "@x12i/ui";
|
|
621
|
+
import { useRef as useRef3, useState as useState3 } from "react";
|
|
622
|
+
|
|
623
|
+
// src/react/internal/copy-button.tsx
|
|
624
|
+
import { Icon as Icon2 } from "@x12i/icons";
|
|
625
|
+
import { IconButton as IconButton2, VisuallyHidden as VisuallyHidden2 } from "@x12i/ui";
|
|
626
|
+
import { useCallback as useCallback3, useId } from "react";
|
|
627
|
+
|
|
628
|
+
// src/react/internal/clipboard.ts
|
|
629
|
+
async function writeToClipboard(text) {
|
|
630
|
+
try {
|
|
631
|
+
if (typeof navigator !== "undefined" && navigator.clipboard?.writeText) {
|
|
632
|
+
await navigator.clipboard.writeText(text);
|
|
633
|
+
return true;
|
|
634
|
+
}
|
|
635
|
+
} catch {
|
|
636
|
+
}
|
|
637
|
+
if (typeof document === "undefined") return false;
|
|
638
|
+
const previous = document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
|
639
|
+
const textarea = document.createElement("textarea");
|
|
640
|
+
try {
|
|
641
|
+
textarea.value = text;
|
|
642
|
+
textarea.setAttribute("readonly", "");
|
|
643
|
+
textarea.setAttribute("aria-hidden", "true");
|
|
644
|
+
textarea.style.position = "fixed";
|
|
645
|
+
textarea.style.opacity = "0";
|
|
646
|
+
textarea.style.pointerEvents = "none";
|
|
647
|
+
document.body.appendChild(textarea);
|
|
648
|
+
textarea.select();
|
|
649
|
+
return typeof document.execCommand === "function" ? document.execCommand("copy") : false;
|
|
650
|
+
} catch {
|
|
651
|
+
return false;
|
|
652
|
+
} finally {
|
|
653
|
+
textarea.remove();
|
|
654
|
+
previous?.focus();
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// src/react/internal/copy-button.tsx
|
|
659
|
+
import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
660
|
+
function CopyButton({ text, label, copiedLabel, failedLabel, onCopied, ...props }) {
|
|
661
|
+
const [status, setStatus] = useTransientState("idle");
|
|
662
|
+
const statusId = useId();
|
|
663
|
+
const handleClick = useCallback3(async () => {
|
|
664
|
+
const value = typeof text === "function" ? text() : text;
|
|
665
|
+
const ok = await writeToClipboard(value);
|
|
666
|
+
setStatus(ok ? "copied" : "failed");
|
|
667
|
+
if (ok) onCopied?.(value);
|
|
668
|
+
}, [text, onCopied, setStatus]);
|
|
669
|
+
return /* @__PURE__ */ jsxs6("span", { "data-kit": "ai-copy", "data-state": status, children: [
|
|
670
|
+
/* @__PURE__ */ jsx7(
|
|
671
|
+
IconButton2,
|
|
672
|
+
{
|
|
673
|
+
variant: "ghost",
|
|
674
|
+
tone: "neutral",
|
|
675
|
+
size: "sm",
|
|
676
|
+
...props,
|
|
677
|
+
label,
|
|
678
|
+
"aria-describedby": statusId,
|
|
679
|
+
onClick: () => {
|
|
680
|
+
void handleClick();
|
|
681
|
+
},
|
|
682
|
+
children: /* @__PURE__ */ jsx7(Icon2, { name: status === "copied" ? "check" : "copy", size: 16 })
|
|
683
|
+
}
|
|
684
|
+
),
|
|
685
|
+
/* @__PURE__ */ jsx7(VisuallyHidden2, { id: statusId, role: "status", "aria-live": "polite", "aria-atomic": "true", children: status === "copied" ? copiedLabel : status === "failed" ? failedLabel : "" })
|
|
686
|
+
] });
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// src/react/internal/json-view.tsx
|
|
690
|
+
import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
691
|
+
function safeStringify(value, space = 2) {
|
|
692
|
+
if (typeof value === "string") return value;
|
|
693
|
+
const seen = /* @__PURE__ */ new WeakSet();
|
|
694
|
+
try {
|
|
695
|
+
const json = JSON.stringify(
|
|
696
|
+
value,
|
|
697
|
+
(_key, entry) => {
|
|
698
|
+
if (typeof entry === "bigint") return `${entry.toString()}n`;
|
|
699
|
+
if (typeof entry === "function" || typeof entry === "symbol") return String(entry);
|
|
700
|
+
if (typeof entry === "object" && entry !== null) {
|
|
701
|
+
if (seen.has(entry)) return "[Circular]";
|
|
702
|
+
seen.add(entry);
|
|
703
|
+
}
|
|
704
|
+
return entry;
|
|
705
|
+
},
|
|
706
|
+
space
|
|
707
|
+
);
|
|
708
|
+
return json ?? "null";
|
|
709
|
+
} catch {
|
|
710
|
+
return String(value);
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
function JsonView({ value, label, maxChars = 4e3, messages: override }) {
|
|
714
|
+
const messages = useAiMessages(override);
|
|
715
|
+
const [expanded, setExpanded] = useState3(false);
|
|
716
|
+
const preRef = useRef3(null);
|
|
717
|
+
const text = safeStringify(value);
|
|
718
|
+
const folded = !expanded && text.length > maxChars;
|
|
719
|
+
const shown = folded ? text.slice(0, maxChars) : text;
|
|
720
|
+
const overflowing = useHorizontalOverflow(preRef, shown);
|
|
721
|
+
return /* @__PURE__ */ jsxs7("div", { "data-kit": "ai-json-view", dir: "ltr", role: "group", "aria-label": label, children: [
|
|
722
|
+
/* @__PURE__ */ jsxs7("div", { "data-kit": "ai-json-view-header", children: [
|
|
723
|
+
/* @__PURE__ */ jsx8("span", { "data-kit": "ai-json-view-label", "aria-hidden": "true", children: label }),
|
|
724
|
+
/* @__PURE__ */ jsx8(
|
|
725
|
+
CopyButton,
|
|
726
|
+
{
|
|
727
|
+
text,
|
|
728
|
+
label: messages.copyJson,
|
|
729
|
+
copiedLabel: messages.copied,
|
|
730
|
+
failedLabel: messages.copyFailed
|
|
731
|
+
}
|
|
732
|
+
)
|
|
733
|
+
] }),
|
|
734
|
+
/* @__PURE__ */ jsx8("pre", { ref: preRef, tabIndex: overflowing ? 0 : void 0, children: /* @__PURE__ */ jsx8("code", { children: shown }) }),
|
|
735
|
+
text.length > maxChars ? /* @__PURE__ */ jsx8(
|
|
736
|
+
Button3,
|
|
737
|
+
{
|
|
738
|
+
variant: "ghost",
|
|
739
|
+
tone: "neutral",
|
|
740
|
+
size: "xs",
|
|
741
|
+
"aria-expanded": !folded,
|
|
742
|
+
onClick: () => setExpanded((open) => !open),
|
|
743
|
+
children: folded ? messages.showMore(text.length - maxChars) : messages.showLess
|
|
744
|
+
}
|
|
745
|
+
) : null
|
|
746
|
+
] });
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
// src/react/internal/use-autosize.ts
|
|
750
|
+
import { useLayoutEffect as useLayoutEffect3 } from "react";
|
|
751
|
+
function useAutosize(ref, value) {
|
|
752
|
+
useLayoutEffect3(() => {
|
|
753
|
+
const element = ref.current;
|
|
754
|
+
if (!element) return;
|
|
755
|
+
const fit = () => {
|
|
756
|
+
element.style.setProperty("--_ai-height", "auto");
|
|
757
|
+
const chrome = element.offsetHeight - element.clientHeight;
|
|
758
|
+
const next = element.scrollHeight + Math.max(0, chrome);
|
|
759
|
+
element.style.setProperty("--_ai-height", next > 0 ? `${next}px` : "auto");
|
|
760
|
+
};
|
|
761
|
+
fit();
|
|
762
|
+
if (typeof ResizeObserver === "undefined") return;
|
|
763
|
+
let lastWidth = element.clientWidth;
|
|
764
|
+
const observer = new ResizeObserver(() => {
|
|
765
|
+
if (element.clientWidth !== lastWidth) {
|
|
766
|
+
lastWidth = element.clientWidth;
|
|
767
|
+
fit();
|
|
768
|
+
}
|
|
769
|
+
});
|
|
770
|
+
observer.observe(element);
|
|
771
|
+
return () => observer.disconnect();
|
|
772
|
+
}, [ref, value]);
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
// src/react/markdown.tsx
|
|
776
|
+
import { VisuallyHidden as VisuallyHidden4 } from "@x12i/ui";
|
|
777
|
+
import {
|
|
778
|
+
createContext as createContext3,
|
|
779
|
+
createElement,
|
|
780
|
+
memo,
|
|
781
|
+
useContext as useContext3,
|
|
782
|
+
useId as useId4,
|
|
783
|
+
useMemo as useMemo3,
|
|
784
|
+
useRef as useRef6
|
|
785
|
+
} from "react";
|
|
786
|
+
import ReactMarkdown from "react-markdown";
|
|
787
|
+
import remarkGfm from "remark-gfm";
|
|
788
|
+
|
|
789
|
+
// src/react/code-block.tsx
|
|
790
|
+
import { useRef as useRef4 } from "react";
|
|
791
|
+
import { jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
792
|
+
function CodeBlock({ code, language = "", onCopied, messages: override, className, ref, ...props }) {
|
|
793
|
+
const messages = useAiMessages(override);
|
|
794
|
+
const preRef = useRef4(null);
|
|
795
|
+
const overflowing = useHorizontalOverflow(preRef, code);
|
|
796
|
+
return /* @__PURE__ */ jsxs8(
|
|
797
|
+
"div",
|
|
798
|
+
{
|
|
799
|
+
...props,
|
|
800
|
+
ref,
|
|
801
|
+
"data-kit": "ai-code-block",
|
|
802
|
+
"data-language": language === "" ? void 0 : language,
|
|
803
|
+
dir: "ltr",
|
|
804
|
+
role: "group",
|
|
805
|
+
"aria-label": messages.codeBlock(language),
|
|
806
|
+
className: cx(className),
|
|
807
|
+
children: [
|
|
808
|
+
/* @__PURE__ */ jsxs8("div", { "data-kit": "ai-code-block-header", children: [
|
|
809
|
+
/* @__PURE__ */ jsx9("span", { "data-kit": "ai-code-block-language", "aria-hidden": "true", children: language === "" ? messages.plainText : language }),
|
|
810
|
+
/* @__PURE__ */ jsx9(
|
|
811
|
+
CopyButton,
|
|
812
|
+
{
|
|
813
|
+
text: code,
|
|
814
|
+
label: messages.copyCode,
|
|
815
|
+
copiedLabel: messages.codeCopied,
|
|
816
|
+
failedLabel: messages.copyFailed,
|
|
817
|
+
onCopied
|
|
818
|
+
}
|
|
819
|
+
)
|
|
820
|
+
] }),
|
|
821
|
+
/* @__PURE__ */ jsx9("pre", { ref: preRef, tabIndex: overflowing ? 0 : void 0, children: /* @__PURE__ */ jsx9("code", { className: language === "" ? void 0 : `language-${language}`, children: code }) })
|
|
822
|
+
]
|
|
823
|
+
}
|
|
824
|
+
);
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
// src/react/sources.tsx
|
|
828
|
+
import { Icon as Icon3 } from "@x12i/icons";
|
|
829
|
+
import { Badge, VisuallyHidden as VisuallyHidden3 } from "@x12i/ui";
|
|
830
|
+
import {
|
|
831
|
+
createContext as createContext2,
|
|
832
|
+
useCallback as useCallback4,
|
|
833
|
+
useContext as useContext2,
|
|
834
|
+
useEffect as useEffect3,
|
|
835
|
+
useId as useId3,
|
|
836
|
+
useMemo as useMemo2,
|
|
837
|
+
useRef as useRef5,
|
|
838
|
+
useState as useState4
|
|
839
|
+
} from "react";
|
|
840
|
+
|
|
841
|
+
// src/react/internal/disclosure.ts
|
|
842
|
+
import { useId as useId2 } from "react";
|
|
843
|
+
function useDisclosure({ open, defaultOpen = false, onOpenChange } = {}) {
|
|
844
|
+
const [current, setOpen] = useControllableState({ value: open, defaultValue: defaultOpen, onChange: onOpenChange });
|
|
845
|
+
const base = useId2();
|
|
846
|
+
const triggerId = `${base}-trigger`;
|
|
847
|
+
const panelId = `${base}-panel`;
|
|
848
|
+
const state = current ? "open" : "closed";
|
|
849
|
+
return {
|
|
850
|
+
open: current,
|
|
851
|
+
setOpen,
|
|
852
|
+
toggle: () => setOpen(!current),
|
|
853
|
+
triggerProps: {
|
|
854
|
+
id: triggerId,
|
|
855
|
+
type: "button",
|
|
856
|
+
"aria-expanded": current,
|
|
857
|
+
"aria-controls": panelId,
|
|
858
|
+
"data-state": state,
|
|
859
|
+
onClick: () => setOpen(!current)
|
|
860
|
+
},
|
|
861
|
+
panelProps: {
|
|
862
|
+
id: panelId,
|
|
863
|
+
hidden: !current,
|
|
864
|
+
"data-state": state
|
|
865
|
+
}
|
|
866
|
+
};
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
// src/react/sources.tsx
|
|
870
|
+
import { jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
871
|
+
var SourcesContext = createContext2(null);
|
|
872
|
+
function useSourcesContext() {
|
|
873
|
+
return useContext2(SourcesContext);
|
|
874
|
+
}
|
|
875
|
+
function sourceDomId(baseId, index) {
|
|
876
|
+
return `${baseId}-source-${index}`;
|
|
877
|
+
}
|
|
878
|
+
function SourcesProvider({ sources, children }) {
|
|
879
|
+
const baseId = useId3();
|
|
880
|
+
const handlers = useRef5(/* @__PURE__ */ new Set());
|
|
881
|
+
const register = useCallback4((handler) => {
|
|
882
|
+
handlers.current.add(handler);
|
|
883
|
+
return () => {
|
|
884
|
+
handlers.current.delete(handler);
|
|
885
|
+
};
|
|
886
|
+
}, []);
|
|
887
|
+
const reveal = useCallback4((index) => {
|
|
888
|
+
if (handlers.current.size === 0) return false;
|
|
889
|
+
handlers.current.forEach((handler) => handler(index));
|
|
890
|
+
return true;
|
|
891
|
+
}, []);
|
|
892
|
+
const value = useMemo2(() => ({ baseId, sources, reveal, register }), [baseId, sources, reveal, register]);
|
|
893
|
+
return /* @__PURE__ */ jsx10(SourcesContext.Provider, { value, children });
|
|
894
|
+
}
|
|
895
|
+
function Citation({ index, source: sourceProp, messages: override, className, onClick, ref, ...props }) {
|
|
896
|
+
const messages = useAiMessages(override);
|
|
897
|
+
const ambient = useSourcesContext();
|
|
898
|
+
const context = sourceProp ? null : ambient;
|
|
899
|
+
const source = sourceProp ?? context?.sources[index - 1];
|
|
900
|
+
const title = source?.title ?? "";
|
|
901
|
+
const label = messages.citationLabel(index, title);
|
|
902
|
+
const handleClick = (event) => {
|
|
903
|
+
onClick?.(event);
|
|
904
|
+
if (event.defaultPrevented || !context) return;
|
|
905
|
+
if (context.reveal(index)) event.preventDefault();
|
|
906
|
+
};
|
|
907
|
+
if (context && source) {
|
|
908
|
+
return /* @__PURE__ */ jsx10(
|
|
909
|
+
"a",
|
|
910
|
+
{
|
|
911
|
+
...props,
|
|
912
|
+
ref,
|
|
913
|
+
"data-kit": "ai-citation",
|
|
914
|
+
className: cx(className),
|
|
915
|
+
href: `#${sourceDomId(context.baseId, index)}`,
|
|
916
|
+
"aria-label": label,
|
|
917
|
+
onClick: handleClick,
|
|
918
|
+
children: index
|
|
919
|
+
}
|
|
920
|
+
);
|
|
921
|
+
}
|
|
922
|
+
const url = source ? sanitizeUrl(source.url, { allowRelative: false }) : null;
|
|
923
|
+
if (url) {
|
|
924
|
+
return /* @__PURE__ */ jsx10(
|
|
925
|
+
"a",
|
|
926
|
+
{
|
|
927
|
+
...props,
|
|
928
|
+
ref,
|
|
929
|
+
"data-kit": "ai-citation",
|
|
930
|
+
className: cx(className),
|
|
931
|
+
href: url,
|
|
932
|
+
target: "_blank",
|
|
933
|
+
rel: "noopener noreferrer",
|
|
934
|
+
"aria-label": `${label} (${messages.opensInNewTab})`,
|
|
935
|
+
onClick,
|
|
936
|
+
children: index
|
|
937
|
+
}
|
|
938
|
+
);
|
|
939
|
+
}
|
|
940
|
+
return /* @__PURE__ */ jsx10("span", { "data-kit": "ai-citation", "data-inert": "", className: cx(className), "aria-label": source ? label : void 0, children: index });
|
|
941
|
+
}
|
|
942
|
+
function SourceCard({ source, index, id, messages: override, className, ref, ...props }) {
|
|
943
|
+
const messages = useAiMessages(override);
|
|
944
|
+
const url = sanitizeUrl(source.url, { allowRelative: false });
|
|
945
|
+
const host = url ? getUrlHostname(url) : null;
|
|
946
|
+
return /* @__PURE__ */ jsxs9(
|
|
947
|
+
"li",
|
|
948
|
+
{
|
|
949
|
+
...props,
|
|
950
|
+
ref,
|
|
951
|
+
id,
|
|
952
|
+
tabIndex: -1,
|
|
953
|
+
"data-kit": "ai-source",
|
|
954
|
+
className: cx(className),
|
|
955
|
+
children: [
|
|
956
|
+
/* @__PURE__ */ jsx10("span", { "data-kit": "ai-source-index", "aria-hidden": "true", children: index }),
|
|
957
|
+
/* @__PURE__ */ jsxs9("div", { "data-kit": "ai-source-body", children: [
|
|
958
|
+
url ? /* @__PURE__ */ jsxs9("a", { "data-kit": "ai-source-link", href: url, target: "_blank", rel: "noopener noreferrer", dir: "auto", children: [
|
|
959
|
+
source.title,
|
|
960
|
+
/* @__PURE__ */ jsx10(Icon3, { name: "external-link", size: 12 }),
|
|
961
|
+
/* @__PURE__ */ jsxs9(VisuallyHidden3, { children: [
|
|
962
|
+
" (",
|
|
963
|
+
messages.opensInNewTab,
|
|
964
|
+
")"
|
|
965
|
+
] })
|
|
966
|
+
] }) : /* @__PURE__ */ jsxs9("span", { "data-kit": "ai-source-link", "data-inert": "", dir: "auto", children: [
|
|
967
|
+
source.title,
|
|
968
|
+
/* @__PURE__ */ jsxs9(VisuallyHidden3, { children: [
|
|
969
|
+
" (",
|
|
970
|
+
messages.noLink,
|
|
971
|
+
")"
|
|
972
|
+
] })
|
|
973
|
+
] }),
|
|
974
|
+
host ? /* @__PURE__ */ jsx10("span", { "data-kit": "ai-source-host", dir: "ltr", children: host }) : null,
|
|
975
|
+
source.snippet ? /* @__PURE__ */ jsx10("p", { "data-kit": "ai-source-snippet", dir: "auto", children: source.snippet }) : null
|
|
976
|
+
] })
|
|
977
|
+
]
|
|
978
|
+
}
|
|
979
|
+
);
|
|
980
|
+
}
|
|
981
|
+
function Sources({ sources: sourcesProp, open, defaultOpen = false, onOpenChange, messages: override, className, ref, ...props }) {
|
|
982
|
+
const messages = useAiMessages(override);
|
|
983
|
+
const context = useSourcesContext();
|
|
984
|
+
const sources = sourcesProp ?? context?.sources ?? [];
|
|
985
|
+
const ownId = useId3();
|
|
986
|
+
const baseId = context?.baseId ?? ownId;
|
|
987
|
+
const disclosure = useDisclosure({ open, defaultOpen, onOpenChange });
|
|
988
|
+
const [target, setTarget] = useState4(null);
|
|
989
|
+
const { setOpen } = disclosure;
|
|
990
|
+
useEffect3(() => {
|
|
991
|
+
if (!context) return;
|
|
992
|
+
return context.register((index) => {
|
|
993
|
+
setOpen(true);
|
|
994
|
+
setTarget((previous) => ({ index, nonce: (previous?.nonce ?? 0) + 1 }));
|
|
995
|
+
});
|
|
996
|
+
}, [context, setOpen]);
|
|
997
|
+
useEffect3(() => {
|
|
998
|
+
if (!target || !disclosure.open) return;
|
|
999
|
+
const element = document.getElementById(sourceDomId(baseId, target.index));
|
|
1000
|
+
element?.focus();
|
|
1001
|
+
element?.scrollIntoView?.({ block: "nearest" });
|
|
1002
|
+
}, [target, disclosure.open, baseId]);
|
|
1003
|
+
if (sources.length === 0) return null;
|
|
1004
|
+
return /* @__PURE__ */ jsxs9("div", { ...props, ref, "data-kit": "ai-sources", "data-state": disclosure.open ? "open" : "closed", className: cx(className), children: [
|
|
1005
|
+
/* @__PURE__ */ jsxs9("button", { ...disclosure.triggerProps, "data-kit": "ai-sources-trigger", children: [
|
|
1006
|
+
/* @__PURE__ */ jsx10("span", { children: messages.sources }),
|
|
1007
|
+
/* @__PURE__ */ jsx10(Badge, { size: "sm", title: messages.sourcesCount(sources.length), children: sources.length }),
|
|
1008
|
+
/* @__PURE__ */ jsx10(Icon3, { name: disclosure.open ? "chevron-up" : "chevron-down", size: 16 })
|
|
1009
|
+
] }),
|
|
1010
|
+
/* @__PURE__ */ jsx10("div", { ...disclosure.panelProps, "data-kit": "ai-sources-panel", children: disclosure.open ? /* @__PURE__ */ jsx10("ol", { role: "list", "data-kit": "ai-sources-list", children: sources.map((source, position) => /* @__PURE__ */ jsx10(
|
|
1011
|
+
SourceCard,
|
|
1012
|
+
{
|
|
1013
|
+
source,
|
|
1014
|
+
index: position + 1,
|
|
1015
|
+
id: sourceDomId(baseId, position + 1),
|
|
1016
|
+
messages: override
|
|
1017
|
+
},
|
|
1018
|
+
source.id
|
|
1019
|
+
)) }) : null })
|
|
1020
|
+
] });
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
// src/react/markdown.tsx
|
|
1024
|
+
import { jsx as jsx11, jsxs as jsxs10 } from "react/jsx-runtime";
|
|
1025
|
+
var ConfigContext = createContext3(null);
|
|
1026
|
+
function useConfig() {
|
|
1027
|
+
const config = useContext3(ConfigContext);
|
|
1028
|
+
if (!config) throw new Error("@x12i/ai: Markdown elements must render inside <Markdown>.");
|
|
1029
|
+
return config;
|
|
1030
|
+
}
|
|
1031
|
+
var SKIP_CITATIONS_IN = /* @__PURE__ */ new Set(["link", "linkReference", "code", "inlineCode", "definition", "html", "image", "imageReference"]);
|
|
1032
|
+
function citationNode(n) {
|
|
1033
|
+
return {
|
|
1034
|
+
type: "citation",
|
|
1035
|
+
data: { hName: "span", hProperties: { dataCitation: String(n) } },
|
|
1036
|
+
children: [{ type: "text", value: String(n) }]
|
|
1037
|
+
};
|
|
1038
|
+
}
|
|
1039
|
+
function collectDefinitions(node, into) {
|
|
1040
|
+
if (node.type === "definition" && node.identifier) into.add(node.identifier.toLowerCase());
|
|
1041
|
+
node.children?.forEach((child) => collectDefinitions(child, into));
|
|
1042
|
+
}
|
|
1043
|
+
function createCitationTransform(maxIndex) {
|
|
1044
|
+
return () => (tree) => {
|
|
1045
|
+
const definitions = /* @__PURE__ */ new Set();
|
|
1046
|
+
collectDefinitions(tree, definitions);
|
|
1047
|
+
const visit = (node) => {
|
|
1048
|
+
if (!node.children) return;
|
|
1049
|
+
const next = [];
|
|
1050
|
+
for (const child of node.children) {
|
|
1051
|
+
let raw = null;
|
|
1052
|
+
if (child.type === "text" && typeof child.value === "string" && child.value.includes("[")) {
|
|
1053
|
+
raw = child.value;
|
|
1054
|
+
} else if (child.type === "linkReference" && child.referenceType === "shortcut" && !definitions.has((child.identifier ?? "").toLowerCase()) && typeof child.label === "string") {
|
|
1055
|
+
raw = `[${child.label}]`;
|
|
1056
|
+
}
|
|
1057
|
+
if (raw !== null) {
|
|
1058
|
+
const segments = parseCitations(raw, { maxIndex });
|
|
1059
|
+
if (segments.some((segment) => segment.type === "citation")) {
|
|
1060
|
+
for (const segment of segments) {
|
|
1061
|
+
if (segment.type === "text") next.push({ type: "text", value: segment.text });
|
|
1062
|
+
else for (const n of segment.numbers) next.push(citationNode(n));
|
|
1063
|
+
}
|
|
1064
|
+
continue;
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
if (!SKIP_CITATIONS_IN.has(child.type)) visit(child);
|
|
1068
|
+
next.push(child);
|
|
1069
|
+
}
|
|
1070
|
+
node.children = next;
|
|
1071
|
+
};
|
|
1072
|
+
visit(tree);
|
|
1073
|
+
};
|
|
1074
|
+
}
|
|
1075
|
+
var BASE_PLUGINS = [remarkGfm];
|
|
1076
|
+
var citationPlugins = /* @__PURE__ */ new Map();
|
|
1077
|
+
function pluginsFor(maxIndex) {
|
|
1078
|
+
if (maxIndex <= 0) return BASE_PLUGINS;
|
|
1079
|
+
let plugins = citationPlugins.get(maxIndex);
|
|
1080
|
+
if (!plugins) {
|
|
1081
|
+
plugins = [remarkGfm, createCitationTransform(maxIndex)];
|
|
1082
|
+
citationPlugins.set(maxIndex, plugins);
|
|
1083
|
+
}
|
|
1084
|
+
return plugins;
|
|
1085
|
+
}
|
|
1086
|
+
function urlTransform(url, key) {
|
|
1087
|
+
if (key === "src") return sanitizeImageUrl(url, { allowDataImages: true }) ?? "";
|
|
1088
|
+
return sanitizeUrl(url) ?? "";
|
|
1089
|
+
}
|
|
1090
|
+
function withoutNode(props) {
|
|
1091
|
+
const rest = { ...props };
|
|
1092
|
+
delete rest.node;
|
|
1093
|
+
return rest;
|
|
1094
|
+
}
|
|
1095
|
+
function textOf(node) {
|
|
1096
|
+
if (!node) return "";
|
|
1097
|
+
if (node.type === "text") return node.value;
|
|
1098
|
+
if (node.type === "element") return node.children.map(textOf).join("");
|
|
1099
|
+
return "";
|
|
1100
|
+
}
|
|
1101
|
+
function Link(raw) {
|
|
1102
|
+
const { href, children, onClick, ...props } = withoutNode(raw);
|
|
1103
|
+
const config = useConfig();
|
|
1104
|
+
const url = sanitizeUrl(href);
|
|
1105
|
+
if (!url) {
|
|
1106
|
+
return /* @__PURE__ */ jsx11("span", { "data-kit": "ai-markdown-link", "data-blocked": "", children });
|
|
1107
|
+
}
|
|
1108
|
+
const isFragment = url.startsWith("#");
|
|
1109
|
+
const isContact = /^(?:mailto|tel):/i.test(url);
|
|
1110
|
+
const opensNewTab = !isFragment && !isContact;
|
|
1111
|
+
const anchorProps = { ...props, href: url };
|
|
1112
|
+
if (anchorProps["aria-describedby"] === "footnote-label") {
|
|
1113
|
+
anchorProps["aria-describedby"] = `${config.idPrefix}footnote-label`;
|
|
1114
|
+
}
|
|
1115
|
+
return /* @__PURE__ */ jsxs10(
|
|
1116
|
+
"a",
|
|
1117
|
+
{
|
|
1118
|
+
...anchorProps,
|
|
1119
|
+
"data-kit": "ai-markdown-link",
|
|
1120
|
+
"data-external": opensNewTab && isExternalUrl(url) ? "" : void 0,
|
|
1121
|
+
target: opensNewTab ? "_blank" : void 0,
|
|
1122
|
+
rel: isFragment ? void 0 : "noopener noreferrer",
|
|
1123
|
+
onClick: (event) => {
|
|
1124
|
+
onClick?.(event);
|
|
1125
|
+
config.onLinkClick?.(event, url);
|
|
1126
|
+
},
|
|
1127
|
+
children: [
|
|
1128
|
+
children,
|
|
1129
|
+
opensNewTab ? /* @__PURE__ */ jsxs10(VisuallyHidden4, { children: [
|
|
1130
|
+
" (",
|
|
1131
|
+
config.messages.opensInNewTab,
|
|
1132
|
+
")"
|
|
1133
|
+
] }) : null
|
|
1134
|
+
]
|
|
1135
|
+
}
|
|
1136
|
+
);
|
|
1137
|
+
}
|
|
1138
|
+
function Image({ src, alt, title }) {
|
|
1139
|
+
const config = useConfig();
|
|
1140
|
+
const altText = alt ?? "";
|
|
1141
|
+
let url = typeof src === "string" ? src : null;
|
|
1142
|
+
if (url !== null && config.transformImageUrl) url = config.transformImageUrl(url) ?? null;
|
|
1143
|
+
const safe = url === null ? null : sanitizeImageUrl(url, { allowDataImages: config.allowDataImages });
|
|
1144
|
+
const blocked = config.images === "none" || safe === null || safe.startsWith("data:") && config.images !== "inline";
|
|
1145
|
+
if (blocked || safe === null) {
|
|
1146
|
+
return /* @__PURE__ */ jsx11("span", { "data-kit": "ai-markdown-image", "data-blocked": "", dir: "auto", children: altText === "" ? config.messages.imageBlocked : altText });
|
|
1147
|
+
}
|
|
1148
|
+
if (config.images === "link") {
|
|
1149
|
+
return /* @__PURE__ */ jsxs10("a", { "data-kit": "ai-markdown-link", href: safe, target: "_blank", rel: "noopener noreferrer", dir: "auto", children: [
|
|
1150
|
+
altText === "" ? safe : altText,
|
|
1151
|
+
/* @__PURE__ */ jsxs10(VisuallyHidden4, { children: [
|
|
1152
|
+
" (",
|
|
1153
|
+
config.messages.opensInNewTab,
|
|
1154
|
+
")"
|
|
1155
|
+
] })
|
|
1156
|
+
] });
|
|
1157
|
+
}
|
|
1158
|
+
return /* @__PURE__ */ jsx11(
|
|
1159
|
+
"img",
|
|
1160
|
+
{
|
|
1161
|
+
"data-kit": "ai-markdown-image",
|
|
1162
|
+
src: safe,
|
|
1163
|
+
alt: altText,
|
|
1164
|
+
title,
|
|
1165
|
+
loading: "lazy",
|
|
1166
|
+
decoding: "async",
|
|
1167
|
+
referrerPolicy: "no-referrer"
|
|
1168
|
+
}
|
|
1169
|
+
);
|
|
1170
|
+
}
|
|
1171
|
+
function Pre({ node, children }) {
|
|
1172
|
+
const config = useConfig();
|
|
1173
|
+
const codeElement = node?.children.find(
|
|
1174
|
+
(child) => child.type === "element" && child.tagName === "code"
|
|
1175
|
+
);
|
|
1176
|
+
if (!codeElement) return /* @__PURE__ */ jsx11("pre", { children });
|
|
1177
|
+
const classNames = codeElement.properties?.className;
|
|
1178
|
+
const classList = Array.isArray(classNames) ? classNames.map(String) : [];
|
|
1179
|
+
const language = classList.find((name) => name.startsWith("language-"))?.slice("language-".length) ?? "";
|
|
1180
|
+
const code = textOf(codeElement).replace(/\n$/, "");
|
|
1181
|
+
return /* @__PURE__ */ jsx11(
|
|
1182
|
+
CodeBlock,
|
|
1183
|
+
{
|
|
1184
|
+
code,
|
|
1185
|
+
language,
|
|
1186
|
+
messages: config.override,
|
|
1187
|
+
onCopied: config.onCopyCode ? (copied) => config.onCopyCode?.(copied, language) : void 0
|
|
1188
|
+
}
|
|
1189
|
+
);
|
|
1190
|
+
}
|
|
1191
|
+
function Table(raw) {
|
|
1192
|
+
const { children, ...props } = withoutNode(raw);
|
|
1193
|
+
const { messages } = useConfig();
|
|
1194
|
+
const ref = useRef6(null);
|
|
1195
|
+
const overflowing = useHorizontalOverflow(ref, children);
|
|
1196
|
+
return /* @__PURE__ */ jsx11(
|
|
1197
|
+
"div",
|
|
1198
|
+
{
|
|
1199
|
+
ref,
|
|
1200
|
+
"data-kit": "ai-markdown-table",
|
|
1201
|
+
role: overflowing ? "group" : void 0,
|
|
1202
|
+
"aria-label": overflowing ? messages.table : void 0,
|
|
1203
|
+
tabIndex: overflowing ? 0 : void 0,
|
|
1204
|
+
children: /* @__PURE__ */ jsx11("table", { ...props, children })
|
|
1205
|
+
}
|
|
1206
|
+
);
|
|
1207
|
+
}
|
|
1208
|
+
function Input(raw) {
|
|
1209
|
+
const { checked, ...props } = withoutNode(raw);
|
|
1210
|
+
const { messages } = useConfig();
|
|
1211
|
+
if (props.type !== "checkbox") return null;
|
|
1212
|
+
return /* @__PURE__ */ jsx11(
|
|
1213
|
+
"input",
|
|
1214
|
+
{
|
|
1215
|
+
...props,
|
|
1216
|
+
type: "checkbox",
|
|
1217
|
+
checked: Boolean(checked),
|
|
1218
|
+
disabled: true,
|
|
1219
|
+
readOnly: true,
|
|
1220
|
+
"aria-label": checked ? messages.taskDone : messages.taskTodo
|
|
1221
|
+
}
|
|
1222
|
+
);
|
|
1223
|
+
}
|
|
1224
|
+
function Span(raw) {
|
|
1225
|
+
const { children, ...props } = withoutNode(raw);
|
|
1226
|
+
const config = useConfig();
|
|
1227
|
+
const citation = props["data-citation"];
|
|
1228
|
+
if (typeof citation === "string") {
|
|
1229
|
+
const index = Number(citation);
|
|
1230
|
+
if (Number.isInteger(index) && index > 0) {
|
|
1231
|
+
return /* @__PURE__ */ jsx11(Citation, { index, source: config.sources?.[index - 1], messages: config.override });
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
return /* @__PURE__ */ jsx11("span", { ...props, children });
|
|
1235
|
+
}
|
|
1236
|
+
function makeHeading(level) {
|
|
1237
|
+
function Heading(raw) {
|
|
1238
|
+
const { children, ...props } = withoutNode(raw);
|
|
1239
|
+
const config = useConfig();
|
|
1240
|
+
const tag = `h${Math.min(6, level + Math.max(0, config.headingOffset))}`;
|
|
1241
|
+
const isFootnoteLabel = props.id === "footnote-label";
|
|
1242
|
+
return createElement(
|
|
1243
|
+
tag,
|
|
1244
|
+
{
|
|
1245
|
+
...props,
|
|
1246
|
+
id: isFootnoteLabel ? `${config.idPrefix}footnote-label` : props.id,
|
|
1247
|
+
dir: "auto",
|
|
1248
|
+
"data-footnote-label": isFootnoteLabel ? "" : void 0
|
|
1249
|
+
},
|
|
1250
|
+
children
|
|
1251
|
+
);
|
|
1252
|
+
}
|
|
1253
|
+
Heading.displayName = `MarkdownHeading${level}`;
|
|
1254
|
+
return Heading;
|
|
1255
|
+
}
|
|
1256
|
+
function Paragraph(props) {
|
|
1257
|
+
return /* @__PURE__ */ jsx11("p", { ...withoutNode(props), dir: "auto" });
|
|
1258
|
+
}
|
|
1259
|
+
function ListItem(props) {
|
|
1260
|
+
return /* @__PURE__ */ jsx11("li", { ...withoutNode(props), dir: "auto" });
|
|
1261
|
+
}
|
|
1262
|
+
function UnorderedList(props) {
|
|
1263
|
+
return /* @__PURE__ */ jsx11("ul", { ...withoutNode(props), dir: "auto" });
|
|
1264
|
+
}
|
|
1265
|
+
function OrderedList(props) {
|
|
1266
|
+
return /* @__PURE__ */ jsx11("ol", { ...withoutNode(props), dir: "auto" });
|
|
1267
|
+
}
|
|
1268
|
+
function Blockquote(props) {
|
|
1269
|
+
return /* @__PURE__ */ jsx11("blockquote", { ...withoutNode(props), dir: "auto" });
|
|
1270
|
+
}
|
|
1271
|
+
function TableCell(props) {
|
|
1272
|
+
return /* @__PURE__ */ jsx11("td", { ...withoutNode(props), dir: "auto" });
|
|
1273
|
+
}
|
|
1274
|
+
function TableHeaderCell(props) {
|
|
1275
|
+
return /* @__PURE__ */ jsx11("th", { ...withoutNode(props), dir: "auto" });
|
|
1276
|
+
}
|
|
1277
|
+
var COMPONENTS = {
|
|
1278
|
+
a: Link,
|
|
1279
|
+
img: Image,
|
|
1280
|
+
pre: Pre,
|
|
1281
|
+
table: Table,
|
|
1282
|
+
input: Input,
|
|
1283
|
+
span: Span,
|
|
1284
|
+
h1: makeHeading(1),
|
|
1285
|
+
h2: makeHeading(2),
|
|
1286
|
+
h3: makeHeading(3),
|
|
1287
|
+
h4: makeHeading(4),
|
|
1288
|
+
h5: makeHeading(5),
|
|
1289
|
+
h6: makeHeading(6),
|
|
1290
|
+
p: Paragraph,
|
|
1291
|
+
li: ListItem,
|
|
1292
|
+
ul: UnorderedList,
|
|
1293
|
+
ol: OrderedList,
|
|
1294
|
+
blockquote: Blockquote,
|
|
1295
|
+
td: TableCell,
|
|
1296
|
+
th: TableHeaderCell
|
|
1297
|
+
};
|
|
1298
|
+
var MarkdownBlock = memo(function MarkdownBlock2({
|
|
1299
|
+
markdown,
|
|
1300
|
+
maxIndex,
|
|
1301
|
+
footnoteLabel,
|
|
1302
|
+
footnoteBackLabel,
|
|
1303
|
+
clobberPrefix
|
|
1304
|
+
}) {
|
|
1305
|
+
const remarkRehypeOptions = useMemo3(
|
|
1306
|
+
() => ({ footnoteLabel, footnoteBackLabel, clobberPrefix, footnoteLabelProperties: {} }),
|
|
1307
|
+
[footnoteLabel, footnoteBackLabel, clobberPrefix]
|
|
1308
|
+
);
|
|
1309
|
+
return /* @__PURE__ */ jsx11(
|
|
1310
|
+
ReactMarkdown,
|
|
1311
|
+
{
|
|
1312
|
+
skipHtml: true,
|
|
1313
|
+
remarkPlugins: pluginsFor(maxIndex),
|
|
1314
|
+
components: COMPONENTS,
|
|
1315
|
+
urlTransform,
|
|
1316
|
+
remarkRehypeOptions,
|
|
1317
|
+
children: markdown
|
|
1318
|
+
}
|
|
1319
|
+
);
|
|
1320
|
+
});
|
|
1321
|
+
function Markdown({
|
|
1322
|
+
children,
|
|
1323
|
+
streaming = false,
|
|
1324
|
+
headingOffset = 2,
|
|
1325
|
+
images = "link",
|
|
1326
|
+
allowDataImages = false,
|
|
1327
|
+
transformImageUrl,
|
|
1328
|
+
citations,
|
|
1329
|
+
sources,
|
|
1330
|
+
onLinkClick,
|
|
1331
|
+
onCopyCode,
|
|
1332
|
+
messages: override,
|
|
1333
|
+
className,
|
|
1334
|
+
ref,
|
|
1335
|
+
...props
|
|
1336
|
+
}) {
|
|
1337
|
+
const messages = useAiMessages(override);
|
|
1338
|
+
const sourcesContext = useSourcesContext();
|
|
1339
|
+
const instanceId = useId4();
|
|
1340
|
+
const sourceCount = (sources ?? sourcesContext?.sources)?.length ?? 0;
|
|
1341
|
+
const maxIndex = citations ?? sourceCount > 0 ? sourceCount : 0;
|
|
1342
|
+
const idPrefix = `${instanceId}-`;
|
|
1343
|
+
const config = useMemo3(
|
|
1344
|
+
() => ({
|
|
1345
|
+
messages,
|
|
1346
|
+
override,
|
|
1347
|
+
images,
|
|
1348
|
+
allowDataImages,
|
|
1349
|
+
transformImageUrl,
|
|
1350
|
+
headingOffset,
|
|
1351
|
+
idPrefix,
|
|
1352
|
+
onLinkClick,
|
|
1353
|
+
onCopyCode,
|
|
1354
|
+
sources
|
|
1355
|
+
}),
|
|
1356
|
+
[messages, override, images, allowDataImages, transformImageUrl, headingOffset, idPrefix, onLinkClick, onCopyCode, sources]
|
|
1357
|
+
);
|
|
1358
|
+
const blocks = useMemo3(() => splitMarkdownBlocks(children), [children]);
|
|
1359
|
+
let content = null;
|
|
1360
|
+
if (blocks.length > 0) {
|
|
1361
|
+
content = blocks.map((block, position) => /* @__PURE__ */ jsx11(
|
|
1362
|
+
MarkdownBlock,
|
|
1363
|
+
{
|
|
1364
|
+
markdown: block,
|
|
1365
|
+
maxIndex,
|
|
1366
|
+
footnoteLabel: messages.footnotes,
|
|
1367
|
+
footnoteBackLabel: messages.footnoteBack,
|
|
1368
|
+
clobberPrefix: idPrefix
|
|
1369
|
+
},
|
|
1370
|
+
position
|
|
1371
|
+
));
|
|
1372
|
+
}
|
|
1373
|
+
return /* @__PURE__ */ jsx11(
|
|
1374
|
+
"div",
|
|
1375
|
+
{
|
|
1376
|
+
...props,
|
|
1377
|
+
ref,
|
|
1378
|
+
"data-kit": "ai-markdown",
|
|
1379
|
+
"data-streaming": streaming ? "" : void 0,
|
|
1380
|
+
className: cx(className),
|
|
1381
|
+
children: /* @__PURE__ */ jsx11(ConfigContext.Provider, { value: config, children: content })
|
|
1382
|
+
}
|
|
1383
|
+
);
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
// src/react/reasoning.tsx
|
|
1387
|
+
import { Icon as Icon4 } from "@x12i/icons";
|
|
1388
|
+
import { Spinner } from "@x12i/ui";
|
|
1389
|
+
import { useEffect as useEffect4, useRef as useRef7 } from "react";
|
|
1390
|
+
import { jsx as jsx12, jsxs as jsxs11 } from "react/jsx-runtime";
|
|
1391
|
+
function Reasoning({
|
|
1392
|
+
text,
|
|
1393
|
+
streaming = false,
|
|
1394
|
+
durationMs,
|
|
1395
|
+
open: openProp,
|
|
1396
|
+
defaultOpen,
|
|
1397
|
+
onOpenChange,
|
|
1398
|
+
locale,
|
|
1399
|
+
messages: override,
|
|
1400
|
+
className,
|
|
1401
|
+
ref,
|
|
1402
|
+
...props
|
|
1403
|
+
}) {
|
|
1404
|
+
const messages = useAiMessages(override);
|
|
1405
|
+
const [open, setOpen] = useControllableState({
|
|
1406
|
+
value: openProp,
|
|
1407
|
+
defaultValue: defaultOpen ?? streaming,
|
|
1408
|
+
onChange: onOpenChange
|
|
1409
|
+
});
|
|
1410
|
+
const userChoice = useRef7(false);
|
|
1411
|
+
const wasStreaming = useRef7(streaming);
|
|
1412
|
+
useEffect4(() => {
|
|
1413
|
+
if (wasStreaming.current && !streaming && !userChoice.current) setOpen(false);
|
|
1414
|
+
if (!wasStreaming.current && streaming && !userChoice.current) setOpen(true);
|
|
1415
|
+
wasStreaming.current = streaming;
|
|
1416
|
+
}, [streaming, setOpen]);
|
|
1417
|
+
const disclosure = useDisclosure({
|
|
1418
|
+
open,
|
|
1419
|
+
onOpenChange: (next) => {
|
|
1420
|
+
userChoice.current = true;
|
|
1421
|
+
setOpen(next);
|
|
1422
|
+
}
|
|
1423
|
+
});
|
|
1424
|
+
const title = streaming ? messages.thinking : durationMs !== void 0 ? messages.thoughtFor(formatDuration(durationMs, locale)) : messages.thought;
|
|
1425
|
+
return /* @__PURE__ */ jsxs11(
|
|
1426
|
+
"div",
|
|
1427
|
+
{
|
|
1428
|
+
...props,
|
|
1429
|
+
ref,
|
|
1430
|
+
"data-kit": "ai-reasoning",
|
|
1431
|
+
"data-state": disclosure.open ? "open" : "closed",
|
|
1432
|
+
"data-streaming": streaming ? "" : void 0,
|
|
1433
|
+
className: cx(className),
|
|
1434
|
+
children: [
|
|
1435
|
+
/* @__PURE__ */ jsxs11("button", { ...disclosure.triggerProps, "data-kit": "ai-reasoning-trigger", children: [
|
|
1436
|
+
streaming ? /* @__PURE__ */ jsx12(Spinner, { size: "sm" }) : /* @__PURE__ */ jsx12(SparklesGlyph, { size: "sm" }),
|
|
1437
|
+
/* @__PURE__ */ jsx12("span", { "data-kit": "ai-reasoning-title", children: title }),
|
|
1438
|
+
/* @__PURE__ */ jsx12(Icon4, { name: disclosure.open ? "chevron-up" : "chevron-down", size: 16 })
|
|
1439
|
+
] }),
|
|
1440
|
+
/* @__PURE__ */ jsx12("div", { ...disclosure.panelProps, "data-kit": "ai-reasoning-panel", "aria-live": "off", children: disclosure.open ? /* @__PURE__ */ jsx12(Markdown, { streaming, headingOffset: 3, messages: override, images: "none", citations: false, children: text }) : null })
|
|
1441
|
+
]
|
|
1442
|
+
}
|
|
1443
|
+
);
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
// src/react/streaming-indicator.tsx
|
|
1447
|
+
import { VisuallyHidden as VisuallyHidden5 } from "@x12i/ui";
|
|
1448
|
+
import { jsx as jsx13, jsxs as jsxs12 } from "react/jsx-runtime";
|
|
1449
|
+
function StreamingIndicator({ label, messages: override, className, ref, ...props }) {
|
|
1450
|
+
const messages = useAiMessages(override);
|
|
1451
|
+
return /* @__PURE__ */ jsxs12("span", { ...props, ref, "data-kit": "ai-streaming-indicator", className: cx(className), children: [
|
|
1452
|
+
/* @__PURE__ */ jsx13("span", { "data-kit": "ai-streaming-dot", "aria-hidden": "true" }),
|
|
1453
|
+
/* @__PURE__ */ jsx13("span", { "data-kit": "ai-streaming-dot", "aria-hidden": "true" }),
|
|
1454
|
+
/* @__PURE__ */ jsx13("span", { "data-kit": "ai-streaming-dot", "aria-hidden": "true" }),
|
|
1455
|
+
/* @__PURE__ */ jsx13(VisuallyHidden5, { children: label ?? messages.thinkingIndicator })
|
|
1456
|
+
] });
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
// src/react/tool-call.tsx
|
|
1460
|
+
import { Icon as Icon5 } from "@x12i/icons";
|
|
1461
|
+
import { Badge as Badge2, Spinner as Spinner2, VisuallyHidden as VisuallyHidden6 } from "@x12i/ui";
|
|
1462
|
+
|
|
1463
|
+
// src/react/approval-prompt.tsx
|
|
1464
|
+
import { Button as Button4 } from "@x12i/ui";
|
|
1465
|
+
import {
|
|
1466
|
+
useId as useId5,
|
|
1467
|
+
useLayoutEffect as useLayoutEffect4,
|
|
1468
|
+
useRef as useRef8
|
|
1469
|
+
} from "react";
|
|
1470
|
+
import { jsx as jsx14, jsxs as jsxs13 } from "react/jsx-runtime";
|
|
1471
|
+
var THREAD_SELECTOR = '[data-kit="ai-chat-thread"]';
|
|
1472
|
+
var CONVERSATION_SELECTOR = '[data-kit="ai-conversation"]';
|
|
1473
|
+
function findScope(element) {
|
|
1474
|
+
return element.closest(THREAD_SELECTOR) ?? element.closest(CONVERSATION_SELECTOR);
|
|
1475
|
+
}
|
|
1476
|
+
function ApprovalPrompt({
|
|
1477
|
+
toolName,
|
|
1478
|
+
input,
|
|
1479
|
+
prompt,
|
|
1480
|
+
onApprove,
|
|
1481
|
+
onDeny,
|
|
1482
|
+
autoFocus = true,
|
|
1483
|
+
returnFocusTo,
|
|
1484
|
+
allowReason = true,
|
|
1485
|
+
renderInput,
|
|
1486
|
+
messages: override,
|
|
1487
|
+
className,
|
|
1488
|
+
ref,
|
|
1489
|
+
...props
|
|
1490
|
+
}) {
|
|
1491
|
+
const messages = useAiMessages(override);
|
|
1492
|
+
const rootRef = useRef8(null);
|
|
1493
|
+
const reasonRef = useRef8(null);
|
|
1494
|
+
const previousFocus = useRef8(null);
|
|
1495
|
+
const titleId = useId5();
|
|
1496
|
+
const descriptionId = useId5();
|
|
1497
|
+
const reasonId = useId5();
|
|
1498
|
+
useLayoutEffect4(() => {
|
|
1499
|
+
const root = rootRef.current;
|
|
1500
|
+
if (!root || !autoFocus) return;
|
|
1501
|
+
const active = document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
|
1502
|
+
const scope = findScope(root);
|
|
1503
|
+
const unrelated = active !== null && active !== document.body && scope !== null && !scope.contains(active);
|
|
1504
|
+
if (unrelated) return;
|
|
1505
|
+
previousFocus.current = active !== null && active !== document.body && !root.contains(active) ? active : null;
|
|
1506
|
+
root.focus();
|
|
1507
|
+
}, []);
|
|
1508
|
+
const restoreFocus = () => {
|
|
1509
|
+
const root = rootRef.current;
|
|
1510
|
+
const explicit = typeof returnFocusTo === "function" ? returnFocusTo() : returnFocusTo?.current;
|
|
1511
|
+
const fallback = root?.closest(THREAD_SELECTOR)?.querySelector('[data-kit="ai-prompt-textarea"]') ?? null;
|
|
1512
|
+
const target = [explicit, previousFocus.current, fallback].find(
|
|
1513
|
+
(element) => element instanceof HTMLElement && element.isConnected && !root?.contains(element)
|
|
1514
|
+
);
|
|
1515
|
+
target?.focus();
|
|
1516
|
+
};
|
|
1517
|
+
const decide = (decision) => {
|
|
1518
|
+
const reason = reasonRef.current?.value.trim();
|
|
1519
|
+
const value = reason === "" ? void 0 : reason;
|
|
1520
|
+
if (decision === "approve") onApprove(value);
|
|
1521
|
+
else onDeny(value);
|
|
1522
|
+
restoreFocus();
|
|
1523
|
+
};
|
|
1524
|
+
return /* @__PURE__ */ jsxs13(
|
|
1525
|
+
"div",
|
|
1526
|
+
{
|
|
1527
|
+
...props,
|
|
1528
|
+
ref: (node) => {
|
|
1529
|
+
rootRef.current = node;
|
|
1530
|
+
if (typeof ref === "function") ref(node);
|
|
1531
|
+
else if (ref) ref.current = node;
|
|
1532
|
+
},
|
|
1533
|
+
role: "group",
|
|
1534
|
+
tabIndex: -1,
|
|
1535
|
+
"aria-labelledby": titleId,
|
|
1536
|
+
"aria-describedby": descriptionId,
|
|
1537
|
+
"data-kit": "ai-approval-prompt",
|
|
1538
|
+
className: cx(className),
|
|
1539
|
+
children: [
|
|
1540
|
+
/* @__PURE__ */ jsx14("p", { "data-kit": "ai-approval-title", id: titleId, dir: "auto", children: messages.approvalTitle(toolName) }),
|
|
1541
|
+
/* @__PURE__ */ jsx14("p", { "data-kit": "ai-approval-description", id: descriptionId, dir: "auto", children: prompt ?? messages.approvalDescription }),
|
|
1542
|
+
/* @__PURE__ */ jsx14("div", { "data-kit": "ai-approval-action", children: renderInput ? renderInput(input) : input === void 0 ? /* @__PURE__ */ jsx14("p", { "data-kit": "ai-approval-no-input", children: messages.noInput }) : /* @__PURE__ */ jsx14(JsonView, { value: input, label: messages.approvalAction, messages: override }) }),
|
|
1543
|
+
allowReason ? /* @__PURE__ */ jsxs13("div", { "data-kit": "ai-approval-reason", children: [
|
|
1544
|
+
/* @__PURE__ */ jsx14("label", { htmlFor: reasonId, children: messages.reasonLabel }),
|
|
1545
|
+
/* @__PURE__ */ jsx14(
|
|
1546
|
+
"input",
|
|
1547
|
+
{
|
|
1548
|
+
ref: reasonRef,
|
|
1549
|
+
id: reasonId,
|
|
1550
|
+
type: "text",
|
|
1551
|
+
dir: "auto",
|
|
1552
|
+
autoComplete: "off",
|
|
1553
|
+
placeholder: messages.reasonPlaceholder
|
|
1554
|
+
}
|
|
1555
|
+
)
|
|
1556
|
+
] }) : null,
|
|
1557
|
+
/* @__PURE__ */ jsxs13("div", { "data-kit": "ai-approval-buttons", children: [
|
|
1558
|
+
/* @__PURE__ */ jsx14(Button4, { variant: "solid", tone: "accent", size: "sm", "aria-label": messages.approveLabel(toolName), onClick: () => decide("approve"), children: messages.approve }),
|
|
1559
|
+
/* @__PURE__ */ jsx14(Button4, { variant: "outline", tone: "neutral", size: "sm", "aria-label": messages.denyLabel(toolName), onClick: () => decide("deny"), children: messages.deny })
|
|
1560
|
+
] })
|
|
1561
|
+
]
|
|
1562
|
+
}
|
|
1563
|
+
);
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
// src/react/tool-call.tsx
|
|
1567
|
+
import { Fragment as Fragment2, jsx as jsx15, jsxs as jsxs14 } from "react/jsx-runtime";
|
|
1568
|
+
var STATE_TONE = {
|
|
1569
|
+
queued: "neutral",
|
|
1570
|
+
running: "info",
|
|
1571
|
+
"needs-approval": "warning",
|
|
1572
|
+
approved: "accent",
|
|
1573
|
+
denied: "danger",
|
|
1574
|
+
succeeded: "success",
|
|
1575
|
+
failed: "danger"
|
|
1576
|
+
};
|
|
1577
|
+
function ToolCall({
|
|
1578
|
+
part,
|
|
1579
|
+
open,
|
|
1580
|
+
defaultOpen = false,
|
|
1581
|
+
onOpenChange,
|
|
1582
|
+
onDecision,
|
|
1583
|
+
renderInput,
|
|
1584
|
+
renderOutput,
|
|
1585
|
+
locale,
|
|
1586
|
+
messages: override,
|
|
1587
|
+
className,
|
|
1588
|
+
ref,
|
|
1589
|
+
...props
|
|
1590
|
+
}) {
|
|
1591
|
+
const messages = useAiMessages(override);
|
|
1592
|
+
const disclosure = useDisclosure({ open, defaultOpen, onOpenChange });
|
|
1593
|
+
const aborted = part.state === "failed" && part.errorCode === "aborted";
|
|
1594
|
+
const hasInput = part.input !== void 0;
|
|
1595
|
+
const hasOutput = part.output !== void 0;
|
|
1596
|
+
const failedMessage = part.state === "failed" ? aborted ? messages.toolAborted : part.error : void 0;
|
|
1597
|
+
const live = part.state === "running" || part.state === "queued";
|
|
1598
|
+
const needsDecision = part.state === "needs-approval" && onDecision !== void 0;
|
|
1599
|
+
return /* @__PURE__ */ jsxs14(
|
|
1600
|
+
"div",
|
|
1601
|
+
{
|
|
1602
|
+
...props,
|
|
1603
|
+
ref,
|
|
1604
|
+
role: "group",
|
|
1605
|
+
"aria-label": messages.toolCall(part.name),
|
|
1606
|
+
"data-kit": "ai-tool-call",
|
|
1607
|
+
"data-state": part.state,
|
|
1608
|
+
"data-open": disclosure.open ? "" : void 0,
|
|
1609
|
+
className: cx(className),
|
|
1610
|
+
children: [
|
|
1611
|
+
/* @__PURE__ */ jsxs14("button", { ...disclosure.triggerProps, "data-kit": "ai-tool-call-trigger", children: [
|
|
1612
|
+
/* @__PURE__ */ jsx15(WrenchGlyph, { size: "sm" }),
|
|
1613
|
+
/* @__PURE__ */ jsx15("span", { "data-kit": "ai-tool-call-name", dir: "ltr", children: part.name }),
|
|
1614
|
+
/* @__PURE__ */ jsxs14(Badge2, { tone: STATE_TONE[part.state], size: "sm", children: [
|
|
1615
|
+
live ? /* @__PURE__ */ jsx15(Spinner2, { size: "xs" }) : null,
|
|
1616
|
+
messages.toolStates[part.state]
|
|
1617
|
+
] }),
|
|
1618
|
+
part.durationMs !== void 0 ? /* @__PURE__ */ jsx15("span", { "data-kit": "ai-tool-call-duration", children: formatDuration(part.durationMs, locale) }) : null,
|
|
1619
|
+
/* @__PURE__ */ jsx15(Icon5, { name: disclosure.open ? "chevron-up" : "chevron-down", size: 16 })
|
|
1620
|
+
] }),
|
|
1621
|
+
failedMessage ? /* @__PURE__ */ jsxs14("p", { "data-kit": "ai-tool-call-error", "data-aborted": aborted ? "" : void 0, dir: "auto", children: [
|
|
1622
|
+
/* @__PURE__ */ jsxs14(VisuallyHidden6, { children: [
|
|
1623
|
+
messages.toolError,
|
|
1624
|
+
": "
|
|
1625
|
+
] }),
|
|
1626
|
+
failedMessage
|
|
1627
|
+
] }) : null,
|
|
1628
|
+
needsDecision ? /* @__PURE__ */ jsx15(
|
|
1629
|
+
ApprovalPrompt,
|
|
1630
|
+
{
|
|
1631
|
+
toolName: part.name,
|
|
1632
|
+
input: part.input,
|
|
1633
|
+
prompt: part.approval?.prompt,
|
|
1634
|
+
renderInput: renderInput ? (input) => renderInput(input, part) : void 0,
|
|
1635
|
+
messages: override,
|
|
1636
|
+
onApprove: (reason) => onDecision?.(part.id, "approve", reason),
|
|
1637
|
+
onDeny: (reason) => onDecision?.(part.id, "deny", reason)
|
|
1638
|
+
}
|
|
1639
|
+
) : null,
|
|
1640
|
+
part.state === "denied" && part.approval?.reason ? /* @__PURE__ */ jsx15("p", { "data-kit": "ai-tool-call-note", dir: "auto", children: part.approval.reason }) : null,
|
|
1641
|
+
/* @__PURE__ */ jsx15("div", { ...disclosure.panelProps, "data-kit": "ai-tool-call-panel", children: disclosure.open ? /* @__PURE__ */ jsxs14(Fragment2, { children: [
|
|
1642
|
+
hasInput ? renderInput ? renderInput(part.input, part) : /* @__PURE__ */ jsx15(JsonView, { value: part.input, label: messages.toolInput, messages: override }) : /* @__PURE__ */ jsx15("p", { "data-kit": "ai-tool-call-empty", children: messages.noInput }),
|
|
1643
|
+
hasOutput ? renderOutput ? renderOutput(part.output, part) : /* @__PURE__ */ jsx15(JsonView, { value: part.output, label: messages.toolOutput, messages: override }) : part.state === "succeeded" ? /* @__PURE__ */ jsx15("p", { "data-kit": "ai-tool-call-empty", children: messages.noOutput }) : null
|
|
1644
|
+
] }) : null })
|
|
1645
|
+
]
|
|
1646
|
+
}
|
|
1647
|
+
);
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
// src/react/message.tsx
|
|
1651
|
+
import { jsx as jsx16, jsxs as jsxs15 } from "react/jsx-runtime";
|
|
1652
|
+
function defaultTimestamp(iso, locale) {
|
|
1653
|
+
const date = new Date(iso);
|
|
1654
|
+
if (Number.isNaN(date.getTime())) return iso;
|
|
1655
|
+
return new Intl.DateTimeFormat(locale, { timeStyle: "short" }).format(date);
|
|
1656
|
+
}
|
|
1657
|
+
function MessageEditor({ initialText, onSubmit, onCancel, messages: override }) {
|
|
1658
|
+
const messages = useAiMessages(override);
|
|
1659
|
+
const [text, setText] = useState5(initialText);
|
|
1660
|
+
const textareaRef = useRef9(null);
|
|
1661
|
+
useAutosize(textareaRef, text);
|
|
1662
|
+
useEffect5(() => {
|
|
1663
|
+
const previous = document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
|
1664
|
+
const textarea = textareaRef.current;
|
|
1665
|
+
textarea?.focus();
|
|
1666
|
+
textarea?.setSelectionRange(textarea.value.length, textarea.value.length);
|
|
1667
|
+
return () => {
|
|
1668
|
+
if (previous?.isConnected) previous.focus();
|
|
1669
|
+
};
|
|
1670
|
+
}, []);
|
|
1671
|
+
const save = () => {
|
|
1672
|
+
const next = text.trim();
|
|
1673
|
+
if (next !== "") onSubmit(next);
|
|
1674
|
+
};
|
|
1675
|
+
const handleKeyDown = (event) => {
|
|
1676
|
+
if (event.nativeEvent.isComposing || event.keyCode === 229) return;
|
|
1677
|
+
if (event.key === "Escape") {
|
|
1678
|
+
event.preventDefault();
|
|
1679
|
+
onCancel();
|
|
1680
|
+
} else if (event.key === "Enter" && (event.ctrlKey || event.metaKey)) {
|
|
1681
|
+
event.preventDefault();
|
|
1682
|
+
save();
|
|
1683
|
+
}
|
|
1684
|
+
};
|
|
1685
|
+
return /* @__PURE__ */ jsxs15("div", { "data-kit": "ai-message-editor", children: [
|
|
1686
|
+
/* @__PURE__ */ jsx16(
|
|
1687
|
+
"textarea",
|
|
1688
|
+
{
|
|
1689
|
+
ref: textareaRef,
|
|
1690
|
+
dir: "auto",
|
|
1691
|
+
rows: 1,
|
|
1692
|
+
value: text,
|
|
1693
|
+
"aria-label": messages.editLabel,
|
|
1694
|
+
onChange: (event) => setText(event.target.value),
|
|
1695
|
+
onKeyDown: handleKeyDown
|
|
1696
|
+
}
|
|
1697
|
+
),
|
|
1698
|
+
/* @__PURE__ */ jsxs15("div", { "data-kit": "ai-message-editor-actions", children: [
|
|
1699
|
+
/* @__PURE__ */ jsx16(Button5, { variant: "solid", tone: "accent", size: "sm", disabled: text.trim() === "", onClick: save, children: messages.saveEdit }),
|
|
1700
|
+
/* @__PURE__ */ jsx16(Button5, { variant: "ghost", tone: "neutral", size: "sm", onClick: onCancel, children: messages.cancelEdit })
|
|
1701
|
+
] })
|
|
1702
|
+
] });
|
|
1703
|
+
}
|
|
1704
|
+
function Message({
|
|
1705
|
+
message,
|
|
1706
|
+
streaming: streamingProp,
|
|
1707
|
+
avatar,
|
|
1708
|
+
name,
|
|
1709
|
+
showTimestamp = false,
|
|
1710
|
+
formatTimestamp = defaultTimestamp,
|
|
1711
|
+
showUsage = false,
|
|
1712
|
+
locale,
|
|
1713
|
+
renderPart,
|
|
1714
|
+
onToolDecision,
|
|
1715
|
+
onRetry,
|
|
1716
|
+
editing = false,
|
|
1717
|
+
onEditSubmit,
|
|
1718
|
+
onEditCancel,
|
|
1719
|
+
actions,
|
|
1720
|
+
images,
|
|
1721
|
+
messages: override,
|
|
1722
|
+
className,
|
|
1723
|
+
ref,
|
|
1724
|
+
...props
|
|
1725
|
+
}) {
|
|
1726
|
+
const messages = useAiMessages(override);
|
|
1727
|
+
const streaming = streamingProp ?? message.status === "streaming";
|
|
1728
|
+
const role = message.role;
|
|
1729
|
+
const assistant = role === "assistant";
|
|
1730
|
+
const author = name ?? messages.roles[role];
|
|
1731
|
+
const sourceParts = useMemo4(
|
|
1732
|
+
() => message.parts.filter((part) => part.type === "source"),
|
|
1733
|
+
[message.parts]
|
|
1734
|
+
);
|
|
1735
|
+
const edited = message.metadata?.["edited"] === true;
|
|
1736
|
+
const tokens = showUsage && message.usage ? totalTokens(message.usage) : void 0;
|
|
1737
|
+
const context = (index) => ({ message, index, streaming });
|
|
1738
|
+
const renderTextPart = (part, index) => {
|
|
1739
|
+
const isLast = index === message.parts.length - 1;
|
|
1740
|
+
const view = () => assistant ? /* @__PURE__ */ jsx16(Markdown, { streaming: streaming && isLast, images, messages: override, children: part.text }) : /* @__PURE__ */ jsx16("p", { "data-kit": "ai-message-text", dir: "auto", children: part.text });
|
|
1741
|
+
const custom = renderPart?.text?.(part, { ...context(index), defaultRender: view });
|
|
1742
|
+
return custom === void 0 ? view() : custom;
|
|
1743
|
+
};
|
|
1744
|
+
const renderReasoningPart = (part, index) => {
|
|
1745
|
+
const isLast = index === message.parts.length - 1;
|
|
1746
|
+
const view = () => /* @__PURE__ */ jsx16(
|
|
1747
|
+
Reasoning,
|
|
1748
|
+
{
|
|
1749
|
+
text: part.text,
|
|
1750
|
+
durationMs: part.durationMs,
|
|
1751
|
+
streaming: streaming && isLast,
|
|
1752
|
+
locale,
|
|
1753
|
+
messages: override
|
|
1754
|
+
}
|
|
1755
|
+
);
|
|
1756
|
+
const custom = renderPart?.reasoning?.(part, { ...context(index), defaultRender: view });
|
|
1757
|
+
return custom === void 0 ? view() : custom;
|
|
1758
|
+
};
|
|
1759
|
+
const renderToolPart = (part, index) => {
|
|
1760
|
+
const view = () => /* @__PURE__ */ jsx16(ToolCall, { part, onDecision: onToolDecision, locale, messages: override });
|
|
1761
|
+
const custom = renderPart?.tool?.(part, { ...context(index), defaultRender: view });
|
|
1762
|
+
return custom === void 0 ? view() : custom;
|
|
1763
|
+
};
|
|
1764
|
+
const renderDataPart = (part, index) => {
|
|
1765
|
+
const view = () => /* @__PURE__ */ jsx16(JsonView, { value: part.data, label: part.kind, messages: override });
|
|
1766
|
+
const renderer = renderPart?.[`data:${part.kind}`] ?? renderPart?.data;
|
|
1767
|
+
const custom = renderer?.(part, { ...context(index), defaultRender: view });
|
|
1768
|
+
return custom === void 0 ? view() : custom;
|
|
1769
|
+
};
|
|
1770
|
+
const renderFiles = (files, index) => {
|
|
1771
|
+
const view = () => /* @__PURE__ */ jsx16(Attachments, { files, locale, messages: override });
|
|
1772
|
+
const custom = renderPart?.files?.(files, { ...context(index), defaultRender: view });
|
|
1773
|
+
return custom === void 0 ? view() : custom;
|
|
1774
|
+
};
|
|
1775
|
+
const renderSources = () => {
|
|
1776
|
+
if (sourceParts.length === 0) return null;
|
|
1777
|
+
const index = message.parts.findIndex((part) => part.type === "source");
|
|
1778
|
+
const view = () => /* @__PURE__ */ jsx16(Sources, { messages: override });
|
|
1779
|
+
const custom = renderPart?.sources?.(sourceParts, { ...context(index), defaultRender: view });
|
|
1780
|
+
return custom === void 0 ? view() : custom;
|
|
1781
|
+
};
|
|
1782
|
+
const renderParts = () => {
|
|
1783
|
+
const nodes = [];
|
|
1784
|
+
const files = message.parts.filter((part) => part.type === "file");
|
|
1785
|
+
let filesDone = false;
|
|
1786
|
+
message.parts.forEach((part, index) => {
|
|
1787
|
+
switch (part.type) {
|
|
1788
|
+
case "text": {
|
|
1789
|
+
if (part.text === "") return;
|
|
1790
|
+
nodes.push(/* @__PURE__ */ jsx16("div", { "data-kit": "ai-message-part", "data-part": "text", children: renderTextPart(part, index) }, `text-${index}`));
|
|
1791
|
+
return;
|
|
1792
|
+
}
|
|
1793
|
+
case "reasoning":
|
|
1794
|
+
nodes.push(/* @__PURE__ */ jsx16("div", { "data-kit": "ai-message-part", "data-part": "reasoning", children: renderReasoningPart(part, index) }, `reasoning-${index}`));
|
|
1795
|
+
return;
|
|
1796
|
+
case "tool":
|
|
1797
|
+
nodes.push(/* @__PURE__ */ jsx16("div", { "data-kit": "ai-message-part", "data-part": "tool", children: renderToolPart(part, index) }, `tool-${part.id}`));
|
|
1798
|
+
return;
|
|
1799
|
+
case "data":
|
|
1800
|
+
nodes.push(/* @__PURE__ */ jsx16("div", { "data-kit": "ai-message-part", "data-part": "data", children: renderDataPart(part, index) }, `data-${index}`));
|
|
1801
|
+
return;
|
|
1802
|
+
case "file":
|
|
1803
|
+
if (!filesDone) {
|
|
1804
|
+
filesDone = true;
|
|
1805
|
+
nodes.push(/* @__PURE__ */ jsx16("div", { "data-kit": "ai-message-part", "data-part": "files", children: renderFiles(files, index) }, `files-${index}`));
|
|
1806
|
+
}
|
|
1807
|
+
return;
|
|
1808
|
+
case "source":
|
|
1809
|
+
return;
|
|
1810
|
+
}
|
|
1811
|
+
});
|
|
1812
|
+
return nodes;
|
|
1813
|
+
};
|
|
1814
|
+
const editingUserText = editing && role === "user";
|
|
1815
|
+
const hasVisibleParts = message.parts.some((part) => part.type !== "text" || part.text !== "");
|
|
1816
|
+
const timestamp = showTimestamp && message.createdAt ? message.createdAt : null;
|
|
1817
|
+
return /* @__PURE__ */ jsxs15(
|
|
1818
|
+
"article",
|
|
1819
|
+
{
|
|
1820
|
+
...props,
|
|
1821
|
+
ref,
|
|
1822
|
+
tabIndex: props.tabIndex ?? -1,
|
|
1823
|
+
"aria-label": messages.messageFrom(author),
|
|
1824
|
+
"aria-busy": streaming ? true : void 0,
|
|
1825
|
+
"data-kit": "ai-message",
|
|
1826
|
+
"data-role": role,
|
|
1827
|
+
"data-status": message.status,
|
|
1828
|
+
"data-streaming": streaming ? "" : void 0,
|
|
1829
|
+
className: cx(className),
|
|
1830
|
+
children: [
|
|
1831
|
+
avatar ? /* @__PURE__ */ jsx16("div", { "data-kit": "ai-message-avatar", children: avatar }) : null,
|
|
1832
|
+
/* @__PURE__ */ jsxs15("div", { "data-kit": "ai-message-main", children: [
|
|
1833
|
+
/* @__PURE__ */ jsxs15("div", { "data-kit": "ai-message-header", children: [
|
|
1834
|
+
/* @__PURE__ */ jsx16("span", { "data-kit": "ai-message-author", dir: "auto", children: author }),
|
|
1835
|
+
assistant ? /* @__PURE__ */ jsx16("span", { "data-kit": "ai-message-label", children: /* @__PURE__ */ jsxs15(Badge3, { size: "sm", variant: "outline", tone: "neutral", children: [
|
|
1836
|
+
/* @__PURE__ */ jsx16("span", { "aria-hidden": "true", children: messages.aiGenerated }),
|
|
1837
|
+
/* @__PURE__ */ jsx16(VisuallyHidden7, { children: messages.aiGeneratedLabel })
|
|
1838
|
+
] }) }) : null,
|
|
1839
|
+
timestamp ? /* @__PURE__ */ jsx16("time", { "data-kit": "ai-message-time", dateTime: timestamp, suppressHydrationWarning: true, children: formatTimestamp(timestamp, locale) }) : null,
|
|
1840
|
+
edited ? /* @__PURE__ */ jsx16("span", { "data-kit": "ai-message-edited", children: messages.edited }) : null
|
|
1841
|
+
] }),
|
|
1842
|
+
/* @__PURE__ */ jsxs15(SourcesProvider, { sources: sourceParts, children: [
|
|
1843
|
+
/* @__PURE__ */ jsxs15("div", { "data-kit": "ai-message-content", "aria-live": streaming ? "off" : void 0, children: [
|
|
1844
|
+
editingUserText ? /* @__PURE__ */ jsx16(
|
|
1845
|
+
MessageEditor,
|
|
1846
|
+
{
|
|
1847
|
+
initialText: messageText(message),
|
|
1848
|
+
onSubmit: (text) => onEditSubmit?.(text),
|
|
1849
|
+
onCancel: () => onEditCancel?.(),
|
|
1850
|
+
messages: override
|
|
1851
|
+
}
|
|
1852
|
+
) : renderParts(),
|
|
1853
|
+
streaming && !hasVisibleParts ? /* @__PURE__ */ jsx16(StreamingIndicator, { messages: override }) : null
|
|
1854
|
+
] }),
|
|
1855
|
+
renderSources()
|
|
1856
|
+
] }),
|
|
1857
|
+
message.status === "stopped" ? /* @__PURE__ */ jsx16("p", { "data-kit": "ai-message-status", "data-status": "stopped", children: messages.stopped }) : null,
|
|
1858
|
+
message.status === "error" && message.error ? /* @__PURE__ */ jsx16(ErrorBanner, { error: message.error, onRetry, messages: override }) : null,
|
|
1859
|
+
tokens !== void 0 && !streaming ? /* @__PURE__ */ jsx16("p", { "data-kit": "ai-message-usage", children: messages.tokens(formatTokenCount(tokens, locale)) }) : null,
|
|
1860
|
+
actions
|
|
1861
|
+
] })
|
|
1862
|
+
]
|
|
1863
|
+
}
|
|
1864
|
+
);
|
|
1865
|
+
}
|
|
1866
|
+
var ChatMessage = Message;
|
|
1867
|
+
|
|
1868
|
+
// src/react/message-actions.tsx
|
|
1869
|
+
import { Icon as Icon6 } from "@x12i/icons";
|
|
1870
|
+
import { IconButton as IconButton3, Inline, VisuallyHidden as VisuallyHidden8 } from "@x12i/ui";
|
|
1871
|
+
import { Fragment as Fragment3, jsx as jsx17, jsxs as jsxs16 } from "react/jsx-runtime";
|
|
1872
|
+
function MessageActions({
|
|
1873
|
+
text,
|
|
1874
|
+
onCopied,
|
|
1875
|
+
onRegenerate,
|
|
1876
|
+
onEdit,
|
|
1877
|
+
feedback = null,
|
|
1878
|
+
onFeedback,
|
|
1879
|
+
branch,
|
|
1880
|
+
disabled = false,
|
|
1881
|
+
children,
|
|
1882
|
+
messages: override,
|
|
1883
|
+
className,
|
|
1884
|
+
ref,
|
|
1885
|
+
...props
|
|
1886
|
+
}) {
|
|
1887
|
+
const messages = useAiMessages(override);
|
|
1888
|
+
return /* @__PURE__ */ jsx17(
|
|
1889
|
+
"div",
|
|
1890
|
+
{
|
|
1891
|
+
...props,
|
|
1892
|
+
ref,
|
|
1893
|
+
role: "group",
|
|
1894
|
+
"aria-label": messages.actions,
|
|
1895
|
+
"data-kit": "ai-message-actions",
|
|
1896
|
+
className: cx(className),
|
|
1897
|
+
children: /* @__PURE__ */ jsxs16(Inline, { gap: "1", align: "center", children: [
|
|
1898
|
+
branch && branch.count > 1 ? /* @__PURE__ */ jsxs16("span", { "data-kit": "ai-branch-nav", children: [
|
|
1899
|
+
/* @__PURE__ */ jsx17(
|
|
1900
|
+
IconButton3,
|
|
1901
|
+
{
|
|
1902
|
+
variant: "ghost",
|
|
1903
|
+
tone: "neutral",
|
|
1904
|
+
size: "sm",
|
|
1905
|
+
label: messages.previousVersion,
|
|
1906
|
+
disabled: disabled || branch.index <= 0,
|
|
1907
|
+
onClick: () => branch.onSelect(branch.index - 1),
|
|
1908
|
+
children: /* @__PURE__ */ jsx17(BranchPrevGlyph, { size: "sm", "data-flip": "" })
|
|
1909
|
+
}
|
|
1910
|
+
),
|
|
1911
|
+
/* @__PURE__ */ jsxs16("span", { "data-kit": "ai-branch-position", dir: "ltr", children: [
|
|
1912
|
+
/* @__PURE__ */ jsx17(VisuallyHidden8, { children: messages.versionLabel(branch.index + 1, branch.count) }),
|
|
1913
|
+
/* @__PURE__ */ jsxs16("span", { "aria-hidden": "true", children: [
|
|
1914
|
+
branch.index + 1,
|
|
1915
|
+
"/",
|
|
1916
|
+
branch.count
|
|
1917
|
+
] })
|
|
1918
|
+
] }),
|
|
1919
|
+
/* @__PURE__ */ jsx17(
|
|
1920
|
+
IconButton3,
|
|
1921
|
+
{
|
|
1922
|
+
variant: "ghost",
|
|
1923
|
+
tone: "neutral",
|
|
1924
|
+
size: "sm",
|
|
1925
|
+
label: messages.nextVersion,
|
|
1926
|
+
disabled: disabled || branch.index >= branch.count - 1,
|
|
1927
|
+
onClick: () => branch.onSelect(branch.index + 1),
|
|
1928
|
+
children: /* @__PURE__ */ jsx17(BranchNextGlyph, { size: "sm", "data-flip": "" })
|
|
1929
|
+
}
|
|
1930
|
+
)
|
|
1931
|
+
] }) : null,
|
|
1932
|
+
text !== void 0 ? /* @__PURE__ */ jsx17(
|
|
1933
|
+
CopyButton,
|
|
1934
|
+
{
|
|
1935
|
+
text,
|
|
1936
|
+
label: messages.copy,
|
|
1937
|
+
copiedLabel: messages.copied,
|
|
1938
|
+
failedLabel: messages.copyFailed,
|
|
1939
|
+
onCopied
|
|
1940
|
+
}
|
|
1941
|
+
) : null,
|
|
1942
|
+
onEdit ? /* @__PURE__ */ jsx17(IconButton3, { variant: "ghost", tone: "neutral", size: "sm", label: messages.edit, disabled, onClick: onEdit, children: /* @__PURE__ */ jsx17(Icon6, { name: "edit", size: 16 }) }) : null,
|
|
1943
|
+
onRegenerate ? /* @__PURE__ */ jsx17(IconButton3, { variant: "ghost", tone: "neutral", size: "sm", label: messages.regenerate, disabled, onClick: onRegenerate, children: /* @__PURE__ */ jsx17(RefreshGlyph, { size: "sm" }) }) : null,
|
|
1944
|
+
onFeedback ? /* @__PURE__ */ jsxs16(Fragment3, { children: [
|
|
1945
|
+
/* @__PURE__ */ jsx17(
|
|
1946
|
+
IconButton3,
|
|
1947
|
+
{
|
|
1948
|
+
variant: "ghost",
|
|
1949
|
+
tone: "neutral",
|
|
1950
|
+
size: "sm",
|
|
1951
|
+
label: messages.goodResponse,
|
|
1952
|
+
"aria-pressed": feedback === "up",
|
|
1953
|
+
onClick: () => onFeedback(feedback === "up" ? null : "up"),
|
|
1954
|
+
children: /* @__PURE__ */ jsx17(ThumbsUpGlyph, { size: "sm", filled: feedback === "up" })
|
|
1955
|
+
}
|
|
1956
|
+
),
|
|
1957
|
+
/* @__PURE__ */ jsx17(
|
|
1958
|
+
IconButton3,
|
|
1959
|
+
{
|
|
1960
|
+
variant: "ghost",
|
|
1961
|
+
tone: "neutral",
|
|
1962
|
+
size: "sm",
|
|
1963
|
+
label: messages.badResponse,
|
|
1964
|
+
"aria-pressed": feedback === "down",
|
|
1965
|
+
onClick: () => onFeedback(feedback === "down" ? null : "down"),
|
|
1966
|
+
children: /* @__PURE__ */ jsx17(ThumbsDownGlyph, { size: "sm", filled: feedback === "down" })
|
|
1967
|
+
}
|
|
1968
|
+
)
|
|
1969
|
+
] }) : null,
|
|
1970
|
+
children
|
|
1971
|
+
] })
|
|
1972
|
+
}
|
|
1973
|
+
);
|
|
1974
|
+
}
|
|
1975
|
+
|
|
1976
|
+
// src/react/prompt-input.tsx
|
|
1977
|
+
import { Icon as Icon7 } from "@x12i/icons";
|
|
1978
|
+
import { IconButton as IconButton4, Spinner as Spinner3, VisuallyHidden as VisuallyHidden9 } from "@x12i/ui";
|
|
1979
|
+
import {
|
|
1980
|
+
useCallback as useCallback5,
|
|
1981
|
+
useId as useId6,
|
|
1982
|
+
useRef as useRef10,
|
|
1983
|
+
useState as useState6
|
|
1984
|
+
} from "react";
|
|
1985
|
+
import { Fragment as Fragment4, jsx as jsx18, jsxs as jsxs17 } from "react/jsx-runtime";
|
|
1986
|
+
var attachmentCounter = 0;
|
|
1987
|
+
function matchCommands(commands, query) {
|
|
1988
|
+
const needle = query.toLowerCase();
|
|
1989
|
+
const starts = [];
|
|
1990
|
+
const contains = [];
|
|
1991
|
+
for (const command of commands) {
|
|
1992
|
+
const name = command.name.toLowerCase();
|
|
1993
|
+
if (name.startsWith(needle)) starts.push(command);
|
|
1994
|
+
else if (needle !== "" && (name.includes(needle) || (command.description ?? "").toLowerCase().includes(needle))) contains.push(command);
|
|
1995
|
+
}
|
|
1996
|
+
return [...starts, ...contains];
|
|
1997
|
+
}
|
|
1998
|
+
function PromptInput({
|
|
1999
|
+
onSubmit,
|
|
2000
|
+
value: valueProp,
|
|
2001
|
+
defaultValue = "",
|
|
2002
|
+
onValueChange,
|
|
2003
|
+
status = "idle",
|
|
2004
|
+
onStop,
|
|
2005
|
+
disabled = false,
|
|
2006
|
+
placeholder,
|
|
2007
|
+
maxLength,
|
|
2008
|
+
minRows = 1,
|
|
2009
|
+
maxRows = 8,
|
|
2010
|
+
submitOnEnter = true,
|
|
2011
|
+
stopOnEscape = true,
|
|
2012
|
+
autoFocus,
|
|
2013
|
+
attachments: attachmentsProp,
|
|
2014
|
+
onRemoveAttachment,
|
|
2015
|
+
onFiles,
|
|
2016
|
+
allowFiles,
|
|
2017
|
+
accept,
|
|
2018
|
+
maxFiles,
|
|
2019
|
+
slashCommands,
|
|
2020
|
+
onCommandSelect,
|
|
2021
|
+
footer,
|
|
2022
|
+
textareaRef: textareaRefProp,
|
|
2023
|
+
locale,
|
|
2024
|
+
messages: override,
|
|
2025
|
+
className,
|
|
2026
|
+
style,
|
|
2027
|
+
ref,
|
|
2028
|
+
...props
|
|
2029
|
+
}) {
|
|
2030
|
+
const messages = useAiMessages(override);
|
|
2031
|
+
const [value, setValue] = useControllableState({ value: valueProp, defaultValue, onChange: onValueChange });
|
|
2032
|
+
const [ownAttachments, setOwnAttachments] = useState6([]);
|
|
2033
|
+
const attachments = attachmentsProp ?? ownAttachments;
|
|
2034
|
+
const [dragging, setDragging] = useState6(false);
|
|
2035
|
+
const [commandIndex, setCommandIndex] = useState6(0);
|
|
2036
|
+
const [dismissedFor, setDismissedFor] = useState6(null);
|
|
2037
|
+
const textareaRef = useRef10(null);
|
|
2038
|
+
const fileInputRef = useRef10(null);
|
|
2039
|
+
const composing = useRef10(false);
|
|
2040
|
+
const dragDepth = useRef10(0);
|
|
2041
|
+
const counterId = useId6();
|
|
2042
|
+
const listId = useId6();
|
|
2043
|
+
const optionIdBase = useId6();
|
|
2044
|
+
const busy = status === "submitted" || status === "streaming";
|
|
2045
|
+
const filesEnabled = allowFiles ?? (onFiles !== void 0 || attachmentsProp !== void 0);
|
|
2046
|
+
const filesFull = maxFiles !== void 0 && attachments.length >= maxFiles;
|
|
2047
|
+
const uploading = attachments.some((attachment) => attachment.status === "uploading");
|
|
2048
|
+
const trimmed = value.trim();
|
|
2049
|
+
const canSend = !disabled && !busy && !uploading && (trimmed !== "" || attachments.length > 0);
|
|
2050
|
+
useAutosize(textareaRef, value);
|
|
2051
|
+
const setTextareaRef = useCallback5(
|
|
2052
|
+
(node) => {
|
|
2053
|
+
textareaRef.current = node;
|
|
2054
|
+
if (typeof textareaRefProp === "function") textareaRefProp(node);
|
|
2055
|
+
else if (textareaRefProp) textareaRefProp.current = node;
|
|
2056
|
+
},
|
|
2057
|
+
[textareaRefProp]
|
|
2058
|
+
);
|
|
2059
|
+
const slashMatch = /^\/([^\s]*)$/.exec(value);
|
|
2060
|
+
const matches = slashCommands && slashMatch ? matchCommands(slashCommands, slashMatch[1] ?? "") : [];
|
|
2061
|
+
const commandsOpen = !disabled && matches.length > 0 && dismissedFor !== value;
|
|
2062
|
+
const activeIndex = Math.min(commandIndex, Math.max(0, matches.length - 1));
|
|
2063
|
+
const activeCommand = commandsOpen ? matches[activeIndex] : void 0;
|
|
2064
|
+
const optionId = (index) => `${optionIdBase}-option-${index}`;
|
|
2065
|
+
const pickCommand = (command) => {
|
|
2066
|
+
if (onCommandSelect) onCommandSelect(command);
|
|
2067
|
+
else {
|
|
2068
|
+
setValue(`/${command.name} `);
|
|
2069
|
+
}
|
|
2070
|
+
setCommandIndex(0);
|
|
2071
|
+
textareaRef.current?.focus();
|
|
2072
|
+
};
|
|
2073
|
+
const addFiles = (incoming) => {
|
|
2074
|
+
if (!filesEnabled || incoming.length === 0 || disabled) return;
|
|
2075
|
+
const room = maxFiles === void 0 ? incoming.length : Math.max(0, maxFiles - attachments.length);
|
|
2076
|
+
const accepted = incoming.slice(0, room);
|
|
2077
|
+
if (accepted.length === 0) return;
|
|
2078
|
+
if (attachmentsProp === void 0) {
|
|
2079
|
+
setOwnAttachments((current) => [
|
|
2080
|
+
...current,
|
|
2081
|
+
...accepted.map((file) => {
|
|
2082
|
+
attachmentCounter += 1;
|
|
2083
|
+
return { id: `file-${attachmentCounter}`, name: file.name, size: file.size, mediaType: file.type, status: "ready", file };
|
|
2084
|
+
})
|
|
2085
|
+
]);
|
|
2086
|
+
}
|
|
2087
|
+
onFiles?.([...accepted]);
|
|
2088
|
+
};
|
|
2089
|
+
const removeAttachment = (id) => {
|
|
2090
|
+
if (attachmentsProp === void 0) setOwnAttachments((current) => current.filter((attachment) => attachment.id !== id));
|
|
2091
|
+
onRemoveAttachment?.(id);
|
|
2092
|
+
textareaRef.current?.focus();
|
|
2093
|
+
};
|
|
2094
|
+
const submit = () => {
|
|
2095
|
+
if (!canSend) return;
|
|
2096
|
+
onSubmit(trimmed, attachments);
|
|
2097
|
+
if (valueProp === void 0) setValue("");
|
|
2098
|
+
if (attachmentsProp === void 0) setOwnAttachments([]);
|
|
2099
|
+
};
|
|
2100
|
+
const handleSubmit = (event) => {
|
|
2101
|
+
event.preventDefault();
|
|
2102
|
+
if (busy) onStop?.();
|
|
2103
|
+
else submit();
|
|
2104
|
+
};
|
|
2105
|
+
const handleKeyDown = (event) => {
|
|
2106
|
+
const imeActive = composing.current || event.nativeEvent.isComposing || event.keyCode === 229;
|
|
2107
|
+
if (commandsOpen && !imeActive) {
|
|
2108
|
+
if (event.key === "ArrowDown") {
|
|
2109
|
+
event.preventDefault();
|
|
2110
|
+
setCommandIndex((activeIndex + 1) % matches.length);
|
|
2111
|
+
return;
|
|
2112
|
+
}
|
|
2113
|
+
if (event.key === "ArrowUp") {
|
|
2114
|
+
event.preventDefault();
|
|
2115
|
+
setCommandIndex((activeIndex - 1 + matches.length) % matches.length);
|
|
2116
|
+
return;
|
|
2117
|
+
}
|
|
2118
|
+
if (!event.shiftKey && (event.key === "Enter" || event.key === "Tab")) {
|
|
2119
|
+
const command = matches[activeIndex];
|
|
2120
|
+
if (command) {
|
|
2121
|
+
event.preventDefault();
|
|
2122
|
+
pickCommand(command);
|
|
2123
|
+
return;
|
|
2124
|
+
}
|
|
2125
|
+
}
|
|
2126
|
+
if (event.key === "Escape") {
|
|
2127
|
+
event.preventDefault();
|
|
2128
|
+
setDismissedFor(value);
|
|
2129
|
+
return;
|
|
2130
|
+
}
|
|
2131
|
+
}
|
|
2132
|
+
if (event.key === "Escape" && stopOnEscape && busy && onStop && !imeActive) {
|
|
2133
|
+
event.preventDefault();
|
|
2134
|
+
onStop();
|
|
2135
|
+
return;
|
|
2136
|
+
}
|
|
2137
|
+
if (event.key !== "Enter" || imeActive) return;
|
|
2138
|
+
const modifier = event.ctrlKey || event.metaKey;
|
|
2139
|
+
const wantsSend = submitOnEnter ? !event.shiftKey : modifier;
|
|
2140
|
+
if (!wantsSend) return;
|
|
2141
|
+
event.preventDefault();
|
|
2142
|
+
submit();
|
|
2143
|
+
};
|
|
2144
|
+
const handlePaste = (event) => {
|
|
2145
|
+
if (!filesEnabled) return;
|
|
2146
|
+
const files = Array.from(event.clipboardData.files);
|
|
2147
|
+
if (files.length === 0) return;
|
|
2148
|
+
if (event.clipboardData.getData("text") !== "") return;
|
|
2149
|
+
event.preventDefault();
|
|
2150
|
+
addFiles(files);
|
|
2151
|
+
};
|
|
2152
|
+
const hasFiles = (event) => Array.from(event.dataTransfer.types).includes("Files");
|
|
2153
|
+
const handleDragEnter = (event) => {
|
|
2154
|
+
if (!filesEnabled || !hasFiles(event)) return;
|
|
2155
|
+
event.preventDefault();
|
|
2156
|
+
dragDepth.current += 1;
|
|
2157
|
+
setDragging(true);
|
|
2158
|
+
};
|
|
2159
|
+
const handleDragOver = (event) => {
|
|
2160
|
+
if (!filesEnabled || !hasFiles(event)) return;
|
|
2161
|
+
event.preventDefault();
|
|
2162
|
+
};
|
|
2163
|
+
const handleDragLeave = (event) => {
|
|
2164
|
+
if (!filesEnabled || !hasFiles(event)) return;
|
|
2165
|
+
dragDepth.current = Math.max(0, dragDepth.current - 1);
|
|
2166
|
+
if (dragDepth.current === 0) setDragging(false);
|
|
2167
|
+
};
|
|
2168
|
+
const handleDrop = (event) => {
|
|
2169
|
+
if (!filesEnabled || !hasFiles(event)) return;
|
|
2170
|
+
event.preventDefault();
|
|
2171
|
+
dragDepth.current = 0;
|
|
2172
|
+
setDragging(false);
|
|
2173
|
+
addFiles(Array.from(event.dataTransfer.files));
|
|
2174
|
+
};
|
|
2175
|
+
const handleFileInput = (event) => {
|
|
2176
|
+
addFiles(Array.from(event.target.files ?? []));
|
|
2177
|
+
event.target.value = "";
|
|
2178
|
+
textareaRef.current?.focus();
|
|
2179
|
+
};
|
|
2180
|
+
const atLimit = maxLength !== void 0 && value.length >= maxLength;
|
|
2181
|
+
const announcement = commandsOpen ? messages.commandsAvailable(matches.length) : atLimit ? messages.characterLimitReached : "";
|
|
2182
|
+
const rowStyle = { ...style, "--_ai-max-rows": maxRows };
|
|
2183
|
+
return /* @__PURE__ */ jsxs17(
|
|
2184
|
+
"form",
|
|
2185
|
+
{
|
|
2186
|
+
...props,
|
|
2187
|
+
ref,
|
|
2188
|
+
noValidate: true,
|
|
2189
|
+
"data-kit": "ai-prompt-input",
|
|
2190
|
+
"data-busy": busy ? "" : void 0,
|
|
2191
|
+
"data-disabled": disabled ? "" : void 0,
|
|
2192
|
+
"data-dragging": dragging ? "" : void 0,
|
|
2193
|
+
className: cx(className),
|
|
2194
|
+
style: rowStyle,
|
|
2195
|
+
onSubmit: handleSubmit,
|
|
2196
|
+
onDragEnter: handleDragEnter,
|
|
2197
|
+
onDragOver: handleDragOver,
|
|
2198
|
+
onDragLeave: handleDragLeave,
|
|
2199
|
+
onDrop: handleDrop,
|
|
2200
|
+
children: [
|
|
2201
|
+
attachments.length > 0 ? /* @__PURE__ */ jsx18("ul", { role: "list", "aria-label": messages.attachments, "data-kit": "ai-prompt-attachments", children: attachments.map((attachment) => /* @__PURE__ */ jsxs17("li", { "data-kit": "ai-prompt-attachment", "data-status": attachment.status ?? "ready", children: [
|
|
2202
|
+
/* @__PURE__ */ jsx18(FileGlyph, { size: "sm" }),
|
|
2203
|
+
/* @__PURE__ */ jsx18("span", { "data-kit": "ai-prompt-attachment-name", dir: "auto", children: attachment.name }),
|
|
2204
|
+
attachment.size !== void 0 ? /* @__PURE__ */ jsx18("span", { "data-kit": "ai-prompt-attachment-size", children: formatBytes(attachment.size, locale) }) : null,
|
|
2205
|
+
attachment.status === "uploading" ? /* @__PURE__ */ jsxs17("span", { "data-kit": "ai-prompt-attachment-status", children: [
|
|
2206
|
+
/* @__PURE__ */ jsx18(Spinner3, { size: "xs" }),
|
|
2207
|
+
/* @__PURE__ */ jsx18(VisuallyHidden9, { children: messages.attachmentUploading })
|
|
2208
|
+
] }) : null,
|
|
2209
|
+
attachment.status === "error" ? /* @__PURE__ */ jsx18("span", { "data-kit": "ai-prompt-attachment-status", children: messages.attachmentFailed }) : null,
|
|
2210
|
+
/* @__PURE__ */ jsx18(
|
|
2211
|
+
IconButton4,
|
|
2212
|
+
{
|
|
2213
|
+
variant: "ghost",
|
|
2214
|
+
tone: "neutral",
|
|
2215
|
+
size: "xs",
|
|
2216
|
+
label: messages.removeAttachment(attachment.name),
|
|
2217
|
+
onClick: () => removeAttachment(attachment.id),
|
|
2218
|
+
children: /* @__PURE__ */ jsx18(Icon7, { name: "close", size: 12 })
|
|
2219
|
+
}
|
|
2220
|
+
)
|
|
2221
|
+
] }, attachment.id)) }) : null,
|
|
2222
|
+
/* @__PURE__ */ jsxs17("div", { "data-kit": "ai-prompt-row", children: [
|
|
2223
|
+
filesEnabled ? /* @__PURE__ */ jsxs17(Fragment4, { children: [
|
|
2224
|
+
/* @__PURE__ */ jsx18(
|
|
2225
|
+
IconButton4,
|
|
2226
|
+
{
|
|
2227
|
+
variant: "ghost",
|
|
2228
|
+
tone: "neutral",
|
|
2229
|
+
size: "md",
|
|
2230
|
+
label: messages.attach,
|
|
2231
|
+
disabled: disabled || filesFull,
|
|
2232
|
+
onClick: () => fileInputRef.current?.click(),
|
|
2233
|
+
children: /* @__PURE__ */ jsx18(PaperclipGlyph, { size: "md" })
|
|
2234
|
+
}
|
|
2235
|
+
),
|
|
2236
|
+
/* @__PURE__ */ jsx18(
|
|
2237
|
+
"input",
|
|
2238
|
+
{
|
|
2239
|
+
ref: fileInputRef,
|
|
2240
|
+
type: "file",
|
|
2241
|
+
multiple: true,
|
|
2242
|
+
hidden: true,
|
|
2243
|
+
tabIndex: -1,
|
|
2244
|
+
"aria-hidden": "true",
|
|
2245
|
+
accept,
|
|
2246
|
+
onChange: handleFileInput
|
|
2247
|
+
}
|
|
2248
|
+
)
|
|
2249
|
+
] }) : null,
|
|
2250
|
+
/* @__PURE__ */ jsx18(
|
|
2251
|
+
"textarea",
|
|
2252
|
+
{
|
|
2253
|
+
ref: setTextareaRef,
|
|
2254
|
+
"data-kit": "ai-prompt-textarea",
|
|
2255
|
+
dir: "auto",
|
|
2256
|
+
rows: minRows,
|
|
2257
|
+
value,
|
|
2258
|
+
maxLength,
|
|
2259
|
+
disabled,
|
|
2260
|
+
autoFocus,
|
|
2261
|
+
placeholder: placeholder ?? messages.promptPlaceholder,
|
|
2262
|
+
"aria-label": messages.promptLabel,
|
|
2263
|
+
"aria-describedby": maxLength !== void 0 ? counterId : void 0,
|
|
2264
|
+
"aria-autocomplete": slashCommands ? "list" : void 0,
|
|
2265
|
+
"aria-controls": commandsOpen ? listId : void 0,
|
|
2266
|
+
"aria-activedescendant": activeCommand ? optionId(activeIndex) : void 0,
|
|
2267
|
+
onChange: (event) => {
|
|
2268
|
+
setValue(event.target.value);
|
|
2269
|
+
setCommandIndex(0);
|
|
2270
|
+
},
|
|
2271
|
+
onKeyDown: handleKeyDown,
|
|
2272
|
+
onPaste: handlePaste,
|
|
2273
|
+
onCompositionStart: () => {
|
|
2274
|
+
composing.current = true;
|
|
2275
|
+
},
|
|
2276
|
+
onCompositionEnd: () => {
|
|
2277
|
+
composing.current = false;
|
|
2278
|
+
}
|
|
2279
|
+
}
|
|
2280
|
+
),
|
|
2281
|
+
/* @__PURE__ */ jsx18(
|
|
2282
|
+
IconButton4,
|
|
2283
|
+
{
|
|
2284
|
+
variant: "solid",
|
|
2285
|
+
tone: busy ? "neutral" : "accent",
|
|
2286
|
+
size: "md",
|
|
2287
|
+
type: busy ? "button" : "submit",
|
|
2288
|
+
label: busy ? messages.stop : messages.send,
|
|
2289
|
+
"data-mode": busy ? "stop" : "send",
|
|
2290
|
+
disabled: busy ? onStop === void 0 : !canSend,
|
|
2291
|
+
onClick: busy ? onStop : void 0,
|
|
2292
|
+
children: busy ? /* @__PURE__ */ jsx18(StopGlyph, { size: "md" }) : /* @__PURE__ */ jsx18(SendGlyph, { size: "md" })
|
|
2293
|
+
}
|
|
2294
|
+
)
|
|
2295
|
+
] }),
|
|
2296
|
+
commandsOpen ? /* @__PURE__ */ jsx18("ul", { role: "listbox", id: listId, "aria-label": messages.commands, "data-kit": "ai-prompt-commands", children: matches.map((command, index) => /* @__PURE__ */ jsxs17(
|
|
2297
|
+
"li",
|
|
2298
|
+
{
|
|
2299
|
+
id: optionId(index),
|
|
2300
|
+
role: "option",
|
|
2301
|
+
"aria-selected": index === activeIndex,
|
|
2302
|
+
"data-active": index === activeIndex ? "" : void 0,
|
|
2303
|
+
onMouseDown: (event) => {
|
|
2304
|
+
event.preventDefault();
|
|
2305
|
+
},
|
|
2306
|
+
onClick: () => pickCommand(command),
|
|
2307
|
+
children: [
|
|
2308
|
+
/* @__PURE__ */ jsxs17("span", { "data-kit": "ai-prompt-command-name", dir: "ltr", children: [
|
|
2309
|
+
"/",
|
|
2310
|
+
command.name
|
|
2311
|
+
] }),
|
|
2312
|
+
command.description ? /* @__PURE__ */ jsx18("span", { "data-kit": "ai-prompt-command-description", dir: "auto", children: command.description }) : null
|
|
2313
|
+
]
|
|
2314
|
+
},
|
|
2315
|
+
command.id ?? command.name
|
|
2316
|
+
)) }) : null,
|
|
2317
|
+
maxLength !== void 0 || footer ? /* @__PURE__ */ jsxs17("div", { "data-kit": "ai-prompt-meta", children: [
|
|
2318
|
+
footer ? /* @__PURE__ */ jsx18("div", { "data-kit": "ai-prompt-footer", children: footer }) : null,
|
|
2319
|
+
maxLength !== void 0 ? /* @__PURE__ */ jsx18("span", { id: counterId, "data-kit": "ai-prompt-counter", "data-limit": atLimit ? "" : void 0, dir: "ltr", children: messages.characterCount(value.length, maxLength) }) : null
|
|
2320
|
+
] }) : null,
|
|
2321
|
+
/* @__PURE__ */ jsx18(VisuallyHidden9, { role: "status", "aria-live": "polite", "aria-atomic": "true", children: announcement }),
|
|
2322
|
+
dragging ? /* @__PURE__ */ jsx18("div", { "data-kit": "ai-prompt-drop", "aria-hidden": "true", children: messages.dropFiles }) : null
|
|
2323
|
+
]
|
|
2324
|
+
}
|
|
2325
|
+
);
|
|
2326
|
+
}
|
|
2327
|
+
|
|
2328
|
+
// src/react/suggestions.tsx
|
|
2329
|
+
import { Button as Button6 } from "@x12i/ui";
|
|
2330
|
+
import { jsx as jsx19 } from "react/jsx-runtime";
|
|
2331
|
+
function labelOf(suggestion) {
|
|
2332
|
+
return typeof suggestion === "string" ? suggestion : suggestion.label;
|
|
2333
|
+
}
|
|
2334
|
+
function Suggestions({ suggestions, onSelect, disabled = false, messages: override, className, ref, ...props }) {
|
|
2335
|
+
const messages = useAiMessages(override);
|
|
2336
|
+
if (suggestions.length === 0) return null;
|
|
2337
|
+
return /* @__PURE__ */ jsx19("div", { ...props, ref, "data-kit": "ai-suggestions", className: cx(className), children: /* @__PURE__ */ jsx19("ul", { role: "list", "aria-label": messages.suggestions, children: suggestions.map((suggestion, position) => {
|
|
2338
|
+
const label = labelOf(suggestion);
|
|
2339
|
+
const prompt = typeof suggestion === "string" ? suggestion : suggestion.prompt ?? suggestion.label;
|
|
2340
|
+
const key = typeof suggestion === "string" ? `${position}-${suggestion}` : suggestion.id ?? `${position}-${suggestion.label}`;
|
|
2341
|
+
return /* @__PURE__ */ jsx19("li", { children: /* @__PURE__ */ jsx19(
|
|
2342
|
+
Button6,
|
|
2343
|
+
{
|
|
2344
|
+
variant: "outline",
|
|
2345
|
+
tone: "neutral",
|
|
2346
|
+
size: "sm",
|
|
2347
|
+
disabled,
|
|
2348
|
+
onClick: () => onSelect(prompt, suggestion),
|
|
2349
|
+
dir: "auto",
|
|
2350
|
+
children: label
|
|
2351
|
+
}
|
|
2352
|
+
) }, key);
|
|
2353
|
+
}) }) });
|
|
2354
|
+
}
|
|
2355
|
+
|
|
2356
|
+
// src/react/chat-thread.tsx
|
|
2357
|
+
import { jsx as jsx20, jsxs as jsxs18 } from "react/jsx-runtime";
|
|
2358
|
+
var ANNOUNCE_MAX_CHARS = 600;
|
|
2359
|
+
function truncate(text) {
|
|
2360
|
+
return text.length > ANNOUNCE_MAX_CHARS ? `${text.slice(0, ANNOUNCE_MAX_CHARS).trimEnd()}\u2026` : text;
|
|
2361
|
+
}
|
|
2362
|
+
function useAnnouncement({ messages, busy, mode, approvalsEnabled }) {
|
|
2363
|
+
const strings = useAiMessages();
|
|
2364
|
+
const [announcement, setAnnouncement] = useState7("");
|
|
2365
|
+
const wasBusy = useRef11(busy);
|
|
2366
|
+
const spoken = useRef11(/* @__PURE__ */ new Map());
|
|
2367
|
+
const approvals = useRef11(null);
|
|
2368
|
+
if (approvals.current === null) {
|
|
2369
|
+
approvals.current = new Set(threadPendingApprovals(messages).map((part) => part.id));
|
|
2370
|
+
}
|
|
2371
|
+
useEffect6(() => {
|
|
2372
|
+
if (mode === "off") {
|
|
2373
|
+
wasBusy.current = busy;
|
|
2374
|
+
return;
|
|
2375
|
+
}
|
|
2376
|
+
const lines = [];
|
|
2377
|
+
if (mode === "sentences" && busy) {
|
|
2378
|
+
for (const message of messages) {
|
|
2379
|
+
if (message.role !== "assistant" || message.status !== "streaming") continue;
|
|
2380
|
+
const { complete } = splitCompletedSentences(messageText(message));
|
|
2381
|
+
const done = spoken.current.get(message.id) ?? 0;
|
|
2382
|
+
if (complete.length > done) {
|
|
2383
|
+
const chunk = markdownToPlainText(complete.slice(done));
|
|
2384
|
+
spoken.current.set(message.id, complete.length);
|
|
2385
|
+
if (chunk !== "") lines.push(chunk);
|
|
2386
|
+
}
|
|
2387
|
+
}
|
|
2388
|
+
}
|
|
2389
|
+
if (wasBusy.current && !busy) {
|
|
2390
|
+
const last = lastMessageWithRole(messages, "assistant");
|
|
2391
|
+
if (last) {
|
|
2392
|
+
const text = messageText(last);
|
|
2393
|
+
const alreadySpoken = spoken.current.get(last.id) ?? 0;
|
|
2394
|
+
spoken.current.delete(last.id);
|
|
2395
|
+
if (last.status === "stopped") {
|
|
2396
|
+
lines.push(strings.announceStopped);
|
|
2397
|
+
} else if (last.status === "complete" || last.status === void 0) {
|
|
2398
|
+
if (mode === "sentences") {
|
|
2399
|
+
const rest = markdownToPlainText(text.slice(alreadySpoken));
|
|
2400
|
+
if (rest !== "") lines.push(rest);
|
|
2401
|
+
} else {
|
|
2402
|
+
const plain = markdownToPlainText(text);
|
|
2403
|
+
if (plain !== "") lines.push(strings.announceReply(truncate(plain)));
|
|
2404
|
+
}
|
|
2405
|
+
}
|
|
2406
|
+
}
|
|
2407
|
+
}
|
|
2408
|
+
wasBusy.current = busy;
|
|
2409
|
+
if (approvalsEnabled) {
|
|
2410
|
+
for (const part of threadPendingApprovals(messages)) {
|
|
2411
|
+
if (!approvals.current?.has(part.id)) {
|
|
2412
|
+
approvals.current?.add(part.id);
|
|
2413
|
+
lines.push(strings.announceApproval(part.name));
|
|
2414
|
+
}
|
|
2415
|
+
}
|
|
2416
|
+
}
|
|
2417
|
+
if (lines.length > 0) setAnnouncement(lines.join(" "));
|
|
2418
|
+
}, [messages, busy, mode, approvalsEnabled, strings]);
|
|
2419
|
+
return announcement;
|
|
2420
|
+
}
|
|
2421
|
+
var ThreadMessageItem = memo2(function ThreadMessageItem2(props) {
|
|
2422
|
+
const { message, streaming, busy } = props;
|
|
2423
|
+
const text = messageText(message);
|
|
2424
|
+
const role = message.role;
|
|
2425
|
+
const interactive = (role === "assistant" || role === "user") && !streaming && !props.editing;
|
|
2426
|
+
const regenerate = interactive && role === "assistant" && props.isLastAssistant && props.canRegenerate;
|
|
2427
|
+
const edit = interactive && role === "user" && props.canEdit;
|
|
2428
|
+
const rate = interactive && role === "assistant" && props.canFeedback;
|
|
2429
|
+
const branch = interactive ? props.branch : void 0;
|
|
2430
|
+
const hasActions = text !== "" || regenerate || edit || rate || branch !== void 0 && branch.count > 1;
|
|
2431
|
+
const actions = interactive && hasActions ? /* @__PURE__ */ jsx20(
|
|
2432
|
+
MessageActions,
|
|
2433
|
+
{
|
|
2434
|
+
text: text === "" ? void 0 : text,
|
|
2435
|
+
onRegenerate: regenerate ? props.onRegenerate : void 0,
|
|
2436
|
+
onEdit: edit ? () => props.onStartEdit(message.id) : void 0,
|
|
2437
|
+
feedback: props.feedback,
|
|
2438
|
+
onFeedback: rate ? (value) => props.onFeedback(message.id, value) : void 0,
|
|
2439
|
+
branch: branch ? { index: branch.index, count: branch.count, onSelect: (index) => props.onSwitchBranch(branch.parentId, index) } : void 0,
|
|
2440
|
+
disabled: busy
|
|
2441
|
+
}
|
|
2442
|
+
) : void 0;
|
|
2443
|
+
return /* @__PURE__ */ jsx20(
|
|
2444
|
+
Message,
|
|
2445
|
+
{
|
|
2446
|
+
message,
|
|
2447
|
+
streaming,
|
|
2448
|
+
avatar: props.avatar,
|
|
2449
|
+
name: props.name,
|
|
2450
|
+
locale: props.locale,
|
|
2451
|
+
images: props.images,
|
|
2452
|
+
showTimestamp: props.showTimestamp,
|
|
2453
|
+
showUsage: props.showUsage,
|
|
2454
|
+
renderPart: props.renderPart,
|
|
2455
|
+
onToolDecision: props.canDecide ? props.onToolDecision : void 0,
|
|
2456
|
+
onRetry: props.canRegenerate ? props.onRegenerate : void 0,
|
|
2457
|
+
editing: props.editing,
|
|
2458
|
+
onEditSubmit: (value) => props.onEditSubmit(message.id, value),
|
|
2459
|
+
onEditCancel: props.onEditCancel,
|
|
2460
|
+
actions
|
|
2461
|
+
}
|
|
2462
|
+
);
|
|
2463
|
+
});
|
|
2464
|
+
function ChatThread({
|
|
2465
|
+
messages: threadMessages,
|
|
2466
|
+
status: statusProp,
|
|
2467
|
+
error = null,
|
|
2468
|
+
onSend,
|
|
2469
|
+
onStop,
|
|
2470
|
+
onRegenerate,
|
|
2471
|
+
onToolDecision,
|
|
2472
|
+
onEdit,
|
|
2473
|
+
branches,
|
|
2474
|
+
onSwitchBranch,
|
|
2475
|
+
onDismissError,
|
|
2476
|
+
onFeedback,
|
|
2477
|
+
feedback: feedbackProp,
|
|
2478
|
+
suggestions,
|
|
2479
|
+
followUps,
|
|
2480
|
+
renderPart,
|
|
2481
|
+
emptyState,
|
|
2482
|
+
header,
|
|
2483
|
+
disclaimer,
|
|
2484
|
+
avatars,
|
|
2485
|
+
names,
|
|
2486
|
+
showTimestamps = false,
|
|
2487
|
+
showUsage = false,
|
|
2488
|
+
images,
|
|
2489
|
+
announce = "complete",
|
|
2490
|
+
disabled = false,
|
|
2491
|
+
promptProps,
|
|
2492
|
+
locale,
|
|
2493
|
+
strings,
|
|
2494
|
+
conversationRef,
|
|
2495
|
+
className,
|
|
2496
|
+
ref,
|
|
2497
|
+
...props
|
|
2498
|
+
}) {
|
|
2499
|
+
return /* @__PURE__ */ jsx20(AiMessagesProvider, { messages: strings, children: /* @__PURE__ */ jsx20(
|
|
2500
|
+
ChatThreadInner,
|
|
2501
|
+
{
|
|
2502
|
+
...props,
|
|
2503
|
+
ref,
|
|
2504
|
+
className,
|
|
2505
|
+
messages: threadMessages,
|
|
2506
|
+
status: statusProp,
|
|
2507
|
+
error,
|
|
2508
|
+
onSend,
|
|
2509
|
+
onStop,
|
|
2510
|
+
onRegenerate,
|
|
2511
|
+
onToolDecision,
|
|
2512
|
+
onEdit,
|
|
2513
|
+
branches,
|
|
2514
|
+
onSwitchBranch,
|
|
2515
|
+
onDismissError,
|
|
2516
|
+
onFeedback,
|
|
2517
|
+
feedback: feedbackProp,
|
|
2518
|
+
suggestions,
|
|
2519
|
+
followUps,
|
|
2520
|
+
renderPart,
|
|
2521
|
+
emptyState,
|
|
2522
|
+
header,
|
|
2523
|
+
disclaimer,
|
|
2524
|
+
avatars,
|
|
2525
|
+
names,
|
|
2526
|
+
showTimestamps,
|
|
2527
|
+
showUsage,
|
|
2528
|
+
images,
|
|
2529
|
+
announce,
|
|
2530
|
+
disabled,
|
|
2531
|
+
promptProps,
|
|
2532
|
+
locale,
|
|
2533
|
+
conversationRef
|
|
2534
|
+
}
|
|
2535
|
+
) });
|
|
2536
|
+
}
|
|
2537
|
+
function ChatThreadInner({
|
|
2538
|
+
messages: threadMessages,
|
|
2539
|
+
status: statusProp,
|
|
2540
|
+
error,
|
|
2541
|
+
onSend,
|
|
2542
|
+
onStop,
|
|
2543
|
+
onRegenerate,
|
|
2544
|
+
onToolDecision,
|
|
2545
|
+
onEdit,
|
|
2546
|
+
branches,
|
|
2547
|
+
onSwitchBranch,
|
|
2548
|
+
onDismissError,
|
|
2549
|
+
onFeedback,
|
|
2550
|
+
feedback: feedbackProp,
|
|
2551
|
+
suggestions,
|
|
2552
|
+
followUps,
|
|
2553
|
+
renderPart,
|
|
2554
|
+
emptyState,
|
|
2555
|
+
header,
|
|
2556
|
+
disclaimer,
|
|
2557
|
+
avatars,
|
|
2558
|
+
names,
|
|
2559
|
+
showTimestamps = false,
|
|
2560
|
+
showUsage = false,
|
|
2561
|
+
images,
|
|
2562
|
+
announce = "complete",
|
|
2563
|
+
disabled = false,
|
|
2564
|
+
promptProps,
|
|
2565
|
+
locale,
|
|
2566
|
+
conversationRef,
|
|
2567
|
+
className,
|
|
2568
|
+
ref,
|
|
2569
|
+
...props
|
|
2570
|
+
}) {
|
|
2571
|
+
const messages = useAiMessages();
|
|
2572
|
+
const last = threadMessages[threadMessages.length - 1];
|
|
2573
|
+
const status = statusProp ?? (last?.status === "streaming" ? "streaming" : "idle");
|
|
2574
|
+
const busy = status === "submitted" || status === "streaming";
|
|
2575
|
+
const [editingId, setEditingId] = useState7(null);
|
|
2576
|
+
const [ownFeedback, setOwnFeedback] = useState7({});
|
|
2577
|
+
const feedbackMap = feedbackProp ?? ownFeedback;
|
|
2578
|
+
const pending = useMemo5(() => threadPendingApprovals(threadMessages), [threadMessages]);
|
|
2579
|
+
const announcement = useAnnouncement({
|
|
2580
|
+
messages: threadMessages,
|
|
2581
|
+
busy,
|
|
2582
|
+
mode: announce,
|
|
2583
|
+
approvalsEnabled: onToolDecision !== void 0
|
|
2584
|
+
});
|
|
2585
|
+
const handleToolDecision = useEventCallback(
|
|
2586
|
+
(callId, decision, reason) => onToolDecision?.(callId, decision, reason)
|
|
2587
|
+
);
|
|
2588
|
+
const handleRegenerate = useEventCallback(() => onRegenerate?.());
|
|
2589
|
+
const handleStartEdit = useEventCallback((id) => setEditingId(id));
|
|
2590
|
+
const handleEditSubmit = useEventCallback((id, text) => {
|
|
2591
|
+
setEditingId(null);
|
|
2592
|
+
onEdit?.(id, text);
|
|
2593
|
+
});
|
|
2594
|
+
const handleEditCancel = useEventCallback(() => setEditingId(null));
|
|
2595
|
+
const handleFeedback = useEventCallback((id, value) => {
|
|
2596
|
+
if (feedbackProp === void 0) setOwnFeedback((current) => ({ ...current, [id]: value }));
|
|
2597
|
+
onFeedback?.(id, value);
|
|
2598
|
+
});
|
|
2599
|
+
const handleSwitchBranch = useEventCallback((parentId, index) => onSwitchBranch?.(parentId, index));
|
|
2600
|
+
const lastAssistantId = useMemo5(() => {
|
|
2601
|
+
for (let index = threadMessages.length - 1; index >= 0; index -= 1) {
|
|
2602
|
+
const message = threadMessages[index];
|
|
2603
|
+
if (message?.role === "assistant") return message.id;
|
|
2604
|
+
}
|
|
2605
|
+
return void 0;
|
|
2606
|
+
}, [threadMessages]);
|
|
2607
|
+
const errorOnMessage = last?.status === "error" && last.error !== void 0;
|
|
2608
|
+
const threadError = error ?? (status === "error" ? { message: "", kind: "unknown" } : null);
|
|
2609
|
+
const showThreadError = threadError !== null && !errorOnMessage;
|
|
2610
|
+
const lastUser = useMemo5(() => {
|
|
2611
|
+
for (let index = threadMessages.length - 1; index >= 0; index -= 1) {
|
|
2612
|
+
const message = threadMessages[index];
|
|
2613
|
+
if (message?.role === "user") return message;
|
|
2614
|
+
}
|
|
2615
|
+
return void 0;
|
|
2616
|
+
}, [threadMessages]);
|
|
2617
|
+
const empty = threadMessages.length === 0 && status === "idle";
|
|
2618
|
+
const waitingForReply = status === "submitted" || status === "streaming" && last?.role !== "assistant";
|
|
2619
|
+
const showFollowUps = followUps !== void 0 && followUps.length > 0 && !busy && last?.role === "assistant" && pending.length === 0;
|
|
2620
|
+
const pickSuggestion = (prompt) => onSend(prompt);
|
|
2621
|
+
return /* @__PURE__ */ jsxs18("section", { ...props, ref, "aria-label": props["aria-label"] ?? messages.chat, "data-kit": "ai-chat-thread", "data-status": status, className: cx(className), children: [
|
|
2622
|
+
header,
|
|
2623
|
+
/* @__PURE__ */ jsxs18(Conversation, { handleRef: conversationRef, children: [
|
|
2624
|
+
empty ? emptyState ?? /* @__PURE__ */ jsx20(EmptyThread, { children: suggestions && suggestions.length > 0 ? /* @__PURE__ */ jsx20(Suggestions, { suggestions, onSelect: pickSuggestion, disabled }) : null }) : /* @__PURE__ */ jsx20("div", { "data-kit": "ai-message-list", children: threadMessages.map((message) => {
|
|
2625
|
+
const role = message.role;
|
|
2626
|
+
const streaming = message.status === "streaming" && busy;
|
|
2627
|
+
const avatar = role === "user" ? avatars?.user : role === "assistant" ? avatars?.assistant : void 0;
|
|
2628
|
+
const name = role === "user" ? names?.user : role === "assistant" ? names?.assistant : void 0;
|
|
2629
|
+
return /* @__PURE__ */ jsx20(
|
|
2630
|
+
ThreadMessageItem,
|
|
2631
|
+
{
|
|
2632
|
+
message,
|
|
2633
|
+
streaming,
|
|
2634
|
+
busy,
|
|
2635
|
+
isLastAssistant: message.id === lastAssistantId,
|
|
2636
|
+
branch: branches?.[message.id],
|
|
2637
|
+
editing: editingId === message.id,
|
|
2638
|
+
feedback: feedbackMap[message.id] ?? null,
|
|
2639
|
+
canFeedback: onFeedback !== void 0,
|
|
2640
|
+
canEdit: onEdit !== void 0,
|
|
2641
|
+
canRegenerate: onRegenerate !== void 0,
|
|
2642
|
+
canDecide: onToolDecision !== void 0,
|
|
2643
|
+
avatar,
|
|
2644
|
+
name,
|
|
2645
|
+
locale,
|
|
2646
|
+
images,
|
|
2647
|
+
showTimestamp: showTimestamps,
|
|
2648
|
+
showUsage,
|
|
2649
|
+
renderPart,
|
|
2650
|
+
onToolDecision: handleToolDecision,
|
|
2651
|
+
onRegenerate: handleRegenerate,
|
|
2652
|
+
onStartEdit: handleStartEdit,
|
|
2653
|
+
onEditSubmit: handleEditSubmit,
|
|
2654
|
+
onEditCancel: handleEditCancel,
|
|
2655
|
+
onFeedback: handleFeedback,
|
|
2656
|
+
onSwitchBranch: handleSwitchBranch
|
|
2657
|
+
},
|
|
2658
|
+
message.id
|
|
2659
|
+
);
|
|
2660
|
+
}) }),
|
|
2661
|
+
waitingForReply ? /* @__PURE__ */ jsx20("div", { "data-kit": "ai-pending", "aria-busy": "true", children: /* @__PURE__ */ jsx20(StreamingIndicator, { label: messages.responding }) }) : null,
|
|
2662
|
+
showThreadError && threadError ? /* @__PURE__ */ jsx20(
|
|
2663
|
+
ErrorBanner,
|
|
2664
|
+
{
|
|
2665
|
+
error: threadError,
|
|
2666
|
+
showDetail: threadError.message !== "",
|
|
2667
|
+
onRetry: onRegenerate && !empty ? handleRegenerate : void 0,
|
|
2668
|
+
onDismiss: onDismissError,
|
|
2669
|
+
onEdit: lastUser && onEdit ? () => setEditingId(lastUser.id) : void 0
|
|
2670
|
+
}
|
|
2671
|
+
) : null,
|
|
2672
|
+
showFollowUps && followUps ? /* @__PURE__ */ jsx20(Suggestions, { suggestions: followUps, onSelect: pickSuggestion, disabled }) : null
|
|
2673
|
+
] }),
|
|
2674
|
+
/* @__PURE__ */ jsxs18("div", { "data-kit": "ai-chat-composer", children: [
|
|
2675
|
+
/* @__PURE__ */ jsx20(
|
|
2676
|
+
PromptInput,
|
|
2677
|
+
{
|
|
2678
|
+
...promptProps,
|
|
2679
|
+
status,
|
|
2680
|
+
onStop,
|
|
2681
|
+
disabled,
|
|
2682
|
+
onSubmit: (text, attachments) => onSend(text, attachments)
|
|
2683
|
+
}
|
|
2684
|
+
),
|
|
2685
|
+
disclaimer === false ? null : /* @__PURE__ */ jsx20("p", { "data-kit": "ai-disclaimer", dir: "auto", children: disclaimer ?? messages.disclaimer })
|
|
2686
|
+
] }),
|
|
2687
|
+
/* @__PURE__ */ jsx20(VisuallyHidden10, { role: "status", "aria-live": "polite", "aria-atomic": "true", children: announcement })
|
|
2688
|
+
] });
|
|
2689
|
+
}
|
|
2690
|
+
|
|
2691
|
+
// src/react/use-thread.ts
|
|
2692
|
+
import { useCallback as useCallback6, useEffect as useEffect7, useMemo as useMemo6, useRef as useRef12, useState as useState8 } from "react";
|
|
2693
|
+
var idCounter = 0;
|
|
2694
|
+
function defaultGenerateId() {
|
|
2695
|
+
idCounter += 1;
|
|
2696
|
+
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") return crypto.randomUUID();
|
|
2697
|
+
return `msg-${Date.now().toString(36)}-${idCounter}`;
|
|
2698
|
+
}
|
|
2699
|
+
function isBusyRef(ref) {
|
|
2700
|
+
return ref.current ? isBusy(ref.current) : false;
|
|
2701
|
+
}
|
|
2702
|
+
function buildRequest(state, trigger, signal) {
|
|
2703
|
+
const messages = state?.messages ?? [];
|
|
2704
|
+
const user = lastUserMessage({ messages });
|
|
2705
|
+
return {
|
|
2706
|
+
trigger,
|
|
2707
|
+
message: user,
|
|
2708
|
+
text: user ? messageText(user) : "",
|
|
2709
|
+
attachments: user ? user.parts.filter((part) => part.type === "file") : [],
|
|
2710
|
+
messages,
|
|
2711
|
+
signal
|
|
2712
|
+
};
|
|
2713
|
+
}
|
|
2714
|
+
function toFileParts(attachments) {
|
|
2715
|
+
return attachments.map((attachment, index) => {
|
|
2716
|
+
let url = attachment.url ?? "";
|
|
2717
|
+
if (url === "" && attachment.file && typeof URL !== "undefined" && typeof URL.createObjectURL === "function") {
|
|
2718
|
+
url = URL.createObjectURL(attachment.file);
|
|
2719
|
+
}
|
|
2720
|
+
return {
|
|
2721
|
+
type: "file",
|
|
2722
|
+
id: attachment.id ?? `file-${idCounter}-${index}`,
|
|
2723
|
+
name: attachment.name,
|
|
2724
|
+
mediaType: attachment.mediaType ?? attachment.file?.type ?? "application/octet-stream",
|
|
2725
|
+
url,
|
|
2726
|
+
...attachment.size === void 0 && attachment.file === void 0 ? {} : { size: attachment.size ?? attachment.file?.size ?? 0 }
|
|
2727
|
+
};
|
|
2728
|
+
});
|
|
2729
|
+
}
|
|
2730
|
+
function useThread(options = {}) {
|
|
2731
|
+
const { batch = true, initialMessages } = options;
|
|
2732
|
+
const stateRef = useRef12(null);
|
|
2733
|
+
if (stateRef.current === null) stateRef.current = createThreadState(initialMessages ?? []);
|
|
2734
|
+
const [state, setState] = useState8(stateRef.current);
|
|
2735
|
+
const runRef = useRef12(null);
|
|
2736
|
+
const frameRef = useRef12(null);
|
|
2737
|
+
const decisionChain = useRef12(Promise.resolve());
|
|
2738
|
+
const mounted = useRef12(true);
|
|
2739
|
+
const onSend = useEventCallback((request) => options.onSend?.(request));
|
|
2740
|
+
const onRegenerate = useEventCallback((request) => options.onRegenerate?.(request));
|
|
2741
|
+
const onToolDecision = useEventCallback((request) => options.onToolDecision?.(request));
|
|
2742
|
+
const onStop = useEventCallback((info) => options.onStop?.(info));
|
|
2743
|
+
const onFinish = useEventCallback((message) => options.onFinish?.(message));
|
|
2744
|
+
const onError = useEventCallback((error) => options.onError?.(error));
|
|
2745
|
+
const hasOnRegenerate = options.onRegenerate !== void 0;
|
|
2746
|
+
const hasOnToolDecision = options.onToolDecision !== void 0;
|
|
2747
|
+
const generateId = useEventCallback(() => (options.generateId ?? defaultGenerateId)());
|
|
2748
|
+
const now = useEventCallback(() => (options.now ?? Date.now)());
|
|
2749
|
+
const cancelFrame = useCallback6(() => {
|
|
2750
|
+
const frame = frameRef.current;
|
|
2751
|
+
if (!frame) return;
|
|
2752
|
+
if (frame.kind === "raf" && typeof cancelAnimationFrame === "function") cancelAnimationFrame(frame.id);
|
|
2753
|
+
else clearTimeout(frame.id);
|
|
2754
|
+
frameRef.current = null;
|
|
2755
|
+
}, []);
|
|
2756
|
+
const flush = useCallback6(() => {
|
|
2757
|
+
cancelFrame();
|
|
2758
|
+
if (mounted.current && stateRef.current) setState(stateRef.current);
|
|
2759
|
+
}, [cancelFrame]);
|
|
2760
|
+
const scheduleRender = useCallback6(() => {
|
|
2761
|
+
if (!batch) {
|
|
2762
|
+
flush();
|
|
2763
|
+
return;
|
|
2764
|
+
}
|
|
2765
|
+
if (frameRef.current) return;
|
|
2766
|
+
const run = () => {
|
|
2767
|
+
frameRef.current = null;
|
|
2768
|
+
if (mounted.current && stateRef.current) setState(stateRef.current);
|
|
2769
|
+
};
|
|
2770
|
+
if (typeof requestAnimationFrame === "function") {
|
|
2771
|
+
frameRef.current = { kind: "raf", id: requestAnimationFrame(run) };
|
|
2772
|
+
} else {
|
|
2773
|
+
frameRef.current = { kind: "timeout", id: window.setTimeout(run, 16) };
|
|
2774
|
+
}
|
|
2775
|
+
}, [batch, flush]);
|
|
2776
|
+
const dispatch = useCallback6(
|
|
2777
|
+
(action) => {
|
|
2778
|
+
if (!stateRef.current) return;
|
|
2779
|
+
stateRef.current = threadReducer(stateRef.current, action);
|
|
2780
|
+
flush();
|
|
2781
|
+
},
|
|
2782
|
+
[flush]
|
|
2783
|
+
);
|
|
2784
|
+
const applyEvent = useCallback6(
|
|
2785
|
+
(event) => {
|
|
2786
|
+
if (!stateRef.current) return;
|
|
2787
|
+
const stamped = event.type === "start" && event.createdAt === void 0 ? { ...event, createdAt: new Date(now()).toISOString() } : event;
|
|
2788
|
+
stateRef.current = applyStreamEvent(stateRef.current, stamped, { at: now() });
|
|
2789
|
+
if (isTerminalStreamEvent(stamped)) flush();
|
|
2790
|
+
else scheduleRender();
|
|
2791
|
+
},
|
|
2792
|
+
[flush, now, scheduleRender]
|
|
2793
|
+
);
|
|
2794
|
+
const failRun = useCallback6(
|
|
2795
|
+
(error) => {
|
|
2796
|
+
const threadError = toThreadError(error);
|
|
2797
|
+
dispatch({ type: "error", error: threadError });
|
|
2798
|
+
onError(threadError);
|
|
2799
|
+
},
|
|
2800
|
+
[dispatch, onError]
|
|
2801
|
+
);
|
|
2802
|
+
const runProducer = useCallback6(
|
|
2803
|
+
async (producer) => {
|
|
2804
|
+
runRef.current?.controller.abort();
|
|
2805
|
+
let finishRun;
|
|
2806
|
+
const run = {
|
|
2807
|
+
controller: new AbortController(),
|
|
2808
|
+
done: new Promise((resolve) => {
|
|
2809
|
+
finishRun = resolve;
|
|
2810
|
+
})
|
|
2811
|
+
};
|
|
2812
|
+
runRef.current = run;
|
|
2813
|
+
const { signal } = run.controller;
|
|
2814
|
+
try {
|
|
2815
|
+
let source;
|
|
2816
|
+
try {
|
|
2817
|
+
source = await producer(signal);
|
|
2818
|
+
} catch (error) {
|
|
2819
|
+
if (!signal.aborted && runRef.current === run) failRun(error);
|
|
2820
|
+
return;
|
|
2821
|
+
}
|
|
2822
|
+
if (runRef.current !== run || signal.aborted) return;
|
|
2823
|
+
if (!source) {
|
|
2824
|
+
dispatch({ type: "finish" });
|
|
2825
|
+
return;
|
|
2826
|
+
}
|
|
2827
|
+
let finished = false;
|
|
2828
|
+
try {
|
|
2829
|
+
for await (const event of createAbortableStream(source, signal)) {
|
|
2830
|
+
if (runRef.current !== run) break;
|
|
2831
|
+
applyEvent(event);
|
|
2832
|
+
if (event.type === "finish") finished = true;
|
|
2833
|
+
if (isTerminalStreamEvent(event)) break;
|
|
2834
|
+
}
|
|
2835
|
+
} catch (error) {
|
|
2836
|
+
if (!signal.aborted && runRef.current === run) failRun(error);
|
|
2837
|
+
return;
|
|
2838
|
+
}
|
|
2839
|
+
if (runRef.current === run && stateRef.current) {
|
|
2840
|
+
if (isBusy(stateRef.current)) {
|
|
2841
|
+
dispatch({ type: "finish" });
|
|
2842
|
+
finished = true;
|
|
2843
|
+
}
|
|
2844
|
+
if (finished) onFinish(lastMessageWithRole(stateRef.current.messages, "assistant"));
|
|
2845
|
+
}
|
|
2846
|
+
} finally {
|
|
2847
|
+
if (runRef.current === run) runRef.current = null;
|
|
2848
|
+
finishRun();
|
|
2849
|
+
}
|
|
2850
|
+
},
|
|
2851
|
+
[applyEvent, dispatch, failRun, onFinish]
|
|
2852
|
+
);
|
|
2853
|
+
const send = useCallback6(
|
|
2854
|
+
async (text, attachments = []) => {
|
|
2855
|
+
const trimmed = text.trim();
|
|
2856
|
+
if (trimmed === "" && attachments.length === 0) return;
|
|
2857
|
+
if (isBusyRef(stateRef)) return;
|
|
2858
|
+
dispatch({
|
|
2859
|
+
type: "add-user-message",
|
|
2860
|
+
id: generateId(),
|
|
2861
|
+
text: trimmed,
|
|
2862
|
+
attachments: toFileParts(attachments),
|
|
2863
|
+
createdAt: new Date(now()).toISOString()
|
|
2864
|
+
});
|
|
2865
|
+
await runProducer((signal) => onSend(buildRequest(stateRef.current, "send", signal)));
|
|
2866
|
+
},
|
|
2867
|
+
[dispatch, generateId, now, onSend, runProducer]
|
|
2868
|
+
);
|
|
2869
|
+
const stop = useCallback6(() => {
|
|
2870
|
+
const run = runRef.current;
|
|
2871
|
+
if (run) {
|
|
2872
|
+
runRef.current = null;
|
|
2873
|
+
run.controller.abort();
|
|
2874
|
+
}
|
|
2875
|
+
if (stateRef.current && isBusy(stateRef.current)) {
|
|
2876
|
+
dispatch({ type: "abort", at: now() });
|
|
2877
|
+
onStop({ messages: stateRef.current.messages });
|
|
2878
|
+
}
|
|
2879
|
+
}, [dispatch, now, onStop]);
|
|
2880
|
+
const regenerate = useCallback6(async () => {
|
|
2881
|
+
if (isBusyRef(stateRef)) return;
|
|
2882
|
+
dispatch({ type: "regenerate" });
|
|
2883
|
+
if (stateRef.current?.status !== "submitted") return;
|
|
2884
|
+
const handler = hasOnRegenerate ? onRegenerate : onSend;
|
|
2885
|
+
await runProducer((signal) => handler(buildRequest(stateRef.current, "regenerate", signal)));
|
|
2886
|
+
}, [dispatch, hasOnRegenerate, onRegenerate, onSend, runProducer]);
|
|
2887
|
+
const edit = useCallback6(
|
|
2888
|
+
async (messageId, text, editOptions = {}) => {
|
|
2889
|
+
if (isBusyRef(stateRef) || text.trim() === "") return;
|
|
2890
|
+
dispatch({
|
|
2891
|
+
type: "edit-message",
|
|
2892
|
+
messageId,
|
|
2893
|
+
text: text.trim(),
|
|
2894
|
+
mode: editOptions.mode ?? "truncate",
|
|
2895
|
+
createdAt: new Date(now()).toISOString()
|
|
2896
|
+
});
|
|
2897
|
+
if (stateRef.current?.status !== "submitted") return;
|
|
2898
|
+
await runProducer((signal) => onSend(buildRequest(stateRef.current, "edit", signal)));
|
|
2899
|
+
},
|
|
2900
|
+
[dispatch, now, onSend, runProducer]
|
|
2901
|
+
);
|
|
2902
|
+
const decide = useCallback6(
|
|
2903
|
+
(callId, decision, reason) => {
|
|
2904
|
+
const current = stateRef.current;
|
|
2905
|
+
const found = current ? findToolCall(current.messages, callId) : void 0;
|
|
2906
|
+
if (!current || !found || found.part.state !== "needs-approval") return Promise.resolve();
|
|
2907
|
+
dispatch({
|
|
2908
|
+
type: "tool-decision",
|
|
2909
|
+
callId,
|
|
2910
|
+
decision: decision === "approve" ? "approved" : "denied",
|
|
2911
|
+
...reason === void 0 ? {} : { reason }
|
|
2912
|
+
});
|
|
2913
|
+
if (!hasOnToolDecision) return Promise.resolve();
|
|
2914
|
+
const messageId = found.message.id;
|
|
2915
|
+
const toolName = found.part.name;
|
|
2916
|
+
const pending = threadPendingApprovals(stateRef.current?.messages ?? []).length;
|
|
2917
|
+
const task = decisionChain.current.then(async () => {
|
|
2918
|
+
if (!mounted.current) return;
|
|
2919
|
+
const active = runRef.current;
|
|
2920
|
+
if (active) {
|
|
2921
|
+
const { signal } = active.controller;
|
|
2922
|
+
try {
|
|
2923
|
+
const result = await onToolDecision({
|
|
2924
|
+
callId,
|
|
2925
|
+
toolName,
|
|
2926
|
+
decision,
|
|
2927
|
+
reason,
|
|
2928
|
+
pending,
|
|
2929
|
+
messages: stateRef.current?.messages ?? [],
|
|
2930
|
+
signal
|
|
2931
|
+
});
|
|
2932
|
+
if (result && runRef.current === active) {
|
|
2933
|
+
for await (const event of createAbortableStream(result, signal)) {
|
|
2934
|
+
if (runRef.current !== active) break;
|
|
2935
|
+
applyEvent(event);
|
|
2936
|
+
if (isTerminalStreamEvent(event)) break;
|
|
2937
|
+
}
|
|
2938
|
+
}
|
|
2939
|
+
} catch (error) {
|
|
2940
|
+
if (!signal.aborted && runRef.current === active) failRun(error);
|
|
2941
|
+
}
|
|
2942
|
+
return;
|
|
2943
|
+
}
|
|
2944
|
+
await runProducer(async (signal) => {
|
|
2945
|
+
const result = await onToolDecision({
|
|
2946
|
+
callId,
|
|
2947
|
+
toolName,
|
|
2948
|
+
decision,
|
|
2949
|
+
reason,
|
|
2950
|
+
pending,
|
|
2951
|
+
messages: stateRef.current?.messages ?? [],
|
|
2952
|
+
signal
|
|
2953
|
+
});
|
|
2954
|
+
if (result) dispatch({ type: "begin-assistant-message", id: messageId });
|
|
2955
|
+
return result;
|
|
2956
|
+
});
|
|
2957
|
+
});
|
|
2958
|
+
decisionChain.current = task.catch(() => {
|
|
2959
|
+
});
|
|
2960
|
+
return task;
|
|
2961
|
+
},
|
|
2962
|
+
[applyEvent, dispatch, failRun, hasOnToolDecision, onToolDecision, runProducer]
|
|
2963
|
+
);
|
|
2964
|
+
const approve = useCallback6((callId, reason) => decide(callId, "approve", reason), [decide]);
|
|
2965
|
+
const deny = useCallback6((callId, reason) => decide(callId, "deny", reason), [decide]);
|
|
2966
|
+
const switchBranch = useCallback6(
|
|
2967
|
+
(parentId, index) => dispatch({ type: "switch-branch", parentId, index }),
|
|
2968
|
+
[dispatch]
|
|
2969
|
+
);
|
|
2970
|
+
const dismissError = useCallback6(() => dispatch({ type: "dismiss-error" }), [dispatch]);
|
|
2971
|
+
const clear = useCallback6(() => {
|
|
2972
|
+
runRef.current?.controller.abort();
|
|
2973
|
+
runRef.current = null;
|
|
2974
|
+
dispatch({ type: "clear" });
|
|
2975
|
+
}, [dispatch]);
|
|
2976
|
+
const setMessages = useCallback6(
|
|
2977
|
+
(messages) => {
|
|
2978
|
+
runRef.current?.controller.abort();
|
|
2979
|
+
runRef.current = null;
|
|
2980
|
+
dispatch({ type: "set-messages", messages: [...messages] });
|
|
2981
|
+
},
|
|
2982
|
+
[dispatch]
|
|
2983
|
+
);
|
|
2984
|
+
useEffect7(() => {
|
|
2985
|
+
mounted.current = true;
|
|
2986
|
+
return () => {
|
|
2987
|
+
mounted.current = false;
|
|
2988
|
+
cancelFrame();
|
|
2989
|
+
runRef.current?.controller.abort();
|
|
2990
|
+
runRef.current = null;
|
|
2991
|
+
};
|
|
2992
|
+
}, [cancelFrame]);
|
|
2993
|
+
const branches = useMemo6(() => getBranchInfoMap(state), [state]);
|
|
2994
|
+
const chatProps = useMemo6(
|
|
2995
|
+
() => ({
|
|
2996
|
+
messages: state.messages,
|
|
2997
|
+
status: state.status,
|
|
2998
|
+
error: state.error,
|
|
2999
|
+
branches,
|
|
3000
|
+
onSend: (text, attachments) => {
|
|
3001
|
+
void send(text, attachments);
|
|
3002
|
+
},
|
|
3003
|
+
onStop: stop,
|
|
3004
|
+
onRegenerate: () => {
|
|
3005
|
+
void regenerate();
|
|
3006
|
+
},
|
|
3007
|
+
onToolDecision: (callId, decision, reason) => {
|
|
3008
|
+
void decide(callId, decision, reason);
|
|
3009
|
+
},
|
|
3010
|
+
onEdit: (messageId, text) => {
|
|
3011
|
+
void edit(messageId, text);
|
|
3012
|
+
},
|
|
3013
|
+
onSwitchBranch: switchBranch,
|
|
3014
|
+
onDismissError: dismissError
|
|
3015
|
+
}),
|
|
3016
|
+
[state.messages, state.status, state.error, branches, send, stop, regenerate, decide, edit, switchBranch, dismissError]
|
|
3017
|
+
);
|
|
3018
|
+
return {
|
|
3019
|
+
messages: state.messages,
|
|
3020
|
+
status: state.status,
|
|
3021
|
+
error: state.error,
|
|
3022
|
+
state,
|
|
3023
|
+
send,
|
|
3024
|
+
stop,
|
|
3025
|
+
regenerate,
|
|
3026
|
+
approve,
|
|
3027
|
+
deny,
|
|
3028
|
+
decide,
|
|
3029
|
+
edit,
|
|
3030
|
+
switchBranch,
|
|
3031
|
+
dismissError,
|
|
3032
|
+
clear,
|
|
3033
|
+
setMessages,
|
|
3034
|
+
dispatch,
|
|
3035
|
+
applyEvent,
|
|
3036
|
+
chatProps
|
|
3037
|
+
};
|
|
3038
|
+
}
|
|
3039
|
+
export {
|
|
3040
|
+
AiMessagesProvider,
|
|
3041
|
+
ApprovalPrompt,
|
|
3042
|
+
Attachments,
|
|
3043
|
+
ChatMessage,
|
|
3044
|
+
ChatThread,
|
|
3045
|
+
Citation,
|
|
3046
|
+
CodeBlock,
|
|
3047
|
+
Conversation,
|
|
3048
|
+
EmptyThread,
|
|
3049
|
+
ErrorBanner,
|
|
3050
|
+
Markdown,
|
|
3051
|
+
Message,
|
|
3052
|
+
MessageActions,
|
|
3053
|
+
PromptInput,
|
|
3054
|
+
Reasoning,
|
|
3055
|
+
SourceCard,
|
|
3056
|
+
Sources,
|
|
3057
|
+
SourcesProvider,
|
|
3058
|
+
StreamingIndicator,
|
|
3059
|
+
Suggestions,
|
|
3060
|
+
ToolCall,
|
|
3061
|
+
defaultMessages,
|
|
3062
|
+
mergeMessages,
|
|
3063
|
+
useAiMessages,
|
|
3064
|
+
useStickToBottom,
|
|
3065
|
+
useThread
|
|
3066
|
+
};
|
|
3067
|
+
//# sourceMappingURL=react.js.map
|