@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/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