@sarchauhan/chat 0.0.0-stage → 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs ADDED
@@ -0,0 +1,2247 @@
1
+ "use client";
2
+ import {
3
+ ArtifactWidget,
4
+ ArtifactsWidget,
5
+ BaseWidget,
6
+ LoadingState,
7
+ MessageContent,
8
+ PatchWidget,
9
+ QuestionWidget,
10
+ ReadFileWidget,
11
+ ResponsiveDetail,
12
+ SearchWidget,
13
+ SubagentWidget,
14
+ TextSkeleton,
15
+ TextSwap,
16
+ ThemeProvider,
17
+ ThinkingBlock,
18
+ WidgetProvider,
19
+ WidgetRenderer,
20
+ calibrateCheckPath,
21
+ clearErrorShake,
22
+ closeDropdown,
23
+ cn,
24
+ computeDiff,
25
+ computeTextDiff,
26
+ computeToolState,
27
+ computeToolWidget,
28
+ createArtifactData,
29
+ createSubagentData,
30
+ createWidgetRegistry,
31
+ defineWidget,
32
+ dropdownOrigins,
33
+ formatArtifactSize,
34
+ formatElapsed,
35
+ formatElapsedWords,
36
+ getUserDisplayText,
37
+ iconSwapStates,
38
+ motion,
39
+ motionData,
40
+ motionRoot,
41
+ normalizeSearchResults,
42
+ openDropdown,
43
+ parseArtifact,
44
+ parseSubagent,
45
+ prebuiltWidgets,
46
+ prefersReducedMotion,
47
+ readDurationMs,
48
+ safeArtifactUrl,
49
+ safeResultUrl,
50
+ successCheckStates,
51
+ swapText,
52
+ triggerErrorShake,
53
+ useDropdownPresence,
54
+ useTheme,
55
+ useWidgets
56
+ } from "./chunk-5SNRYULC.mjs";
57
+
58
+ // src/components/Chat.tsx
59
+ import { PenSquare } from "lucide-react";
60
+ import { useCallback as useCallback6, useLayoutEffect, useMemo as useMemo7, useRef as useRef10 } from "react";
61
+
62
+ // src/theme/theme.toggle.tsx
63
+ import { Moon, Sun } from "lucide-react";
64
+ import { jsx } from "react/jsx-runtime";
65
+ function ThemeToggle({ className }) {
66
+ const { resolvedTheme, toggleTheme } = useTheme();
67
+ const isDark = resolvedTheme === "dark";
68
+ return /* @__PURE__ */ jsx(
69
+ "button",
70
+ {
71
+ type: "button",
72
+ className: ["chat-theme-toggle", className].filter(Boolean).join(" "),
73
+ onClick: toggleTheme,
74
+ "aria-label": isDark ? "Switch to light theme" : "Switch to dark theme",
75
+ title: isDark ? "Light mode" : "Dark mode",
76
+ children: isDark ? /* @__PURE__ */ jsx(Sun, { size: 16, strokeWidth: 1.75 }) : /* @__PURE__ */ jsx(Moon, { size: 16, strokeWidth: 1.75 })
77
+ }
78
+ );
79
+ }
80
+
81
+ // src/components/Chat/chat.tsx
82
+ import { MessageCircle } from "lucide-react";
83
+ import { useEffect as useEffect6, useRef as useRef9, useState as useState7 } from "react";
84
+
85
+ // src/components/Chat/chat.attachments.tsx
86
+ import { FileText, Paperclip, X } from "lucide-react";
87
+ import { useRef as useRef6 } from "react";
88
+
89
+ // src/components/Chat/context/index.ts
90
+ import { useMemo as useMemo5 } from "react";
91
+
92
+ // src/components/Chat/context/composer.context.tsx
93
+ import {
94
+ createContext as createContext4,
95
+ useCallback as useCallback4,
96
+ useContext as useContext4,
97
+ useMemo as useMemo4,
98
+ useRef as useRef5,
99
+ useState as useState5
100
+ } from "react";
101
+
102
+ // src/components/Chat/chat.tooltip.tsx
103
+ import { memo, useEffect, useRef, useState } from "react";
104
+ import { createPortal } from "react-dom";
105
+ import { jsx as jsx2 } from "react/jsx-runtime";
106
+ var isEditableSelection = (selection) => {
107
+ var _a, _b;
108
+ const anchorElement = (_a = selection.anchorNode) == null ? void 0 : _a.parentElement;
109
+ const focusElement = (_b = selection.focusNode) == null ? void 0 : _b.parentElement;
110
+ return Boolean(
111
+ (anchorElement == null ? void 0 : anchorElement.closest("textarea,input,[contenteditable='true']")) || (focusElement == null ? void 0 : focusElement.closest("textarea,input,[contenteditable='true']"))
112
+ );
113
+ };
114
+ var getSelectionRect = (range) => {
115
+ const rect = range.getBoundingClientRect();
116
+ const fallbackRect = range.getClientRects()[0];
117
+ return rect.width || rect.height ? rect : fallbackRect;
118
+ };
119
+ var positionFromRange = (range, container) => {
120
+ const selectionRect = getSelectionRect(range);
121
+ if (!selectionRect) {
122
+ return null;
123
+ }
124
+ const containerRect = container.getBoundingClientRect();
125
+ return {
126
+ top: Math.max(8, selectionRect.top - containerRect.top + container.scrollTop),
127
+ left: selectionRect.left - containerRect.left + container.scrollLeft + selectionRect.width / 2
128
+ };
129
+ };
130
+ var readSelection = () => {
131
+ const selection = window.getSelection();
132
+ const text = selection == null ? void 0 : selection.toString().replace(/\s+/g, " ").trim();
133
+ if (!selection || !text || selection.rangeCount === 0 || selection.isCollapsed) {
134
+ return null;
135
+ }
136
+ if (isEditableSelection(selection)) {
137
+ return null;
138
+ }
139
+ return {
140
+ text,
141
+ range: selection.getRangeAt(0).cloneRange()
142
+ };
143
+ };
144
+ var getPortalContainer = (range) => {
145
+ const node = range.commonAncestorContainer;
146
+ const element = node instanceof Element ? node : node.parentElement;
147
+ const found = element == null ? void 0 : element.closest(".chat-messages");
148
+ return found instanceof HTMLElement ? found : null;
149
+ };
150
+ var ChatTooltip = memo(function ChatTooltip2({
151
+ onAddReference
152
+ }) {
153
+ const [selectionAction, setSelectionAction] = useState(null);
154
+ const rangeRef = useRef(null);
155
+ const textRef = useRef("");
156
+ useEffect(() => {
157
+ const clearSelectionAction = () => {
158
+ rangeRef.current = null;
159
+ textRef.current = "";
160
+ setSelectionAction(null);
161
+ };
162
+ const syncPosition = () => {
163
+ const range = rangeRef.current;
164
+ if (!range) {
165
+ return;
166
+ }
167
+ const container = getPortalContainer(range);
168
+ if (!container) {
169
+ clearSelectionAction();
170
+ return;
171
+ }
172
+ const position = positionFromRange(range, container);
173
+ if (!position) {
174
+ clearSelectionAction();
175
+ return;
176
+ }
177
+ setSelectionAction({
178
+ text: textRef.current,
179
+ top: position.top,
180
+ left: position.left,
181
+ container
182
+ });
183
+ };
184
+ const captureSelection = () => {
185
+ const next = readSelection();
186
+ if (!next) {
187
+ clearSelectionAction();
188
+ return;
189
+ }
190
+ rangeRef.current = next.range;
191
+ textRef.current = next.text;
192
+ syncPosition();
193
+ };
194
+ document.addEventListener("mouseup", captureSelection);
195
+ document.addEventListener("keyup", captureSelection);
196
+ document.addEventListener("selectionchange", captureSelection);
197
+ window.addEventListener("scroll", syncPosition, true);
198
+ window.addEventListener("resize", syncPosition);
199
+ return () => {
200
+ document.removeEventListener("mouseup", captureSelection);
201
+ document.removeEventListener("keyup", captureSelection);
202
+ document.removeEventListener("selectionchange", captureSelection);
203
+ window.removeEventListener("scroll", syncPosition, true);
204
+ window.removeEventListener("resize", syncPosition);
205
+ };
206
+ }, [onAddReference]);
207
+ if (!selectionAction) {
208
+ return null;
209
+ }
210
+ return createPortal(
211
+ /* @__PURE__ */ jsx2(
212
+ "button",
213
+ {
214
+ type: "button",
215
+ className: "chat-reference-action",
216
+ style: { top: selectionAction.top, left: selectionAction.left },
217
+ "aria-label": "Add selected text as reference",
218
+ onPointerDown: (event) => event.preventDefault(),
219
+ onClick: () => {
220
+ var _a;
221
+ onAddReference(selectionAction.text);
222
+ (_a = window.getSelection()) == null ? void 0 : _a.removeAllRanges();
223
+ rangeRef.current = null;
224
+ textRef.current = "";
225
+ setSelectionAction(null);
226
+ },
227
+ children: "Add reference"
228
+ }
229
+ ),
230
+ selectionAction.container
231
+ );
232
+ });
233
+
234
+ // src/components/Chat/context/message.helpers.ts
235
+ var normalizeReferenceText = (text) => text.replace(/\s+/g, " ").trim().slice(0, 4e3);
236
+ var createMessageId = (prefix) => {
237
+ var _a, _b, _c;
238
+ return (_c = (_b = (_a = globalThis.crypto) == null ? void 0 : _a.randomUUID) == null ? void 0 : _b.call(_a)) != null ? _c : `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2)}`;
239
+ };
240
+ var createUserMessage = (text, files = []) => ({
241
+ id: createMessageId("msg"),
242
+ role: "user",
243
+ parts: [
244
+ ...files.map((file) => ({
245
+ type: "file",
246
+ mediaType: file.mediaType,
247
+ url: file.url,
248
+ filename: file.filename
249
+ })),
250
+ ...text ? [{ type: "text", text }] : []
251
+ ]
252
+ });
253
+ var updateMessageText = (message, text) => ({
254
+ ...message,
255
+ parts: [{ type: "text", text }]
256
+ });
257
+ var upsertAssistantMessage = (current, nextMessage) => {
258
+ const existingIndex = current.findIndex((message) => message.id === nextMessage.id);
259
+ if (existingIndex === -1) {
260
+ return [...current, nextMessage];
261
+ }
262
+ return current.map((message, index) => index === existingIndex ? nextMessage : message);
263
+ };
264
+
265
+ // src/components/Chat/context/messages.context.tsx
266
+ import {
267
+ createContext as createContext3,
268
+ useCallback as useCallback3,
269
+ useContext as useContext3,
270
+ useEffect as useEffect4,
271
+ useMemo as useMemo3,
272
+ useRef as useRef4,
273
+ useState as useState4
274
+ } from "react";
275
+ import { flushSync } from "react-dom";
276
+
277
+ // src/components/Chat/context/model.context.tsx
278
+ import {
279
+ createContext,
280
+ useCallback,
281
+ useContext,
282
+ useEffect as useEffect2,
283
+ useMemo,
284
+ useRef as useRef2,
285
+ useState as useState2
286
+ } from "react";
287
+
288
+ // src/components/Chat/context/types.ts
289
+ var ProviderId = /* @__PURE__ */ ((ProviderId2) => {
290
+ ProviderId2["SYMPHONY"] = "symphony";
291
+ ProviderId2["MOCK"] = "mock";
292
+ ProviderId2["OPENAI"] = "openai";
293
+ ProviderId2["GOOGLE"] = "google";
294
+ ProviderId2["GEMINI"] = "gemini";
295
+ ProviderId2["CLAUDE"] = "anthropic";
296
+ ProviderId2["GROK"] = "grok";
297
+ ProviderId2["OLLAMA"] = "ollama";
298
+ ProviderId2["LOCAL"] = "local";
299
+ return ProviderId2;
300
+ })(ProviderId || {});
301
+ var PROVIDER_IDS = new Set(Object.values(ProviderId));
302
+ var isProviderId = (value) => typeof value === "string" && PROVIDER_IDS.has(value);
303
+ var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
304
+ var isRegistryModel = (value) => {
305
+ if (!isRecord(value) || typeof value.id !== "string" || typeof value.label !== "string") {
306
+ return false;
307
+ }
308
+ if (value.description !== void 0 && typeof value.description !== "string") {
309
+ return false;
310
+ }
311
+ if (value.thinkingLevels !== void 0 && (!Array.isArray(value.thinkingLevels) || !value.thinkingLevels.every((level) => typeof level === "string"))) {
312
+ return false;
313
+ }
314
+ return true;
315
+ };
316
+ var isRegistryProvider = (value) => isRecord(value) && typeof value.id === "string" && typeof value.label === "string" && typeof value.defaultModel === "string" && Array.isArray(value.models) && value.models.every(isRegistryModel) && (value.name === void 0 || typeof value.name === "string") && (value.logo === void 0 || typeof value.logo === "string");
317
+ var parseRegistryConfig = (value) => {
318
+ if (!isRecord(value) || typeof value.defaultProviderId !== "string" || !Array.isArray(value.providers)) {
319
+ return void 0;
320
+ }
321
+ const providers = value.providers.filter(isRegistryProvider);
322
+ if (providers.length === 0) {
323
+ return void 0;
324
+ }
325
+ return {
326
+ defaultProviderId: value.defaultProviderId,
327
+ providers
328
+ };
329
+ };
330
+ var defaultRegistry = {
331
+ defaultProviderId: "mock" /* MOCK */,
332
+ providers: [
333
+ {
334
+ id: "symphony" /* SYMPHONY */,
335
+ label: "Symphony (custom harness)",
336
+ defaultModel: "",
337
+ models: []
338
+ },
339
+ {
340
+ id: "mock" /* MOCK */,
341
+ label: "Mock",
342
+ defaultModel: "canned",
343
+ models: [{ id: "canned", label: "Canned replies" }]
344
+ },
345
+ {
346
+ id: "openai" /* OPENAI */,
347
+ label: "OpenAI",
348
+ defaultModel: "gpt-4.1",
349
+ models: [{ id: "gpt-4.1", label: "GPT-4.1" }]
350
+ },
351
+ {
352
+ id: "anthropic" /* CLAUDE */,
353
+ label: "Anthropic",
354
+ defaultModel: "claude-3-7-sonnet-20250219",
355
+ models: [{ id: "claude-3-7-sonnet-20250219", label: "Claude 3.7 Sonnet" }]
356
+ },
357
+ {
358
+ id: "google" /* GOOGLE */,
359
+ label: "Google",
360
+ defaultModel: "gemini-2.5-flash",
361
+ models: [{ id: "gemini-2.5-flash", label: "Gemini 2.5 Flash" }]
362
+ },
363
+ {
364
+ id: "ollama" /* OLLAMA */,
365
+ label: "Ollama",
366
+ defaultModel: "smallthinker:latest",
367
+ models: [{ id: "smallthinker:latest", label: "smallthinker:latest" }]
368
+ }
369
+ ]
370
+ };
371
+
372
+ // src/components/Chat/context/model.context.tsx
373
+ import { jsx as jsx3 } from "react/jsx-runtime";
374
+ var ModelContext = createContext(null);
375
+ function ModelProvider({
376
+ children,
377
+ defaultProvider = "mock" /* MOCK */,
378
+ registryUrl = "/api/ai/registry"
379
+ }) {
380
+ const [registry, setRegistry] = useState2(defaultRegistry);
381
+ const [provider, setProviderState] = useState2(defaultProvider);
382
+ const [model, setModelState] = useState2(
383
+ () => {
384
+ var _a, _b;
385
+ return (_b = (_a = defaultRegistry.providers.find((entry) => entry.id === defaultProvider)) == null ? void 0 : _a.defaultModel) != null ? _b : "";
386
+ }
387
+ );
388
+ const [thinkingLevels, setThinkingLevelsState] = useState2(() => {
389
+ const initial = {};
390
+ for (const p of defaultRegistry.providers) {
391
+ for (const m of p.models) {
392
+ const levels = m.thinkingLevels;
393
+ if (levels == null ? void 0 : levels.length) initial[`${p.id}::${m.id}`] = levels.includes("medium") ? "medium" : levels[0];
394
+ }
395
+ }
396
+ return initial;
397
+ });
398
+ const providerRef = useRef2(provider);
399
+ const modelRef = useRef2(model);
400
+ const thinkingLevelsRef = useRef2(thinkingLevels);
401
+ providerRef.current = provider;
402
+ modelRef.current = model;
403
+ thinkingLevelsRef.current = thinkingLevels;
404
+ useEffect2(() => {
405
+ let cancelled = false;
406
+ const loadRegistry = async () => {
407
+ var _a;
408
+ try {
409
+ const response = await fetch(registryUrl, { cache: "no-store" });
410
+ if (!response.ok) {
411
+ throw new Error("Failed to load registry");
412
+ }
413
+ const data = parseRegistryConfig(await response.json());
414
+ if (cancelled || !data) {
415
+ return;
416
+ }
417
+ setRegistry(data);
418
+ const nextThinking = {};
419
+ for (const p of data.providers) {
420
+ for (const m of p.models) {
421
+ const levels = m.thinkingLevels;
422
+ if (levels == null ? void 0 : levels.length) nextThinking[`${p.id}::${m.id}`] = levels.includes("medium") ? "medium" : levels[0];
423
+ }
424
+ }
425
+ setThinkingLevelsState((prev) => ({ ...nextThinking, ...prev }));
426
+ const preferred = (_a = data.providers.find((entry) => entry.id === data.defaultProviderId)) != null ? _a : data.providers[0];
427
+ if (preferred) {
428
+ setProviderState(preferred.id);
429
+ setModelState(preferred.defaultModel);
430
+ }
431
+ } catch {
432
+ if (!cancelled) {
433
+ setRegistry(defaultRegistry);
434
+ }
435
+ }
436
+ };
437
+ void loadRegistry();
438
+ return () => {
439
+ cancelled = true;
440
+ };
441
+ }, [registryUrl]);
442
+ useEffect2(() => {
443
+ var _a, _b;
444
+ const nextProvider = (_b = (_a = registry.providers.find((entry) => entry.id === provider)) != null ? _a : registry.providers.find((entry) => entry.id === registry.defaultProviderId)) != null ? _b : registry.providers[0];
445
+ if (!nextProvider) {
446
+ return;
447
+ }
448
+ const hasModel = nextProvider.models.some((entry) => entry.id === model);
449
+ if (!hasModel) {
450
+ setProviderState(nextProvider.id);
451
+ setModelState(nextProvider.defaultModel);
452
+ }
453
+ }, [model, provider, registry]);
454
+ const setProvider = useCallback((nextProvider, nextModel) => {
455
+ setProviderState(nextProvider);
456
+ if (nextModel) {
457
+ setModelState(nextModel);
458
+ }
459
+ }, []);
460
+ const getThinkingLevel = useCallback(
461
+ (providerId, modelId) => thinkingLevels[`${providerId}::${modelId}`],
462
+ [thinkingLevels]
463
+ );
464
+ const setThinkingLevel = useCallback((providerId, modelId, level) => {
465
+ setThinkingLevelsState((prev) => ({ ...prev, [`${providerId}::${modelId}`]: level }));
466
+ }, []);
467
+ const setModel = useCallback((nextModel) => {
468
+ setModelState(nextModel);
469
+ }, []);
470
+ const value = useMemo(
471
+ () => ({
472
+ registry,
473
+ provider,
474
+ model,
475
+ thinkingLevels,
476
+ getThinkingLevel,
477
+ setThinkingLevel,
478
+ setProvider,
479
+ setModel,
480
+ providerRef,
481
+ modelRef,
482
+ thinkingLevelsRef
483
+ }),
484
+ [model, provider, registry, setModel, setProvider, thinkingLevels, getThinkingLevel, setThinkingLevel]
485
+ );
486
+ return /* @__PURE__ */ jsx3(ModelContext.Provider, { value, children });
487
+ }
488
+ function useModel() {
489
+ const context = useContext(ModelContext);
490
+ if (!context) {
491
+ throw new Error("useModel must be used inside ModelProvider");
492
+ }
493
+ return context;
494
+ }
495
+
496
+ // src/components/Chat/context/thread.context.tsx
497
+ import {
498
+ createContext as createContext2,
499
+ useCallback as useCallback2,
500
+ useContext as useContext2,
501
+ useEffect as useEffect3,
502
+ useMemo as useMemo2,
503
+ useRef as useRef3,
504
+ useState as useState3
505
+ } from "react";
506
+ import { jsx as jsx4 } from "react/jsx-runtime";
507
+ var ThreadContext = createContext2(null);
508
+ function ThreadProvider({
509
+ adapter,
510
+ children,
511
+ defaultThreadId
512
+ }) {
513
+ const [threads, setThreads] = useState3([]);
514
+ const [activeThreadId, setActiveThreadId] = useState3(defaultThreadId != null ? defaultThreadId : null);
515
+ const [isLoadingThread, setIsLoadingThread] = useState3(true);
516
+ const activeThreadIdRef = useRef3(activeThreadId);
517
+ activeThreadIdRef.current = activeThreadId;
518
+ const adapterRef = useRef3(adapter);
519
+ adapterRef.current = adapter;
520
+ useEffect3(() => {
521
+ let cancelled = false;
522
+ const bootstrap = async () => {
523
+ setIsLoadingThread(true);
524
+ if (defaultThreadId) {
525
+ setThreads(
526
+ (current) => current.some((thread2) => thread2.id === defaultThreadId) ? current : [{ id: defaultThreadId, title: "New chat" }, ...current]
527
+ );
528
+ setActiveThreadId(defaultThreadId);
529
+ setIsLoadingThread(false);
530
+ return;
531
+ }
532
+ const currentAdapter = adapterRef.current;
533
+ if (!currentAdapter.listThreads || !currentAdapter.createThread) {
534
+ const localThread = { id: createMessageId("thread"), title: "New chat" };
535
+ setThreads([localThread]);
536
+ setActiveThreadId(localThread.id);
537
+ setIsLoadingThread(false);
538
+ return;
539
+ }
540
+ const nextThreads = await currentAdapter.listThreads();
541
+ if (cancelled) {
542
+ return;
543
+ }
544
+ if (nextThreads.length > 0) {
545
+ setThreads(nextThreads);
546
+ setActiveThreadId(nextThreads[0].id);
547
+ setIsLoadingThread(false);
548
+ return;
549
+ }
550
+ const thread = await currentAdapter.createThread();
551
+ if (cancelled) {
552
+ return;
553
+ }
554
+ setThreads([thread]);
555
+ setActiveThreadId(thread.id);
556
+ setIsLoadingThread(false);
557
+ };
558
+ bootstrap().catch(() => {
559
+ if (!cancelled) {
560
+ setIsLoadingThread(false);
561
+ }
562
+ });
563
+ return () => {
564
+ cancelled = true;
565
+ };
566
+ }, [defaultThreadId]);
567
+ const selectThread = useCallback2(async (threadId) => {
568
+ setActiveThreadId((current) => current === threadId ? current : threadId);
569
+ }, []);
570
+ const createThread = useCallback2(async () => {
571
+ const currentAdapter = adapterRef.current;
572
+ const thread = currentAdapter.createThread ? await currentAdapter.createThread() : { id: createMessageId("thread"), title: "New chat" };
573
+ setThreads((current) => [thread, ...current]);
574
+ setActiveThreadId(thread.id);
575
+ }, []);
576
+ const deleteThread = useCallback2(
577
+ async (threadId) => {
578
+ var _a, _b;
579
+ await ((_b = (_a = adapterRef.current).deleteThread) == null ? void 0 : _b.call(_a, threadId));
580
+ setThreads((current) => {
581
+ const remaining = current.filter((thread) => thread.id !== threadId);
582
+ setActiveThreadId((active) => {
583
+ var _a2, _b2;
584
+ if (active !== threadId) {
585
+ return active;
586
+ }
587
+ const nextActive = (_b2 = (_a2 = remaining[0]) == null ? void 0 : _a2.id) != null ? _b2 : null;
588
+ if (!nextActive) {
589
+ void createThread();
590
+ }
591
+ return nextActive;
592
+ });
593
+ return remaining;
594
+ });
595
+ },
596
+ [createThread]
597
+ );
598
+ const value = useMemo2(
599
+ () => ({
600
+ threads,
601
+ activeThreadId,
602
+ isLoadingThread,
603
+ activeThreadIdRef,
604
+ setThreads,
605
+ setIsLoadingThread,
606
+ selectThread,
607
+ createThread,
608
+ deleteThread
609
+ }),
610
+ [
611
+ activeThreadId,
612
+ createThread,
613
+ deleteThread,
614
+ isLoadingThread,
615
+ selectThread,
616
+ threads
617
+ ]
618
+ );
619
+ return /* @__PURE__ */ jsx4(ThreadContext.Provider, { value, children });
620
+ }
621
+ function useThread() {
622
+ const context = useContext2(ThreadContext);
623
+ if (!context) {
624
+ throw new Error("useThread must be used inside ThreadProvider");
625
+ }
626
+ return context;
627
+ }
628
+
629
+ // src/components/Chat/context/messages.context.tsx
630
+ import { jsx as jsx5 } from "react/jsx-runtime";
631
+ var MINIMUM_RESPONSE_LOADING_MS = 350;
632
+ var wait = (milliseconds) => new Promise((resolve) => window.setTimeout(resolve, milliseconds));
633
+ var hasVisibleAssistantContent = (message) => message.parts.some((part) => {
634
+ if (part.type === "text") {
635
+ return "text" in part && typeof part.text === "string" && part.text.trim().length > 0;
636
+ }
637
+ if (part.type === "data") {
638
+ return "name" in part && part.name !== "usage" && part.name !== "context" && part.name !== "context-warning";
639
+ }
640
+ return ["reasoning", "tool", "widget", "source-url", "source-document", "file", "unknown"].includes(part.type);
641
+ });
642
+ var MessagesContext = createContext3(null);
643
+ function MessagesProvider({ adapter, children }) {
644
+ const { activeThreadId, activeThreadIdRef, setThreads, setIsLoadingThread } = useThread();
645
+ const { providerRef, modelRef, thinkingLevelsRef } = useModel();
646
+ const [messages, setMessages] = useState4([]);
647
+ const [status, setStatus] = useState4("ready");
648
+ const [isWaitingForResponse, setIsWaitingForResponse] = useState4(false);
649
+ const messagesRef = useRef4(messages);
650
+ const statusRef = useRef4(status);
651
+ const adapterRef = useRef4(adapter);
652
+ const abortRef = useRef4(null);
653
+ messagesRef.current = messages;
654
+ statusRef.current = status;
655
+ adapterRef.current = adapter;
656
+ useEffect4(() => {
657
+ let cancelled = false;
658
+ const loadMessages = async () => {
659
+ if (!activeThreadId) {
660
+ setMessages([]);
661
+ setIsLoadingThread(false);
662
+ return;
663
+ }
664
+ const currentAdapter = adapterRef.current;
665
+ if (!currentAdapter.loadMessages) {
666
+ setIsLoadingThread(false);
667
+ return;
668
+ }
669
+ setIsLoadingThread(true);
670
+ try {
671
+ const nextMessages = await currentAdapter.loadMessages(activeThreadId);
672
+ if (cancelled) {
673
+ return;
674
+ }
675
+ setMessages(nextMessages);
676
+ setIsLoadingThread(false);
677
+ } catch {
678
+ if (!cancelled) {
679
+ setMessages([]);
680
+ setIsLoadingThread(false);
681
+ setStatus("error");
682
+ }
683
+ }
684
+ };
685
+ void loadMessages();
686
+ return () => {
687
+ cancelled = true;
688
+ };
689
+ }, [activeThreadId, setIsLoadingThread]);
690
+ const streamMessage = useCallback3(
691
+ async ({
692
+ message,
693
+ nextMessages,
694
+ provider,
695
+ model,
696
+ thinkingLevel
697
+ }) => {
698
+ var _a;
699
+ const threadId = activeThreadIdRef.current;
700
+ if (!threadId) {
701
+ setIsWaitingForResponse(false);
702
+ return;
703
+ }
704
+ (_a = abortRef.current) == null ? void 0 : _a.abort();
705
+ const abortController = new AbortController();
706
+ abortRef.current = abortController;
707
+ setStatus("submitted");
708
+ const responseStartedAt = Date.now();
709
+ let receivedVisibleResponse = false;
710
+ try {
711
+ for await (const assistantMessage of adapterRef.current.sendMessage({
712
+ threadId,
713
+ message,
714
+ messages: nextMessages,
715
+ provider,
716
+ model,
717
+ thinkingLevel,
718
+ signal: abortController.signal
719
+ })) {
720
+ if (!receivedVisibleResponse && hasVisibleAssistantContent(assistantMessage)) {
721
+ receivedVisibleResponse = true;
722
+ const remaining = MINIMUM_RESPONSE_LOADING_MS - (Date.now() - responseStartedAt);
723
+ if (remaining > 0) {
724
+ await wait(remaining);
725
+ }
726
+ if (abortController.signal.aborted) {
727
+ setIsWaitingForResponse(false);
728
+ setStatus("ready");
729
+ return;
730
+ }
731
+ }
732
+ if (receivedVisibleResponse) {
733
+ setIsWaitingForResponse(false);
734
+ setStatus("streaming");
735
+ }
736
+ setMessages((current) => upsertAssistantMessage(current, assistantMessage));
737
+ }
738
+ setStatus("ready");
739
+ } catch (error) {
740
+ setIsWaitingForResponse(false);
741
+ if (abortController.signal.aborted) {
742
+ setStatus("ready");
743
+ return;
744
+ }
745
+ setStatus("error");
746
+ throw error;
747
+ } finally {
748
+ setIsWaitingForResponse(false);
749
+ if (abortRef.current === abortController) {
750
+ abortRef.current = null;
751
+ }
752
+ }
753
+ },
754
+ [activeThreadIdRef]
755
+ );
756
+ const sendMessage = useCallback3(
757
+ async (message, options) => {
758
+ var _a, _b, _c, _d, _e;
759
+ const text = message.text.trim();
760
+ const files = (_a = message.files) != null ? _a : [];
761
+ const threadId = activeThreadIdRef.current;
762
+ if (!text && files.length === 0 || !threadId) {
763
+ return;
764
+ }
765
+ const userMessage = createUserMessage(text, files);
766
+ const nextMessages = [...messagesRef.current, userMessage];
767
+ flushSync(() => {
768
+ setMessages(nextMessages);
769
+ setIsWaitingForResponse(true);
770
+ });
771
+ const resolvedProvider = (_c = (_b = options == null ? void 0 : options.body) == null ? void 0 : _b.provider) != null ? _c : providerRef.current;
772
+ const resolvedModel = (_e = (_d = options == null ? void 0 : options.body) == null ? void 0 : _d.model) != null ? _e : modelRef.current;
773
+ const thinkingLevel = thinkingLevelsRef.current[`${resolvedProvider}::${resolvedModel}`];
774
+ await streamMessage({
775
+ message: userMessage,
776
+ nextMessages,
777
+ provider: resolvedProvider,
778
+ model: resolvedModel,
779
+ thinkingLevel
780
+ });
781
+ },
782
+ [activeThreadIdRef, modelRef, providerRef, streamMessage]
783
+ );
784
+ const editAndResendMessage = useCallback3(
785
+ async (messageId, text) => {
786
+ var _a;
787
+ const trimmedText = text.trim();
788
+ const threadId = activeThreadIdRef.current;
789
+ const isSending2 = statusRef.current === "submitted" || statusRef.current === "streaming";
790
+ if (!trimmedText || !threadId || isSending2) {
791
+ return;
792
+ }
793
+ const previousMessages = messagesRef.current;
794
+ const messageIndex = previousMessages.findIndex((message2) => message2.id === messageId);
795
+ const message = previousMessages[messageIndex];
796
+ if (!message || message.role !== "user") {
797
+ return;
798
+ }
799
+ let editedMessages = previousMessages.slice(0, messageIndex + 1).map(
800
+ (currentMessage) => currentMessage.id === messageId ? updateMessageText(currentMessage, trimmedText) : currentMessage
801
+ );
802
+ setThreads((current) => {
803
+ const firstUserMessage = editedMessages.find(
804
+ (currentMessage) => currentMessage.role === "user"
805
+ );
806
+ const firstUserTitle = firstUserMessage ? getUserDisplayText(firstUserMessage).trim().slice(0, 60) : "";
807
+ return current.map(
808
+ (thread) => thread.id === threadId && firstUserTitle ? { ...thread, title: firstUserTitle } : thread
809
+ );
810
+ });
811
+ try {
812
+ if (adapterRef.current.editMessage) {
813
+ editedMessages = await adapterRef.current.editMessage({
814
+ threadId,
815
+ messageId,
816
+ text: trimmedText
817
+ });
818
+ }
819
+ const editedMessage = (_a = editedMessages.find((currentMessage) => currentMessage.id === messageId)) != null ? _a : updateMessageText(message, trimmedText);
820
+ setMessages(editedMessages);
821
+ setIsWaitingForResponse(true);
822
+ const thinkingLevel = thinkingLevelsRef.current[`${providerRef.current}::${modelRef.current}`];
823
+ await streamMessage({
824
+ message: editedMessage,
825
+ nextMessages: editedMessages,
826
+ provider: providerRef.current,
827
+ model: modelRef.current,
828
+ thinkingLevel
829
+ });
830
+ } catch (error) {
831
+ setIsWaitingForResponse(false);
832
+ setMessages(previousMessages);
833
+ throw error;
834
+ }
835
+ },
836
+ [activeThreadIdRef, modelRef, providerRef, setThreads, streamMessage]
837
+ );
838
+ const stopResponse = useCallback3(() => {
839
+ var _a;
840
+ const isSending2 = statusRef.current === "submitted" || statusRef.current === "streaming";
841
+ if (!isSending2) {
842
+ return;
843
+ }
844
+ (_a = abortRef.current) == null ? void 0 : _a.abort();
845
+ setIsWaitingForResponse(false);
846
+ setStatus("ready");
847
+ }, []);
848
+ const isSending = status === "submitted" || status === "streaming";
849
+ const value = useMemo3(
850
+ () => ({
851
+ messages,
852
+ status,
853
+ isSending,
854
+ isWaitingForResponse,
855
+ sendMessage,
856
+ editAndResendMessage,
857
+ stopResponse
858
+ }),
859
+ [editAndResendMessage, isSending, isWaitingForResponse, messages, sendMessage, status, stopResponse]
860
+ );
861
+ return /* @__PURE__ */ jsx5(MessagesContext.Provider, { value, children });
862
+ }
863
+ function useMessages() {
864
+ const context = useContext3(MessagesContext);
865
+ if (!context) {
866
+ throw new Error("useMessages must be used inside MessagesProvider");
867
+ }
868
+ return context;
869
+ }
870
+
871
+ // src/components/Chat/context/composer.context.tsx
872
+ import { jsx as jsx6, jsxs } from "react/jsx-runtime";
873
+ var readFileAsDataUrl = (file) => new Promise((resolve, reject) => {
874
+ const reader = new FileReader();
875
+ reader.onload = () => {
876
+ if (typeof reader.result === "string") {
877
+ resolve(reader.result);
878
+ return;
879
+ }
880
+ reject(new Error("Could not read file"));
881
+ };
882
+ reader.onerror = () => {
883
+ var _a;
884
+ reject((_a = reader.error) != null ? _a : new Error("Could not read file"));
885
+ };
886
+ reader.readAsDataURL(file);
887
+ });
888
+ var ComposerContext = createContext4(null);
889
+ function ComposerProvider({ children }) {
890
+ const { activeThreadId, activeThreadIdRef, isLoadingThread, setThreads } = useThread();
891
+ const { providerRef, modelRef } = useModel();
892
+ const { sendMessage, isSending } = useMessages();
893
+ const [input, setInputState] = useState5("");
894
+ const [references, setReferences] = useState5([]);
895
+ const [attachments, setAttachments] = useState5([]);
896
+ const inputRef = useRef5(input);
897
+ const referencesRef = useRef5(references);
898
+ const attachmentsRef = useRef5(attachments);
899
+ inputRef.current = input;
900
+ referencesRef.current = references;
901
+ attachmentsRef.current = attachments;
902
+ const setInput = useCallback4((value2) => {
903
+ setInputState(value2);
904
+ }, []);
905
+ const addReference = useCallback4((text) => {
906
+ const normalized = normalizeReferenceText(text);
907
+ if (!normalized) {
908
+ return;
909
+ }
910
+ setReferences((current) => {
911
+ if (current.some((reference) => reference.text === normalized)) {
912
+ return current;
913
+ }
914
+ return [
915
+ ...current,
916
+ {
917
+ id: createMessageId("ref"),
918
+ text: normalized
919
+ }
920
+ ];
921
+ });
922
+ }, []);
923
+ const removeReference = useCallback4((id) => {
924
+ setReferences((current) => current.filter((reference) => reference.id !== id));
925
+ }, []);
926
+ const clearReferences = useCallback4(() => {
927
+ setReferences([]);
928
+ }, []);
929
+ const addFiles = useCallback4(async (list) => {
930
+ const incoming = Array.from(list);
931
+ const next = [];
932
+ for (const file of incoming) {
933
+ try {
934
+ const url = await readFileAsDataUrl(file);
935
+ next.push({
936
+ id: createMessageId("file"),
937
+ filename: file.name || "file",
938
+ mediaType: file.type || "application/octet-stream",
939
+ url
940
+ });
941
+ } catch {
942
+ }
943
+ }
944
+ if (next.length === 0) {
945
+ return;
946
+ }
947
+ setAttachments((current) => [...current, ...next]);
948
+ }, []);
949
+ const removeAttachment = useCallback4((id) => {
950
+ setAttachments((current) => current.filter((attachment) => attachment.id !== id));
951
+ }, []);
952
+ const disabled = isSending || isLoadingThread || !activeThreadId;
953
+ const canSend = !disabled && (!!input.trim() || attachments.length > 0);
954
+ const disabledRef = useRef5(disabled);
955
+ disabledRef.current = disabled;
956
+ const submitInput = useCallback4(async () => {
957
+ var _a;
958
+ const text = inputRef.current.trim();
959
+ const threadId = activeThreadIdRef.current;
960
+ const attachmentsSnapshot = attachmentsRef.current;
961
+ if (!text && attachmentsSnapshot.length === 0 || !threadId || disabledRef.current) {
962
+ return;
963
+ }
964
+ const nextTitle = (text || ((_a = attachmentsSnapshot[0]) == null ? void 0 : _a.filename) || "").slice(0, 60);
965
+ const referencesSnapshot = referencesRef.current;
966
+ const referenceText = referencesSnapshot.map(
967
+ (reference, index) => `<reference ${index + 1}>
968
+ ${reference.text}
969
+ </reference ${index + 1}>`
970
+ ).join("\n\n");
971
+ const messageText = referenceText ? `Use the following selected references as context:
972
+
973
+ ${referenceText}
974
+
975
+ User message:
976
+ ${text}` : text;
977
+ setInputState("");
978
+ setReferences([]);
979
+ setAttachments([]);
980
+ setThreads((current) => {
981
+ const next = current.map(
982
+ (thread) => thread.id === threadId && thread.title === "New chat" ? { ...thread, title: nextTitle || thread.title } : thread
983
+ );
984
+ const selected = next.find((thread) => thread.id === threadId);
985
+ const remaining = next.filter((thread) => thread.id !== threadId);
986
+ return selected ? [selected, ...remaining] : next;
987
+ });
988
+ try {
989
+ await sendMessage(
990
+ {
991
+ text: messageText,
992
+ files: attachmentsSnapshot.map((file) => ({
993
+ mediaType: file.mediaType,
994
+ url: file.url,
995
+ filename: file.filename
996
+ }))
997
+ },
998
+ {
999
+ body: {
1000
+ provider: providerRef.current,
1001
+ model: modelRef.current
1002
+ }
1003
+ }
1004
+ );
1005
+ } catch (error) {
1006
+ setInputState(text);
1007
+ setReferences(referencesSnapshot);
1008
+ setAttachments(attachmentsSnapshot);
1009
+ throw error;
1010
+ }
1011
+ }, [activeThreadIdRef, modelRef, providerRef, sendMessage, setThreads]);
1012
+ const value = useMemo4(
1013
+ () => ({
1014
+ input,
1015
+ setInput,
1016
+ references,
1017
+ addReference,
1018
+ removeReference,
1019
+ clearReferences,
1020
+ attachments,
1021
+ addFiles,
1022
+ removeAttachment,
1023
+ disabled,
1024
+ canSend,
1025
+ submitInput
1026
+ }),
1027
+ [
1028
+ addReference,
1029
+ canSend,
1030
+ clearReferences,
1031
+ disabled,
1032
+ input,
1033
+ addFiles,
1034
+ attachments,
1035
+ references,
1036
+ removeAttachment,
1037
+ removeReference,
1038
+ setInput,
1039
+ submitInput
1040
+ ]
1041
+ );
1042
+ return /* @__PURE__ */ jsxs(ComposerContext.Provider, { value, children: [
1043
+ children,
1044
+ /* @__PURE__ */ jsx6(ChatTooltip, { onAddReference: addReference })
1045
+ ] });
1046
+ }
1047
+ function useComposer() {
1048
+ const context = useContext4(ComposerContext);
1049
+ if (!context) {
1050
+ throw new Error("useComposer must be used inside ComposerProvider");
1051
+ }
1052
+ return context;
1053
+ }
1054
+
1055
+ // src/components/Chat/context/chat.provider.tsx
1056
+ import { jsx as jsx7 } from "react/jsx-runtime";
1057
+ function ChatContextProvider({
1058
+ adapter,
1059
+ children,
1060
+ defaultProvider = "mock" /* MOCK */,
1061
+ defaultThreadId,
1062
+ registryUrl = "/api/ai/registry"
1063
+ }) {
1064
+ return /* @__PURE__ */ jsx7(ModelProvider, { defaultProvider, registryUrl, children: /* @__PURE__ */ jsx7(ThreadProvider, { adapter, defaultThreadId, children: /* @__PURE__ */ jsx7(MessagesProvider, { adapter, children: /* @__PURE__ */ jsx7(ComposerProvider, { children }) }) }) });
1065
+ }
1066
+
1067
+ // src/components/Chat/context/index.ts
1068
+ function useChat() {
1069
+ const composer = useComposer();
1070
+ const messages = useMessages();
1071
+ const model = useModel();
1072
+ const thread = useThread();
1073
+ return useMemo5(() => {
1074
+ const dispatch = (action) => {
1075
+ switch (action.type) {
1076
+ case "setInput":
1077
+ composer.setInput(action.data.input);
1078
+ break;
1079
+ case "setProvider":
1080
+ model.setProvider(action.data.provider, action.data.model);
1081
+ break;
1082
+ case "setModel":
1083
+ model.setModel(action.data.model);
1084
+ break;
1085
+ case "addReference":
1086
+ composer.addReference(action.data.text);
1087
+ break;
1088
+ case "removeReference":
1089
+ composer.removeReference(action.data.id);
1090
+ break;
1091
+ case "clearReferences":
1092
+ composer.clearReferences();
1093
+ break;
1094
+ default:
1095
+ break;
1096
+ }
1097
+ };
1098
+ return {
1099
+ state: {
1100
+ input: composer.input,
1101
+ provider: model.provider,
1102
+ model: model.model,
1103
+ references: composer.references,
1104
+ disabled: composer.disabled,
1105
+ sendDisabled: !composer.canSend
1106
+ },
1107
+ dispatch,
1108
+ messages: messages.messages,
1109
+ status: messages.status,
1110
+ activeThreadId: thread.activeThreadId,
1111
+ threads: thread.threads,
1112
+ isLoadingThread: thread.isLoadingThread,
1113
+ registry: model.registry,
1114
+ sendMessage: messages.sendMessage,
1115
+ submitInput: composer.submitInput,
1116
+ editAndResendMessage: messages.editAndResendMessage,
1117
+ stopResponse: messages.stopResponse,
1118
+ selectThread: thread.selectThread,
1119
+ createThread: thread.createThread,
1120
+ deleteThread: thread.deleteThread
1121
+ };
1122
+ }, [composer, messages, model, thread]);
1123
+ }
1124
+
1125
+ // src/components/Chat/chat.attachments.tsx
1126
+ import { Fragment, jsx as jsx8, jsxs as jsxs2 } from "react/jsx-runtime";
1127
+ var isImageMediaType = (mediaType) => mediaType.startsWith("image/");
1128
+ var ChatAttachments = () => {
1129
+ const { attachments, removeAttachment } = useComposer();
1130
+ if (attachments.length === 0) {
1131
+ return null;
1132
+ }
1133
+ return /* @__PURE__ */ jsx8("div", { className: "chat-attachments", children: attachments.map((attachment) => {
1134
+ const image = isImageMediaType(attachment.mediaType);
1135
+ return /* @__PURE__ */ jsxs2(
1136
+ "div",
1137
+ {
1138
+ className: image ? "chat-attachment chat-attachment-image" : "chat-attachment chat-attachment-file",
1139
+ children: [
1140
+ image ? /* @__PURE__ */ jsx8("img", { src: attachment.url, alt: attachment.filename }) : /* @__PURE__ */ jsxs2(Fragment, { children: [
1141
+ /* @__PURE__ */ jsx8(FileText, { size: 14, "aria-hidden": "true" }),
1142
+ /* @__PURE__ */ jsx8("span", { className: "chat-attachment-name", children: attachment.filename })
1143
+ ] }),
1144
+ /* @__PURE__ */ jsx8(
1145
+ "button",
1146
+ {
1147
+ type: "button",
1148
+ className: "chat-attachment-remove",
1149
+ "aria-label": `Remove ${attachment.filename}`,
1150
+ onClick: () => removeAttachment(attachment.id),
1151
+ children: /* @__PURE__ */ jsx8(X, { size: 12 })
1152
+ }
1153
+ )
1154
+ ]
1155
+ },
1156
+ attachment.id
1157
+ );
1158
+ }) });
1159
+ };
1160
+ var ChatAttachButton = () => {
1161
+ const { disabled, addFiles } = useComposer();
1162
+ const inputRef = useRef6(null);
1163
+ return /* @__PURE__ */ jsxs2(Fragment, { children: [
1164
+ /* @__PURE__ */ jsx8(
1165
+ "button",
1166
+ {
1167
+ type: "button",
1168
+ className: "chat-attach",
1169
+ "aria-label": "Add attachment",
1170
+ disabled,
1171
+ onClick: () => {
1172
+ var _a;
1173
+ return (_a = inputRef.current) == null ? void 0 : _a.click();
1174
+ },
1175
+ children: /* @__PURE__ */ jsx8(Paperclip, { size: 16 })
1176
+ }
1177
+ ),
1178
+ /* @__PURE__ */ jsx8(
1179
+ "input",
1180
+ {
1181
+ ref: inputRef,
1182
+ className: "chat-attach-input",
1183
+ type: "file",
1184
+ multiple: true,
1185
+ tabIndex: -1,
1186
+ "aria-hidden": "true",
1187
+ onChange: (event) => {
1188
+ const files = event.currentTarget.files;
1189
+ if (files && files.length > 0) {
1190
+ void addFiles(files);
1191
+ }
1192
+ event.currentTarget.value = "";
1193
+ }
1194
+ }
1195
+ )
1196
+ ] });
1197
+ };
1198
+
1199
+ // src/components/Chat/chat.input.tsx
1200
+ import { useEffect as useEffect5, useRef as useRef7 } from "react";
1201
+ import { jsx as jsx9 } from "react/jsx-runtime";
1202
+ var ChatInput = ({
1203
+ placeholder = "Message...",
1204
+ expanded = true
1205
+ }) => {
1206
+ const { input, setInput, disabled, submitInput, attachments } = useComposer();
1207
+ const ref = useRef7(null);
1208
+ const hasDraft = input.trim().length > 0 || attachments.length > 0;
1209
+ useEffect5(() => {
1210
+ const el = ref.current;
1211
+ if (!el) return;
1212
+ if (!expanded) {
1213
+ el.style.height = "";
1214
+ el.style.overflowY = "hidden";
1215
+ return;
1216
+ }
1217
+ el.style.height = "auto";
1218
+ const shell = el.closest(".chat-composer-shell");
1219
+ const isSmall = (shell == null ? void 0 : shell.dataset.variant) === "small";
1220
+ const max = isSmall ? 128 : 320;
1221
+ el.style.height = `${Math.min(el.scrollHeight, max)}px`;
1222
+ el.style.overflowY = el.scrollHeight > max ? "auto" : "hidden";
1223
+ }, [expanded, input]);
1224
+ return /* @__PURE__ */ jsx9("div", { className: "chat-composer-input", children: /* @__PURE__ */ jsx9(
1225
+ "textarea",
1226
+ {
1227
+ ref,
1228
+ value: input,
1229
+ onChange: (event) => {
1230
+ const wrap = event.currentTarget.closest(`.${motion.inputWrap}`);
1231
+ if (wrap) {
1232
+ clearErrorShake(wrap);
1233
+ }
1234
+ setInput(event.target.value);
1235
+ },
1236
+ placeholder,
1237
+ disabled,
1238
+ rows: 1,
1239
+ onKeyDown: (event) => {
1240
+ if (event.key !== "Enter" || event.shiftKey) return;
1241
+ if (event.nativeEvent.isComposing) return;
1242
+ event.preventDefault();
1243
+ if (disabled) {
1244
+ return;
1245
+ }
1246
+ if (!hasDraft) {
1247
+ const wrap = event.currentTarget.closest(`.${motion.inputWrap}`);
1248
+ if (wrap) {
1249
+ triggerErrorShake(wrap);
1250
+ }
1251
+ return;
1252
+ }
1253
+ void submitInput();
1254
+ }
1255
+ }
1256
+ ) });
1257
+ };
1258
+
1259
+ // src/components/Chat/chat.references.tsx
1260
+ import { X as X2 } from "lucide-react";
1261
+ import { jsx as jsx10, jsxs as jsxs3 } from "react/jsx-runtime";
1262
+ var ChatReferences = () => {
1263
+ const { references, removeReference } = useComposer();
1264
+ if (references.length === 0) {
1265
+ return null;
1266
+ }
1267
+ return /* @__PURE__ */ jsx10("div", { className: "chat-references", children: references.map((reference, index) => /* @__PURE__ */ jsxs3("div", { className: "chat-reference", title: reference.text, children: [
1268
+ /* @__PURE__ */ jsx10("span", { className: "chat-reference-text", children: reference.text }),
1269
+ /* @__PURE__ */ jsx10(
1270
+ "button",
1271
+ {
1272
+ type: "button",
1273
+ className: "chat-icon-button",
1274
+ "aria-label": `Remove reference ${index + 1}`,
1275
+ onClick: () => removeReference(reference.id),
1276
+ children: /* @__PURE__ */ jsx10(X2, { size: 12 })
1277
+ }
1278
+ )
1279
+ ] }, reference.id)) });
1280
+ };
1281
+
1282
+ // src/components/Chat/chat.send.tsx
1283
+ import { Send, Square } from "lucide-react";
1284
+ import { jsx as jsx11 } from "react/jsx-runtime";
1285
+ var ChatSend = () => {
1286
+ const { canSend, submitInput } = useComposer();
1287
+ const { status, stopResponse, isSending } = useMessages();
1288
+ return /* @__PURE__ */ jsx11(
1289
+ "button",
1290
+ {
1291
+ className: "chat-send",
1292
+ type: "button",
1293
+ "data-state": isSending ? "stop" : "send",
1294
+ onClick: isSending ? stopResponse : submitInput,
1295
+ disabled: isSending ? false : !canSend,
1296
+ "aria-label": isSending ? "Stop response" : "Send message",
1297
+ children: isSending ? /* @__PURE__ */ jsx11(Square, { size: 14, fill: "currentColor" }) : /* @__PURE__ */ jsx11(Send, { size: 16 })
1298
+ }
1299
+ );
1300
+ };
1301
+
1302
+ // src/components/Chat/chat.select.tsx
1303
+ import { Fragment as Fragment2, useCallback as useCallback5, useMemo as useMemo6, useRef as useRef8, useState as useState6 } from "react";
1304
+
1305
+ // src/ui/select.tsx
1306
+ import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react";
1307
+ import { Select as SelectPrimitive } from "radix-ui";
1308
+ import { jsx as jsx12, jsxs as jsxs4 } from "react/jsx-runtime";
1309
+ function Select({
1310
+ ...props
1311
+ }) {
1312
+ return /* @__PURE__ */ jsx12(SelectPrimitive.Root, { "data-slot": "select", ...props });
1313
+ }
1314
+ function SelectGroup({
1315
+ ...props
1316
+ }) {
1317
+ return /* @__PURE__ */ jsx12(SelectPrimitive.Group, { "data-slot": "select-group", ...props });
1318
+ }
1319
+ function SelectTrigger({
1320
+ className,
1321
+ size = "default",
1322
+ children,
1323
+ ...props
1324
+ }) {
1325
+ return /* @__PURE__ */ jsxs4(
1326
+ SelectPrimitive.Trigger,
1327
+ {
1328
+ "data-slot": "select-trigger",
1329
+ "data-size": size,
1330
+ className: cn(
1331
+ "border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
1332
+ className
1333
+ ),
1334
+ ...props,
1335
+ children: [
1336
+ children,
1337
+ /* @__PURE__ */ jsx12(SelectPrimitive.Icon, { asChild: true, children: /* @__PURE__ */ jsx12(ChevronDownIcon, { className: "size-4 opacity-50" }) })
1338
+ ]
1339
+ }
1340
+ );
1341
+ }
1342
+ function SelectContent({
1343
+ className,
1344
+ children,
1345
+ portalContainer,
1346
+ position = "item-aligned",
1347
+ align = "center",
1348
+ ...props
1349
+ }) {
1350
+ return /* @__PURE__ */ jsx12(SelectPrimitive.Portal, { container: portalContainer, children: /* @__PURE__ */ jsxs4(
1351
+ SelectPrimitive.Content,
1352
+ {
1353
+ "data-slot": "select-content",
1354
+ className: cn(
1355
+ "bg-popover text-popover-foreground relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
1356
+ position === "popper" && "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
1357
+ className
1358
+ ),
1359
+ position,
1360
+ align,
1361
+ ...props,
1362
+ children: [
1363
+ /* @__PURE__ */ jsx12(SelectScrollUpButton, {}),
1364
+ /* @__PURE__ */ jsx12(
1365
+ SelectPrimitive.Viewport,
1366
+ {
1367
+ "data-slot": "select-viewport",
1368
+ className: cn(
1369
+ "p-1",
1370
+ position === "popper" && "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1"
1371
+ ),
1372
+ children
1373
+ }
1374
+ ),
1375
+ /* @__PURE__ */ jsx12(SelectScrollDownButton, {})
1376
+ ]
1377
+ }
1378
+ ) });
1379
+ }
1380
+ function SelectLabel({
1381
+ className,
1382
+ ...props
1383
+ }) {
1384
+ return /* @__PURE__ */ jsx12(
1385
+ SelectPrimitive.Label,
1386
+ {
1387
+ "data-slot": "select-label",
1388
+ className: cn("text-muted-foreground px-2 py-1.5 text-xs", className),
1389
+ ...props
1390
+ }
1391
+ );
1392
+ }
1393
+ function SelectItem({
1394
+ className,
1395
+ children,
1396
+ ...props
1397
+ }) {
1398
+ return /* @__PURE__ */ jsxs4(
1399
+ SelectPrimitive.Item,
1400
+ {
1401
+ "data-slot": "select-item",
1402
+ className: cn(
1403
+ "focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
1404
+ className
1405
+ ),
1406
+ ...props,
1407
+ children: [
1408
+ /* @__PURE__ */ jsx12(
1409
+ "span",
1410
+ {
1411
+ "data-slot": "select-item-indicator",
1412
+ className: "absolute right-2 flex size-3.5 items-center justify-center",
1413
+ children: /* @__PURE__ */ jsx12(SelectPrimitive.ItemIndicator, { children: /* @__PURE__ */ jsx12(CheckIcon, { className: "size-4" }) })
1414
+ }
1415
+ ),
1416
+ /* @__PURE__ */ jsx12(SelectPrimitive.ItemText, { children })
1417
+ ]
1418
+ }
1419
+ );
1420
+ }
1421
+ function SelectScrollUpButton({
1422
+ className,
1423
+ ...props
1424
+ }) {
1425
+ return /* @__PURE__ */ jsx12(
1426
+ SelectPrimitive.ScrollUpButton,
1427
+ {
1428
+ "data-slot": "select-scroll-up-button",
1429
+ className: cn(
1430
+ "flex cursor-default items-center justify-center py-1",
1431
+ className
1432
+ ),
1433
+ ...props,
1434
+ children: /* @__PURE__ */ jsx12(ChevronUpIcon, { className: "size-4" })
1435
+ }
1436
+ );
1437
+ }
1438
+ function SelectScrollDownButton({
1439
+ className,
1440
+ ...props
1441
+ }) {
1442
+ return /* @__PURE__ */ jsx12(
1443
+ SelectPrimitive.ScrollDownButton,
1444
+ {
1445
+ "data-slot": "select-scroll-down-button",
1446
+ className: cn(
1447
+ "flex cursor-default items-center justify-center py-1",
1448
+ className
1449
+ ),
1450
+ ...props,
1451
+ children: /* @__PURE__ */ jsx12(ChevronDownIcon, { className: "size-4" })
1452
+ }
1453
+ );
1454
+ }
1455
+
1456
+ // src/components/Chat/chat.select.tsx
1457
+ import { Check, Ellipsis } from "lucide-react";
1458
+ import { Fragment as Fragment3, jsx as jsx13, jsxs as jsxs5 } from "react/jsx-runtime";
1459
+ var SELECT_ORIGIN = "top-left";
1460
+ var THINKING_ORIGIN = "top-right";
1461
+ var ProviderBadge = ({ logo, name }) => {
1462
+ if (logo) {
1463
+ return /* @__PURE__ */ jsx13(
1464
+ "img",
1465
+ {
1466
+ className: "chat-model-provider-logo",
1467
+ src: logo,
1468
+ alt: "",
1469
+ "aria-hidden": "true"
1470
+ }
1471
+ );
1472
+ }
1473
+ return /* @__PURE__ */ jsx13("span", { className: "chat-model-provider-logo chat-model-provider-fallback", "aria-hidden": "true", children: name.slice(0, 1) });
1474
+ };
1475
+ var ChatSelect = () => {
1476
+ var _a, _b, _c;
1477
+ const { provider, model, registry, setProvider, getThinkingLevel, setThinkingLevel } = useModel();
1478
+ const { isSending } = useMessages();
1479
+ const [portalContainer, setPortalContainer] = useState6(null);
1480
+ const [query, setQuery] = useState6("");
1481
+ const [thinkingOpen, setThinkingOpen] = useState6(null);
1482
+ const [menuOpen, setMenuOpen] = useState6(false);
1483
+ const rootRef = useRef8(null);
1484
+ const menu = useDropdownPresence(menuOpen, rootRef);
1485
+ const selectedValue = `${provider}::${model}`;
1486
+ const selectedProvider = registry.providers.find((entry) => entry.id === provider);
1487
+ const selectedModel = selectedProvider == null ? void 0 : selectedProvider.models.find((entry) => entry.id === model);
1488
+ const providerName = (_b = (_a = selectedProvider == null ? void 0 : selectedProvider.name) != null ? _a : selectedProvider == null ? void 0 : selectedProvider.label) != null ? _b : provider;
1489
+ const modelLabel = (_c = selectedModel == null ? void 0 : selectedModel.label) != null ? _c : model || "Loading models\u2026";
1490
+ const filteredProviders = useMemo6(() => {
1491
+ const q = query.trim().toLowerCase();
1492
+ if (!q) return registry.providers;
1493
+ return registry.providers.map((entry) => ({
1494
+ ...entry,
1495
+ models: entry.models.filter(
1496
+ (item) => item.label.toLowerCase().includes(q) || item.id.toLowerCase().includes(q) || item.description && item.description.toLowerCase().includes(q) || entry.label.toLowerCase().includes(q)
1497
+ )
1498
+ })).filter((entry) => entry.models.length > 0);
1499
+ }, [registry.providers, query]);
1500
+ const setRootElement = useCallback5((node) => {
1501
+ const found = node == null ? void 0 : node.closest(".chat-root");
1502
+ rootRef.current = found instanceof HTMLElement ? found : null;
1503
+ setPortalContainer(found instanceof HTMLElement ? found : null);
1504
+ }, []);
1505
+ return /* @__PURE__ */ jsx13("div", { ref: setRootElement, children: /* @__PURE__ */ jsxs5(
1506
+ Select,
1507
+ {
1508
+ open: menuOpen || menu.mounted,
1509
+ onOpenChange: setMenuOpen,
1510
+ value: selectedValue,
1511
+ onValueChange: (value) => {
1512
+ const separator = value.indexOf("::");
1513
+ if (separator <= 0) {
1514
+ return;
1515
+ }
1516
+ const nextProvider = value.slice(0, separator);
1517
+ const nextModel = value.slice(separator + 2);
1518
+ const match = registry.providers.find((entry) => entry.id === nextProvider);
1519
+ if (!match || !nextModel) {
1520
+ return;
1521
+ }
1522
+ setProvider(match.id, nextModel);
1523
+ },
1524
+ disabled: isSending,
1525
+ children: [
1526
+ /* @__PURE__ */ jsx13(SelectTrigger, { className: "chat-model-trigger", children: /* @__PURE__ */ jsxs5("span", { className: "chat-model-selected", children: [
1527
+ selectedModel ? /* @__PURE__ */ jsx13(ProviderBadge, { logo: selectedProvider == null ? void 0 : selectedProvider.logo, name: providerName }) : null,
1528
+ /* @__PURE__ */ jsx13("span", { children: modelLabel })
1529
+ ] }) }),
1530
+ /* @__PURE__ */ jsxs5(
1531
+ SelectContent,
1532
+ {
1533
+ align: "start",
1534
+ position: "popper",
1535
+ sideOffset: 6,
1536
+ collisionPadding: 12,
1537
+ className: cn("chat-model-content", motion.dropdown, menu.className),
1538
+ "data-origin": SELECT_ORIGIN,
1539
+ portalContainer,
1540
+ onCloseAutoFocus: (event) => {
1541
+ event.preventDefault();
1542
+ setQuery("");
1543
+ setThinkingOpen(null);
1544
+ },
1545
+ onPointerDownOutside: () => setThinkingOpen(null),
1546
+ children: [
1547
+ /* @__PURE__ */ jsx13("div", { className: "chat-model-search", children: /* @__PURE__ */ jsx13(
1548
+ "input",
1549
+ {
1550
+ autoFocus: true,
1551
+ placeholder: "Search models",
1552
+ value: query,
1553
+ onChange: (event) => setQuery(event.target.value),
1554
+ onKeyDown: (event) => event.stopPropagation(),
1555
+ className: "chat-model-search-input"
1556
+ }
1557
+ ) }),
1558
+ /* @__PURE__ */ jsx13("div", { className: "chat-model-list", children: filteredProviders.length === 0 ? /* @__PURE__ */ jsx13("div", { className: "chat-model-empty", children: "No models found" }) : filteredProviders.map((entry) => {
1559
+ var _a2;
1560
+ const providerName2 = (_a2 = entry.name) != null ? _a2 : entry.label;
1561
+ const hideLabel = filteredProviders.length === 1;
1562
+ return /* @__PURE__ */ jsx13(Fragment2, { children: /* @__PURE__ */ jsxs5(SelectGroup, { className: "chat-model-group", children: [
1563
+ hideLabel ? null : /* @__PURE__ */ jsxs5(SelectLabel, { className: "chat-model-label", children: [
1564
+ /* @__PURE__ */ jsx13(ProviderBadge, { logo: entry.logo, name: providerName2 }),
1565
+ /* @__PURE__ */ jsx13("span", { children: providerName2 })
1566
+ ] }),
1567
+ entry.models.map((item) => {
1568
+ var _a3;
1569
+ const key = `${entry.id}::${item.id}`;
1570
+ const levels = item.thinkingLevels;
1571
+ const defaultThinking = levels && levels.length > 0 ? levels.includes("medium") ? "medium" : levels[0] : void 0;
1572
+ const thinking = defaultThinking && levels ? { levels } : void 0;
1573
+ const activeThinking = defaultThinking ? (_a3 = getThinkingLevel(entry.id, item.id)) != null ? _a3 : defaultThinking : void 0;
1574
+ const isOpen = thinkingOpen === key;
1575
+ return /* @__PURE__ */ jsx13(
1576
+ ModelRow,
1577
+ {
1578
+ itemKey: key,
1579
+ item,
1580
+ thinking,
1581
+ activeThinking,
1582
+ isOpen,
1583
+ onToggleThinking: () => setThinkingOpen(isOpen ? null : key),
1584
+ onPickThinking: (level) => {
1585
+ setThinkingLevel(entry.id, item.id, level);
1586
+ setThinkingOpen(null);
1587
+ }
1588
+ },
1589
+ key
1590
+ );
1591
+ })
1592
+ ] }) }, entry.id);
1593
+ }) })
1594
+ ]
1595
+ }
1596
+ )
1597
+ ]
1598
+ }
1599
+ ) });
1600
+ };
1601
+ var ModelRow = ({
1602
+ itemKey,
1603
+ item,
1604
+ thinking,
1605
+ activeThinking,
1606
+ isOpen,
1607
+ onToggleThinking,
1608
+ onPickThinking
1609
+ }) => {
1610
+ const menu = useDropdownPresence(isOpen);
1611
+ return /* @__PURE__ */ jsxs5("div", { className: "chat-model-row", children: [
1612
+ /* @__PURE__ */ jsxs5(SelectItem, { value: itemKey, className: "chat-model-item", children: [
1613
+ /* @__PURE__ */ jsx13("span", { className: "chat-model-item-label", children: item.label }),
1614
+ item.description ? /* @__PURE__ */ jsx13("span", { className: "chat-model-item-hint", children: item.description }) : thinking && activeThinking ? /* @__PURE__ */ jsx13("span", { className: "chat-model-item-hint", children: activeThinking }) : null
1615
+ ] }),
1616
+ thinking ? /* @__PURE__ */ jsxs5(Fragment3, { children: [
1617
+ /* @__PURE__ */ jsx13(
1618
+ "button",
1619
+ {
1620
+ type: "button",
1621
+ "aria-label": "Thinking settings",
1622
+ className: "chat-model-thinking-trigger",
1623
+ "data-open": isOpen ? "true" : "false",
1624
+ onPointerDown: (event) => {
1625
+ event.preventDefault();
1626
+ event.stopPropagation();
1627
+ },
1628
+ onClick: (event) => {
1629
+ event.preventDefault();
1630
+ event.stopPropagation();
1631
+ onToggleThinking();
1632
+ },
1633
+ children: /* @__PURE__ */ jsx13(Ellipsis, { className: "chat-model-thinking-icon" })
1634
+ }
1635
+ ),
1636
+ menu.mounted ? /* @__PURE__ */ jsx13(
1637
+ "div",
1638
+ {
1639
+ className: cn("chat-model-thinking-menu", motion.dropdown, menu.className),
1640
+ "data-origin": THINKING_ORIGIN,
1641
+ onPointerDown: (event) => event.stopPropagation(),
1642
+ onClick: (event) => event.stopPropagation(),
1643
+ children: thinking.levels.map((level) => {
1644
+ const isActive = level === activeThinking;
1645
+ return /* @__PURE__ */ jsxs5(
1646
+ "button",
1647
+ {
1648
+ type: "button",
1649
+ className: "chat-model-thinking-option",
1650
+ "data-active": isActive ? "true" : "false",
1651
+ onClick: (event) => {
1652
+ event.preventDefault();
1653
+ event.stopPropagation();
1654
+ onPickThinking(level);
1655
+ },
1656
+ children: [
1657
+ /* @__PURE__ */ jsx13("span", { children: level }),
1658
+ isActive ? /* @__PURE__ */ jsx13(Check, { className: "chat-model-thinking-check" }) : null
1659
+ ]
1660
+ },
1661
+ level
1662
+ );
1663
+ })
1664
+ }
1665
+ ) : null
1666
+ ] }) : null
1667
+ ] });
1668
+ };
1669
+
1670
+ // src/components/Chat/chat.tsx
1671
+ import { jsx as jsx14, jsxs as jsxs6 } from "react/jsx-runtime";
1672
+ var focusStaysInComposer = (shell, node) => {
1673
+ if (node && shell.contains(node)) {
1674
+ return true;
1675
+ }
1676
+ return node instanceof Element && node.closest("[data-slot='select-content']") !== null;
1677
+ };
1678
+ var ChatComposer = ({
1679
+ showModelSelector = true,
1680
+ collapsedVariant = "pill",
1681
+ defaultExpanded = false
1682
+ }) => {
1683
+ const { status } = useMessages();
1684
+ const { input, attachments, references } = useComposer();
1685
+ const shellRef = useRef9(null);
1686
+ const [focused, setFocused] = useState7(defaultExpanded);
1687
+ const expanded = focused || input.trim().length > 0 || attachments.length > 0 || references.length > 0;
1688
+ useEffect6(() => {
1689
+ if (status !== "error" || !shellRef.current) {
1690
+ return;
1691
+ }
1692
+ triggerErrorShake(shellRef.current);
1693
+ }, [status]);
1694
+ useEffect6(() => {
1695
+ if (!focused) {
1696
+ return;
1697
+ }
1698
+ const onFocusIn = () => {
1699
+ const shell = shellRef.current;
1700
+ if (!shell) {
1701
+ return;
1702
+ }
1703
+ if (focusStaysInComposer(shell, document.activeElement)) {
1704
+ return;
1705
+ }
1706
+ setFocused(false);
1707
+ };
1708
+ document.addEventListener("focusin", onFocusIn, true);
1709
+ return () => document.removeEventListener("focusin", onFocusIn, true);
1710
+ }, [focused]);
1711
+ return /* @__PURE__ */ jsxs6(
1712
+ "div",
1713
+ {
1714
+ ref: shellRef,
1715
+ className: cn("chat-composer-shell", motion.inputWrap, motion.input),
1716
+ "data-expanded": expanded ? "true" : "false",
1717
+ "data-collapsed": collapsedVariant,
1718
+ onFocus: () => setFocused(true),
1719
+ onBlur: (event) => {
1720
+ const shell = event.currentTarget;
1721
+ const next = event.relatedTarget;
1722
+ if (next instanceof Node && focusStaysInComposer(shell, next)) {
1723
+ return;
1724
+ }
1725
+ window.setTimeout(() => {
1726
+ if (focusStaysInComposer(shell, document.activeElement)) {
1727
+ return;
1728
+ }
1729
+ setFocused(false);
1730
+ }, 0);
1731
+ },
1732
+ children: [
1733
+ expanded || collapsedVariant !== "circle" ? null : /* @__PURE__ */ jsx14(MessageCircle, { className: "chat-composer-collapsed-icon", size: 18, "aria-hidden": "true" }),
1734
+ /* @__PURE__ */ jsx14(ChatAttachments, {}),
1735
+ /* @__PURE__ */ jsx14(ChatReferences, {}),
1736
+ /* @__PURE__ */ jsx14(ChatInput, { expanded }),
1737
+ /* @__PURE__ */ jsxs6("div", { className: "chat-composer-row", children: [
1738
+ /* @__PURE__ */ jsxs6("div", { className: "chat-composer-tools", children: [
1739
+ /* @__PURE__ */ jsx14(ChatAttachButton, {}),
1740
+ showModelSelector && expanded && collapsedVariant === "circle" ? /* @__PURE__ */ jsx14(ChatSelect, {}) : null
1741
+ ] }),
1742
+ /* @__PURE__ */ jsx14(ChatSend, {})
1743
+ ] })
1744
+ ]
1745
+ }
1746
+ );
1747
+ };
1748
+
1749
+ // src/components/Message/message.item.tsx
1750
+ import { Pencil } from "lucide-react";
1751
+ import { useState as useState10 } from "react";
1752
+ import { normalizeAgentParts } from "@sarchauhan/protocol";
1753
+
1754
+ // src/components/Message/message.feedback.tsx
1755
+ import { ThumbsDown, ThumbsUp } from "lucide-react";
1756
+ import { useState as useState9 } from "react";
1757
+
1758
+ // src/components/Message/message.copy.tsx
1759
+ import { Check as Check2, Copy } from "lucide-react";
1760
+ import { useState as useState8 } from "react";
1761
+ import { jsx as jsx15 } from "react/jsx-runtime";
1762
+ var MessageCopy = ({ text, disabled = false }) => {
1763
+ const [isCopied, setIsCopied] = useState8(false);
1764
+ const copyMessage = async () => {
1765
+ await navigator.clipboard.writeText(text);
1766
+ setIsCopied(true);
1767
+ window.setTimeout(() => setIsCopied(false), 1500);
1768
+ };
1769
+ return /* @__PURE__ */ jsx15(
1770
+ "button",
1771
+ {
1772
+ type: "button",
1773
+ className: "chat-feedback-button",
1774
+ title: isCopied ? "Copied" : "Copy response",
1775
+ "aria-label": isCopied ? "Copied" : "Copy response",
1776
+ onClick: () => void copyMessage(),
1777
+ disabled,
1778
+ children: isCopied ? /* @__PURE__ */ jsx15(Check2, { size: 16 }) : /* @__PURE__ */ jsx15(Copy, { size: 16 })
1779
+ }
1780
+ );
1781
+ };
1782
+
1783
+ // src/components/Message/message.regenerate.tsx
1784
+ import { RotateCcw } from "lucide-react";
1785
+ import { jsx as jsx16 } from "react/jsx-runtime";
1786
+ var MessageRegenerate = ({ onRegenerate, disabled = false }) => /* @__PURE__ */ jsx16(
1787
+ "button",
1788
+ {
1789
+ type: "button",
1790
+ className: "chat-feedback-button",
1791
+ title: "Regenerate response",
1792
+ "aria-label": "Regenerate response",
1793
+ disabled,
1794
+ onClick: onRegenerate,
1795
+ children: /* @__PURE__ */ jsx16(RotateCcw, { size: 17 })
1796
+ }
1797
+ );
1798
+
1799
+ // src/components/Message/message.feedback.tsx
1800
+ import { jsx as jsx17, jsxs as jsxs7 } from "react/jsx-runtime";
1801
+ var MessageFeedback = ({
1802
+ responseText,
1803
+ onRegenerate,
1804
+ canRegenerate,
1805
+ disabled = false
1806
+ }) => {
1807
+ const [feedback, setFeedback] = useState9(null);
1808
+ if (!responseText.trim()) {
1809
+ return null;
1810
+ }
1811
+ return /* @__PURE__ */ jsxs7("div", { className: "chat-message-feedback", "aria-label": "Message actions", children: [
1812
+ /* @__PURE__ */ jsx17(MessageCopy, { text: responseText, disabled }),
1813
+ /* @__PURE__ */ jsx17(
1814
+ MessageRegenerate,
1815
+ {
1816
+ onRegenerate,
1817
+ disabled: !canRegenerate || disabled
1818
+ }
1819
+ ),
1820
+ /* @__PURE__ */ jsx17(
1821
+ "button",
1822
+ {
1823
+ type: "button",
1824
+ className: `chat-feedback-button${feedback === "up" ? " is-selected" : ""}`,
1825
+ title: "Good response",
1826
+ "aria-label": "Good response",
1827
+ "aria-pressed": feedback === "up",
1828
+ onClick: () => setFeedback(feedback === "up" ? null : "up"),
1829
+ disabled,
1830
+ children: /* @__PURE__ */ jsx17(ThumbsUp, { size: 18 })
1831
+ }
1832
+ ),
1833
+ /* @__PURE__ */ jsx17(
1834
+ "button",
1835
+ {
1836
+ type: "button",
1837
+ className: `chat-feedback-button${feedback === "down" ? " is-selected" : ""}`,
1838
+ title: "Poor response",
1839
+ "aria-label": "Poor response",
1840
+ "aria-pressed": feedback === "down",
1841
+ onClick: () => setFeedback(feedback === "down" ? null : "down"),
1842
+ disabled,
1843
+ children: /* @__PURE__ */ jsx17(ThumbsDown, { size: 18 })
1844
+ }
1845
+ )
1846
+ ] });
1847
+ };
1848
+
1849
+ // src/components/Message/message.usage.tsx
1850
+ import {
1851
+ parseContext,
1852
+ parseUsage
1853
+ } from "@sarchauhan/protocol";
1854
+ import { Fragment as Fragment4, jsxs as jsxs8 } from "react/jsx-runtime";
1855
+ var formatTokens = (value) => new Intl.NumberFormat("en-US").format(value);
1856
+ var isTokenCount = (value) => typeof value === "number" && Number.isFinite(value);
1857
+ var getContextMetrics = (context) => {
1858
+ const currentTokens = context == null ? void 0 : context.current_tokens;
1859
+ const contextWindow = context == null ? void 0 : context.context_window;
1860
+ return isTokenCount(currentTokens) && isTokenCount(contextWindow) ? { currentTokens, contextWindow } : null;
1861
+ };
1862
+ var MessageUsage = ({ parts }) => {
1863
+ var _a, _b;
1864
+ const usage = parseUsage((_a = parts.find((part) => part.name === "usage")) == null ? void 0 : _a.data);
1865
+ const context = parseContext((_b = parts.find((part) => part.name === "context")) == null ? void 0 : _b.data);
1866
+ const contextMetrics = getContextMetrics(context);
1867
+ const tokenMetrics = usage ? [
1868
+ ["in", usage.input_tokens],
1869
+ ["out", usage.output_tokens],
1870
+ ["reasoning", usage.reasoning_tokens],
1871
+ ["total", usage.total_tokens]
1872
+ ].filter((metric) => isTokenCount(metric[1])) : [];
1873
+ if (!contextMetrics && tokenMetrics.length === 0) return null;
1874
+ return /* @__PURE__ */ jsxs8("p", { className: "chat-message-usage", "aria-label": "Context and token usage", children: [
1875
+ contextMetrics ? /* @__PURE__ */ jsxs8(Fragment4, { children: [
1876
+ "Context ",
1877
+ formatTokens(contextMetrics.currentTokens),
1878
+ " / ",
1879
+ formatTokens(contextMetrics.contextWindow),
1880
+ " (",
1881
+ Math.round(contextMetrics.currentTokens / contextMetrics.contextWindow * 100),
1882
+ "%)"
1883
+ ] }) : null,
1884
+ tokenMetrics.length > 0 ? /* @__PURE__ */ jsxs8(Fragment4, { children: [
1885
+ contextMetrics ? " \xB7 " : null,
1886
+ "Tokens ",
1887
+ tokenMetrics.map(([label, value]) => `${label} ${formatTokens(value)}`).join(" \xB7 ")
1888
+ ] }) : null
1889
+ ] });
1890
+ };
1891
+
1892
+ // src/components/Message/message.item.tsx
1893
+ import { jsx as jsx18, jsxs as jsxs9 } from "react/jsx-runtime";
1894
+ var getMessageTargetId = (messageId) => `chat-message-${messageId}`;
1895
+ var MessageItem = ({ message }) => {
1896
+ var _a;
1897
+ const { editAndResendMessage, isSending, messages } = useMessages();
1898
+ const [isEditing, setIsEditing] = useState10(false);
1899
+ const [draft, setDraft] = useState10(() => getUserDisplayText(message));
1900
+ const [isSubmittingEdit, setIsSubmittingEdit] = useState10(false);
1901
+ const isUser = message.role === "user";
1902
+ const isActiveAssistantMessage = isSending && ((_a = messages[messages.length - 1]) == null ? void 0 : _a.id) === message.id;
1903
+ const assistantText = message.parts.filter((part) => part.type === "text").map((part) => "text" in part ? String(part.text) : "").join("\n").trim();
1904
+ const dataParts = normalizeAgentParts(message.parts).filter(
1905
+ (part) => part.type === "data"
1906
+ );
1907
+ const previousUserMessage = [...messages].reverse().find((item) => item.role === "user");
1908
+ const cancelEdit = () => {
1909
+ setDraft(getUserDisplayText(message));
1910
+ setIsEditing(false);
1911
+ };
1912
+ const submitEdit = async () => {
1913
+ if (!draft.trim() || isSending || isSubmittingEdit) {
1914
+ return;
1915
+ }
1916
+ setIsSubmittingEdit(true);
1917
+ try {
1918
+ await editAndResendMessage(message.id, draft);
1919
+ setIsEditing(false);
1920
+ } finally {
1921
+ setIsSubmittingEdit(false);
1922
+ }
1923
+ };
1924
+ if (isUser) {
1925
+ return /* @__PURE__ */ jsx18("div", { id: getMessageTargetId(message.id), className: "chat-message chat-message-user", children: /* @__PURE__ */ jsxs9("div", { className: `chat-message-user-inner${isEditing ? " is-editing" : ""}`, children: [
1926
+ /* @__PURE__ */ jsx18("div", { className: "chat-message-bubble", children: isEditing ? /* @__PURE__ */ jsx18(
1927
+ "textarea",
1928
+ {
1929
+ className: "chat-edit-area",
1930
+ value: draft,
1931
+ disabled: isSubmittingEdit,
1932
+ autoFocus: true,
1933
+ onChange: (event) => setDraft(event.target.value),
1934
+ onKeyDown: (event) => {
1935
+ if (event.key !== "Enter" || event.shiftKey) return;
1936
+ if (event.nativeEvent.isComposing) return;
1937
+ event.preventDefault();
1938
+ void submitEdit();
1939
+ }
1940
+ }
1941
+ ) : /* @__PURE__ */ jsx18(MessageContent, { parts: message.parts, isUser: true }) }),
1942
+ isEditing ? /* @__PURE__ */ jsxs9("div", { className: "chat-message-actions", children: [
1943
+ /* @__PURE__ */ jsx18(
1944
+ "button",
1945
+ {
1946
+ type: "button",
1947
+ className: "chat-text-button",
1948
+ onClick: cancelEdit,
1949
+ disabled: isSubmittingEdit,
1950
+ children: "Cancel"
1951
+ }
1952
+ ),
1953
+ /* @__PURE__ */ jsx18(
1954
+ "button",
1955
+ {
1956
+ type: "button",
1957
+ className: "chat-text-button chat-text-button-done",
1958
+ onClick: submitEdit,
1959
+ disabled: !draft.trim() || isSubmittingEdit || isSending,
1960
+ children: "Done"
1961
+ }
1962
+ )
1963
+ ] }) : /* @__PURE__ */ jsx18(
1964
+ "button",
1965
+ {
1966
+ type: "button",
1967
+ className: "chat-icon-button",
1968
+ onClick: () => {
1969
+ setDraft(getUserDisplayText(message));
1970
+ setIsEditing(true);
1971
+ },
1972
+ disabled: isSending,
1973
+ "aria-label": "Edit and resend message",
1974
+ children: /* @__PURE__ */ jsx18(Pencil, { size: 14 })
1975
+ }
1976
+ )
1977
+ ] }) });
1978
+ }
1979
+ return /* @__PURE__ */ jsx18("div", { className: "chat-message chat-message-assistant", children: /* @__PURE__ */ jsxs9("div", { className: "chat-message-assistant-inner", children: [
1980
+ /* @__PURE__ */ jsx18(MessageContent, { parts: message.parts, isUser: false }),
1981
+ /* @__PURE__ */ jsx18(MessageUsage, { parts: dataParts }),
1982
+ isActiveAssistantMessage ? null : /* @__PURE__ */ jsx18(
1983
+ MessageFeedback,
1984
+ {
1985
+ responseText: assistantText,
1986
+ canRegenerate: !!previousUserMessage,
1987
+ disabled: isSending,
1988
+ onRegenerate: () => {
1989
+ if (previousUserMessage) {
1990
+ void editAndResendMessage(
1991
+ previousUserMessage.id,
1992
+ getUserDisplayText(previousUserMessage)
1993
+ );
1994
+ }
1995
+ }
1996
+ }
1997
+ )
1998
+ ] }) });
1999
+ };
2000
+
2001
+ // src/components/Message/message.tsx
2002
+ import { jsx as jsx19, jsxs as jsxs10 } from "react/jsx-runtime";
2003
+ var WaitingForResponse = () => /* @__PURE__ */ jsx19("div", { className: "chat-response-status", role: "status", "aria-live": "polite", children: /* @__PURE__ */ jsx19(TextSkeleton, {}) });
2004
+ var Message = () => {
2005
+ const { messages, isWaitingForResponse } = useMessages();
2006
+ const { isLoadingThread } = useThread();
2007
+ if (isLoadingThread) {
2008
+ return /* @__PURE__ */ jsx19("div", { className: "chat-messages-inner", children: /* @__PURE__ */ jsx19("div", { className: "chat-loading chat-loading-initial", role: "status", "aria-live": "polite", children: "Loading conversation\u2026" }) });
2009
+ }
2010
+ if (messages.length === 0) {
2011
+ return /* @__PURE__ */ jsx19("div", { className: "chat-messages-inner", children: /* @__PURE__ */ jsx19("div", { className: "chat-empty", children: /* @__PURE__ */ jsxs10("div", { children: [
2012
+ /* @__PURE__ */ jsx19("p", { className: "chat-empty-title", children: "How can I help?" }),
2013
+ /* @__PURE__ */ jsx19("p", { className: "chat-empty-copy", children: "Ask a question, explore ideas, or use tools and widgets when the agent needs them." })
2014
+ ] }) }) });
2015
+ }
2016
+ return /* @__PURE__ */ jsxs10("div", { className: "chat-messages-inner", children: [
2017
+ messages.map((message) => /* @__PURE__ */ jsx19(MessageItem, { message }, message.id)),
2018
+ isWaitingForResponse ? /* @__PURE__ */ jsx19(WaitingForResponse, {}) : null
2019
+ ] });
2020
+ };
2021
+
2022
+ // src/components/Chat.tsx
2023
+ import { jsx as jsx20, jsxs as jsxs11 } from "react/jsx-runtime";
2024
+ function ChatShell({
2025
+ className,
2026
+ style,
2027
+ widgets,
2028
+ showThemeToggle = true,
2029
+ showModelSelector = true,
2030
+ collapsedVariant = "pill",
2031
+ defaultExpanded = false
2032
+ }) {
2033
+ const { sendMessage, isSending, messages, status } = useMessages();
2034
+ const { createThread } = useThread();
2035
+ const { resolvedTheme } = useTheme();
2036
+ const registry = useMemo7(() => createWidgetRegistry(widgets), [widgets]);
2037
+ const messagesRef = useRef10(null);
2038
+ useLayoutEffect(() => {
2039
+ const container = messagesRef.current;
2040
+ if (!container) {
2041
+ return;
2042
+ }
2043
+ container.scrollTop = container.scrollHeight;
2044
+ }, [messages, status]);
2045
+ useLayoutEffect(() => {
2046
+ const container = messagesRef.current;
2047
+ if (!container || status !== "submitted" && status !== "streaming") {
2048
+ return;
2049
+ }
2050
+ const scrollToEnd = () => {
2051
+ container.scrollTop = container.scrollHeight;
2052
+ };
2053
+ const observer = new ResizeObserver(scrollToEnd);
2054
+ observer.observe(container);
2055
+ const inner = container.firstElementChild;
2056
+ if (inner) {
2057
+ observer.observe(inner);
2058
+ }
2059
+ scrollToEnd();
2060
+ return () => observer.disconnect();
2061
+ }, [status]);
2062
+ const respondToWidget = useCallback6(
2063
+ async (response) => {
2064
+ var _a, _b;
2065
+ const text = (_b = (_a = response.label) != null ? _a : response.actionId) != null ? _b : typeof response.value === "string" ? response.value : JSON.stringify(response.value);
2066
+ if (!text.trim()) {
2067
+ return;
2068
+ }
2069
+ await sendMessage({ text });
2070
+ },
2071
+ [sendMessage]
2072
+ );
2073
+ return /* @__PURE__ */ jsx20(
2074
+ WidgetProvider,
2075
+ {
2076
+ widgets: registry,
2077
+ respondToWidget,
2078
+ disabled: isSending,
2079
+ children: /* @__PURE__ */ jsxs11(
2080
+ "div",
2081
+ {
2082
+ className: ["chat-root", className].filter(Boolean).join(" "),
2083
+ style,
2084
+ "data-theme": resolvedTheme,
2085
+ children: [
2086
+ /* @__PURE__ */ jsxs11("div", { className: "chat-toolbar", children: [
2087
+ /* @__PURE__ */ jsx20(
2088
+ "button",
2089
+ {
2090
+ type: "button",
2091
+ className: "chat-theme-toggle",
2092
+ onClick: () => void createThread(),
2093
+ "aria-label": "Start a new chat",
2094
+ title: "New chat",
2095
+ disabled: isSending,
2096
+ children: /* @__PURE__ */ jsx20(PenSquare, { size: 16, strokeWidth: 1.75 })
2097
+ }
2098
+ ),
2099
+ showThemeToggle && /* @__PURE__ */ jsx20(ThemeToggle, {})
2100
+ ] }),
2101
+ /* @__PURE__ */ jsx20("div", { className: "chat-messages", ref: messagesRef, children: /* @__PURE__ */ jsx20(Message, {}) }),
2102
+ /* @__PURE__ */ jsx20("div", { className: "chat-composer", children: /* @__PURE__ */ jsx20(
2103
+ ChatComposer,
2104
+ {
2105
+ showModelSelector,
2106
+ collapsedVariant,
2107
+ defaultExpanded
2108
+ }
2109
+ ) })
2110
+ ]
2111
+ }
2112
+ )
2113
+ }
2114
+ );
2115
+ }
2116
+ function Chat({
2117
+ adapter,
2118
+ className,
2119
+ defaultThreadId,
2120
+ defaultProvider,
2121
+ registryUrl,
2122
+ style,
2123
+ widgets,
2124
+ theme,
2125
+ defaultTheme = "system",
2126
+ onThemeChange,
2127
+ showThemeToggle = true,
2128
+ showModelSelector = true,
2129
+ collapsedVariant = "pill",
2130
+ defaultExpanded = false
2131
+ }) {
2132
+ return /* @__PURE__ */ jsx20(
2133
+ ThemeProvider,
2134
+ {
2135
+ theme,
2136
+ defaultTheme,
2137
+ onThemeChange,
2138
+ children: /* @__PURE__ */ jsx20(
2139
+ ChatContextProvider,
2140
+ {
2141
+ adapter,
2142
+ defaultThreadId,
2143
+ defaultProvider,
2144
+ registryUrl,
2145
+ children: /* @__PURE__ */ jsx20(
2146
+ ChatShell,
2147
+ {
2148
+ className,
2149
+ style,
2150
+ widgets,
2151
+ showThemeToggle,
2152
+ showModelSelector,
2153
+ collapsedVariant,
2154
+ defaultExpanded
2155
+ }
2156
+ )
2157
+ }
2158
+ )
2159
+ }
2160
+ );
2161
+ }
2162
+
2163
+ // src/index.ts
2164
+ import { defineAdapter } from "@sarchauhan/adapter";
2165
+ import {
2166
+ applyAgentEvent,
2167
+ createAgentMessageState,
2168
+ createWidgetData,
2169
+ normalizeAgentMessage,
2170
+ normalizeAgentParts as normalizeAgentParts2,
2171
+ reduceAgentEvents
2172
+ } from "@sarchauhan/protocol";
2173
+ export {
2174
+ ArtifactWidget,
2175
+ ArtifactsWidget,
2176
+ BaseWidget,
2177
+ Chat,
2178
+ ChatComposer,
2179
+ ChatContextProvider,
2180
+ LoadingState,
2181
+ Message,
2182
+ MessageContent,
2183
+ MessageFeedback,
2184
+ MessageUsage,
2185
+ PatchWidget,
2186
+ ProviderId,
2187
+ QuestionWidget,
2188
+ ReadFileWidget,
2189
+ ResponsiveDetail,
2190
+ SearchWidget,
2191
+ SubagentWidget,
2192
+ TextSwap,
2193
+ ThemeProvider,
2194
+ ThemeToggle,
2195
+ ThinkingBlock,
2196
+ WidgetProvider,
2197
+ WidgetRenderer,
2198
+ applyAgentEvent,
2199
+ calibrateCheckPath,
2200
+ clearErrorShake,
2201
+ closeDropdown,
2202
+ computeDiff,
2203
+ computeTextDiff,
2204
+ computeToolState,
2205
+ computeToolWidget,
2206
+ createAgentMessageState,
2207
+ createArtifactData,
2208
+ createSubagentData,
2209
+ createWidgetData,
2210
+ createWidgetRegistry,
2211
+ defineAdapter,
2212
+ defineWidget,
2213
+ dropdownOrigins,
2214
+ formatArtifactSize,
2215
+ formatElapsed,
2216
+ formatElapsedWords,
2217
+ iconSwapStates,
2218
+ isProviderId,
2219
+ motion,
2220
+ motionData,
2221
+ motionRoot,
2222
+ normalizeAgentMessage,
2223
+ normalizeAgentParts2 as normalizeAgentParts,
2224
+ normalizeSearchResults,
2225
+ openDropdown,
2226
+ parseArtifact,
2227
+ parseRegistryConfig,
2228
+ parseSubagent,
2229
+ prebuiltWidgets,
2230
+ prefersReducedMotion,
2231
+ readDurationMs,
2232
+ reduceAgentEvents,
2233
+ safeArtifactUrl,
2234
+ safeResultUrl,
2235
+ successCheckStates,
2236
+ swapText,
2237
+ triggerErrorShake,
2238
+ useChat,
2239
+ useComposer,
2240
+ useDropdownPresence,
2241
+ useMessages,
2242
+ useModel,
2243
+ useTheme,
2244
+ useThread,
2245
+ useWidgets
2246
+ };
2247
+ //# sourceMappingURL=index.mjs.map