@xinghunm/ai-chat 0.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/index.mjs ADDED
@@ -0,0 +1,4004 @@
1
+ // src/components/ai-chat/index.tsx
2
+ import styled18 from "@emotion/styled";
3
+ import { ConfigProvider } from "@xinghunm/compass-ui";
4
+
5
+ // src/components/ai-chat-provider/index.tsx
6
+ import { useRef, useMemo, useState } from "react";
7
+ import axios2 from "axios";
8
+
9
+ // src/context/chat-context.ts
10
+ import { createContext } from "react";
11
+ var ChatContext = createContext(null);
12
+
13
+ // src/store/chat-store.ts
14
+ import { createStore } from "zustand/vanilla";
15
+
16
+ // src/types/index.ts
17
+ var CHAT_AGENT_MODES = ["ask", "plan", "agent"];
18
+ var DEFAULT_CHAT_AGENT_MODE = "agent";
19
+ var DEFAULT_AI_CHAT_LABELS = {
20
+ sendButton: "Send",
21
+ stopButton: "Stop",
22
+ retryButton: "Retry",
23
+ placeholder: "Ask something...",
24
+ modeLabelAsk: "Ask",
25
+ modeLabelPlan: "Plan",
26
+ modeLabelAgent: "Agent",
27
+ newChat: "New Chat",
28
+ emptyStateTitle: "How can I help you?",
29
+ emptyStateSubtitle: "Start a conversation",
30
+ attachmentLimitNotice: "Images exceeded the limit. Only the first 10 images were kept.",
31
+ userRoleLabel: "User",
32
+ assistantRoleLabel: "Assistant",
33
+ stoppedResponse: "Response stopped",
34
+ assistantStreamingAriaLabel: "assistant streaming",
35
+ networkError: "Network request failed. Please try again.",
36
+ genericError: "Something went wrong. Please try again."
37
+ };
38
+
39
+ // src/store/chat-store.ts
40
+ var DEFAULT_CHAT_SESSION_TITLE = "New Chat";
41
+ var IMAGE_MESSAGE_SESSION_TITLE = "Image message";
42
+ var resolveSessionTitleFromMessage = (message) => {
43
+ const trimmedContent = message.content.trim();
44
+ if (trimmedContent) {
45
+ return trimmedContent.slice(0, 30);
46
+ }
47
+ if ((message.attachments?.length ?? 0) > 0) {
48
+ return IMAGE_MESSAGE_SESSION_TITLE;
49
+ }
50
+ return DEFAULT_CHAT_SESSION_TITLE;
51
+ };
52
+ var finalizeStreamingMessage = (state, sessionId, status, clearError = false) => {
53
+ const message = state.streamingMessageBySession[sessionId];
54
+ const hasRenderableMessagePayload = Boolean(
55
+ message?.content || message?.blocks?.length || message?.attachments?.length
56
+ );
57
+ const nextMessages = [...state.messagesBySession[sessionId] ?? []];
58
+ if (message && (!status || hasRenderableMessagePayload)) {
59
+ nextMessages.push({ ...message, status: status ?? message.status });
60
+ }
61
+ return {
62
+ ...state,
63
+ messagesBySession: {
64
+ ...state.messagesBySession,
65
+ [sessionId]: nextMessages
66
+ },
67
+ streamingMessageBySession: {
68
+ ...state.streamingMessageBySession,
69
+ [sessionId]: void 0
70
+ },
71
+ isStreamingBySession: {
72
+ ...state.isStreamingBySession,
73
+ [sessionId]: false
74
+ },
75
+ isStoppingBySession: {
76
+ ...state.isStoppingBySession,
77
+ [sessionId]: false
78
+ },
79
+ errorBySession: clearError ? { ...state.errorBySession, [sessionId]: null } : state.errorBySession
80
+ };
81
+ };
82
+ var createChatStore = (initialState) => createStore((set, get) => ({
83
+ activeSessionId: null,
84
+ preferredMode: initialState?.preferredMode ?? DEFAULT_CHAT_AGENT_MODE,
85
+ sessions: [],
86
+ messagesBySession: {},
87
+ streamingMessageBySession: {},
88
+ isStreamingBySession: {},
89
+ isStoppingBySession: {},
90
+ errorBySession: {},
91
+ // ---- Session management ------------------------------------------------
92
+ createSession: (session) => {
93
+ const state = get();
94
+ const exists = state.sessions.some((s) => s.sessionId === session.sessionId);
95
+ const nextSession = {
96
+ ...session,
97
+ mode: session.mode ?? DEFAULT_CHAT_AGENT_MODE
98
+ };
99
+ const nextSessions = exists ? state.sessions : [nextSession, ...state.sessions];
100
+ const nextMessagesBySession = { ...state.messagesBySession };
101
+ const nextErrorBySession = { ...state.errorBySession };
102
+ const nextIsStreamingBySession = { ...state.isStreamingBySession };
103
+ const nextIsStoppingBySession = { ...state.isStoppingBySession };
104
+ const sid = session.sessionId;
105
+ if (nextMessagesBySession[sid] === void 0)
106
+ nextMessagesBySession[sid] = [];
107
+ if (nextErrorBySession[sid] === void 0)
108
+ nextErrorBySession[sid] = null;
109
+ if (nextIsStreamingBySession[sid] === void 0)
110
+ nextIsStreamingBySession[sid] = false;
111
+ if (nextIsStoppingBySession[sid] === void 0)
112
+ nextIsStoppingBySession[sid] = false;
113
+ set({
114
+ sessions: nextSessions,
115
+ activeSessionId: sid,
116
+ messagesBySession: nextMessagesBySession,
117
+ errorBySession: nextErrorBySession,
118
+ isStreamingBySession: nextIsStreamingBySession,
119
+ isStoppingBySession: nextIsStoppingBySession
120
+ });
121
+ },
122
+ setActiveSession: (sessionId) => {
123
+ set({ activeSessionId: sessionId });
124
+ },
125
+ replaceSessionId: (previousSessionId, nextSessionId) => {
126
+ if (!previousSessionId || !nextSessionId || previousSessionId === nextSessionId) {
127
+ return;
128
+ }
129
+ const state = get();
130
+ const nextSessions = state.sessions.map(
131
+ (s) => s.sessionId === previousSessionId ? { ...s, sessionId: nextSessionId } : s
132
+ );
133
+ const nextMessagesBySession = { ...state.messagesBySession };
134
+ if (previousSessionId in nextMessagesBySession) {
135
+ nextMessagesBySession[nextSessionId] = (nextMessagesBySession[previousSessionId] ?? []).map(
136
+ (m) => ({ ...m, sessionId: nextSessionId })
137
+ );
138
+ delete nextMessagesBySession[previousSessionId];
139
+ }
140
+ const nextStreamingMessageBySession = { ...state.streamingMessageBySession };
141
+ if (previousSessionId in nextStreamingMessageBySession) {
142
+ const prev = nextStreamingMessageBySession[previousSessionId];
143
+ if (prev) {
144
+ nextStreamingMessageBySession[nextSessionId] = { ...prev, sessionId: nextSessionId };
145
+ } else if (!(nextSessionId in nextStreamingMessageBySession)) {
146
+ nextStreamingMessageBySession[nextSessionId] = void 0;
147
+ }
148
+ delete nextStreamingMessageBySession[previousSessionId];
149
+ }
150
+ const nextIsStreamingBySession = { ...state.isStreamingBySession };
151
+ if (previousSessionId in nextIsStreamingBySession) {
152
+ nextIsStreamingBySession[nextSessionId] = nextIsStreamingBySession[previousSessionId] ?? false;
153
+ delete nextIsStreamingBySession[previousSessionId];
154
+ }
155
+ const nextIsStoppingBySession = { ...state.isStoppingBySession };
156
+ if (previousSessionId in nextIsStoppingBySession) {
157
+ nextIsStoppingBySession[nextSessionId] = nextIsStoppingBySession[previousSessionId] ?? false;
158
+ delete nextIsStoppingBySession[previousSessionId];
159
+ }
160
+ const nextErrorBySession = { ...state.errorBySession };
161
+ if (previousSessionId in nextErrorBySession) {
162
+ nextErrorBySession[nextSessionId] = nextErrorBySession[previousSessionId] ?? null;
163
+ delete nextErrorBySession[previousSessionId];
164
+ }
165
+ const nextActiveSessionId = state.activeSessionId === previousSessionId ? nextSessionId : state.activeSessionId;
166
+ set({
167
+ sessions: nextSessions,
168
+ messagesBySession: nextMessagesBySession,
169
+ streamingMessageBySession: nextStreamingMessageBySession,
170
+ isStreamingBySession: nextIsStreamingBySession,
171
+ isStoppingBySession: nextIsStoppingBySession,
172
+ errorBySession: nextErrorBySession,
173
+ activeSessionId: nextActiveSessionId
174
+ });
175
+ },
176
+ setPreferredMode: (mode) => {
177
+ set({ preferredMode: mode });
178
+ },
179
+ setSessionMode: (sessionId, mode) => {
180
+ const state = get();
181
+ const nextSessions = state.sessions.map(
182
+ (s) => s.sessionId === sessionId ? { ...s, mode } : s
183
+ );
184
+ set({ sessions: nextSessions });
185
+ },
186
+ // ---- Message operations ------------------------------------------------
187
+ appendMessage: (sessionId, message) => {
188
+ const state = get();
189
+ const nextMessagesBySession = { ...state.messagesBySession };
190
+ if (!nextMessagesBySession[sessionId]) {
191
+ nextMessagesBySession[sessionId] = [];
192
+ }
193
+ nextMessagesBySession[sessionId] = [...nextMessagesBySession[sessionId], message];
194
+ const nextSessions = state.sessions.map((s) => {
195
+ if (s.sessionId !== sessionId)
196
+ return s;
197
+ const updated = { ...s, updatedAt: message.createdAt };
198
+ if (message.role === "user" && s.title === DEFAULT_CHAT_SESSION_TITLE) {
199
+ updated.title = resolveSessionTitleFromMessage(message);
200
+ }
201
+ return updated;
202
+ });
203
+ const nextErrorBySession = state.errorBySession[sessionId] === void 0 ? { ...state.errorBySession, [sessionId]: null } : state.errorBySession;
204
+ const nextIsStreamingBySession = state.isStreamingBySession[sessionId] === void 0 ? { ...state.isStreamingBySession, [sessionId]: false } : state.isStreamingBySession;
205
+ set({
206
+ messagesBySession: nextMessagesBySession,
207
+ sessions: nextSessions,
208
+ errorBySession: nextErrorBySession,
209
+ isStreamingBySession: nextIsStreamingBySession
210
+ });
211
+ },
212
+ startStreamingMessage: (sessionId, message) => {
213
+ const state = get();
214
+ set({
215
+ streamingMessageBySession: {
216
+ ...state.streamingMessageBySession,
217
+ [sessionId]: message
218
+ },
219
+ isStreamingBySession: {
220
+ ...state.isStreamingBySession,
221
+ [sessionId]: true
222
+ },
223
+ isStoppingBySession: {
224
+ ...state.isStoppingBySession,
225
+ [sessionId]: false
226
+ },
227
+ errorBySession: {
228
+ ...state.errorBySession,
229
+ [sessionId]: null
230
+ }
231
+ });
232
+ },
233
+ updateStreamingMessage: (sessionId, content) => {
234
+ get().patchStreamingMessage(sessionId, { content });
235
+ },
236
+ patchStreamingMessage: (sessionId, patch) => {
237
+ const state = get();
238
+ const target = state.streamingMessageBySession[sessionId];
239
+ if (!target)
240
+ return;
241
+ set({
242
+ streamingMessageBySession: {
243
+ ...state.streamingMessageBySession,
244
+ [sessionId]: { ...target, ...patch }
245
+ }
246
+ });
247
+ },
248
+ completeStreamingMessage: (sessionId) => {
249
+ set((state) => finalizeStreamingMessage(state, sessionId, "done"));
250
+ },
251
+ requestStopStreaming: (sessionId) => {
252
+ const state = get();
253
+ set({
254
+ isStoppingBySession: {
255
+ ...state.isStoppingBySession,
256
+ [sessionId]: true
257
+ }
258
+ });
259
+ },
260
+ finalizeStoppedStreamingMessage: (sessionId) => {
261
+ set((state) => finalizeStreamingMessage(state, sessionId, "stopped", true));
262
+ },
263
+ setSessionError: (sessionId, error) => {
264
+ const state = get();
265
+ set({
266
+ errorBySession: { ...state.errorBySession, [sessionId]: error }
267
+ });
268
+ },
269
+ clearSessionError: (sessionId) => {
270
+ const state = get();
271
+ set({
272
+ errorBySession: { ...state.errorBySession, [sessionId]: null }
273
+ });
274
+ },
275
+ updateQuestionnaireAnswers: (sessionId, messageId, questionnaireId, answers) => {
276
+ const state = get();
277
+ const sessionMessages = state.messagesBySession[sessionId];
278
+ const updateBlocks = (message) => {
279
+ if (!message.blocks?.length)
280
+ return message;
281
+ let hasUpdatedBlock = false;
282
+ const nextBlocks = message.blocks.map((block) => {
283
+ if (block.type !== "questionnaire" || block.questionnaire.questionnaireId !== questionnaireId) {
284
+ return block;
285
+ }
286
+ hasUpdatedBlock = true;
287
+ return {
288
+ ...block,
289
+ questionnaire: { ...block.questionnaire, answers: { ...answers } }
290
+ };
291
+ });
292
+ if (!hasUpdatedBlock)
293
+ return message;
294
+ return { ...message, blocks: nextBlocks };
295
+ };
296
+ const nextMessagesBySession = { ...state.messagesBySession };
297
+ if (sessionMessages) {
298
+ nextMessagesBySession[sessionId] = sessionMessages.map(
299
+ (m) => m.id === messageId ? updateBlocks(m) : m
300
+ );
301
+ }
302
+ const currentStreaming = state.streamingMessageBySession[sessionId];
303
+ const nextStreamingMessageBySession = { ...state.streamingMessageBySession };
304
+ if (currentStreaming && currentStreaming.id === messageId) {
305
+ nextStreamingMessageBySession[sessionId] = updateBlocks(currentStreaming);
306
+ }
307
+ set({
308
+ messagesBySession: nextMessagesBySession,
309
+ streamingMessageBySession: nextStreamingMessageBySession
310
+ });
311
+ }
312
+ }));
313
+
314
+ // src/transport/default-chat-transport.ts
315
+ import axios from "axios";
316
+
317
+ // src/api/index.ts
318
+ var CHAT_MODELS_PATH = "/models";
319
+ var CHAT_TERMINATE_PATH = "/chat/terminate";
320
+ var getChatModels = async (client, path = CHAT_MODELS_PATH) => {
321
+ const response = await client.get(path);
322
+ return response.data;
323
+ };
324
+ var terminateChat = async (client, sessionId, path = CHAT_TERMINATE_PATH) => {
325
+ const response = await client.post(
326
+ path,
327
+ sessionId ? { session_id: sessionId } : {},
328
+ { headers: sessionId ? { "X-Session-ID": sessionId } : void 0 }
329
+ );
330
+ return response.data;
331
+ };
332
+
333
+ // src/api/chat-stream.ts
334
+ var CHAT_COMPLETIONS_PATH = "/chat/completions";
335
+ var SSE_CONTENT_TYPE = "text/event-stream";
336
+ var isObjectRecord = (value) => typeof value === "object" && value !== null;
337
+ var isPayloadPacketData = (value) => isObjectRecord(value) && Array.isArray(value.payload);
338
+ var isStructuredPacketData = (value) => isObjectRecord(value) && ("content" in value || "message" in value || "blocks" in value);
339
+ var parseSseEvent = (eventText) => {
340
+ const dataLine = eventText.split("\n").map((line) => line.trim()).find((line) => line.startsWith("data:"));
341
+ if (!dataLine)
342
+ return null;
343
+ const raw = dataLine.slice(5).trim();
344
+ if (!raw)
345
+ return null;
346
+ try {
347
+ return JSON.parse(raw);
348
+ } catch {
349
+ return null;
350
+ }
351
+ };
352
+ var extractChatStreamUpdate = (packet) => {
353
+ if (packet.type !== "delta" && packet.type !== "message_complete") {
354
+ return null;
355
+ }
356
+ if (isPayloadPacketData(packet.data)) {
357
+ const contentDelta = packet.data.payload[0]?.delta?.content ?? "";
358
+ return contentDelta ? { contentDelta } : null;
359
+ }
360
+ if (!isStructuredPacketData(packet.data)) {
361
+ return null;
362
+ }
363
+ const content = typeof packet.data.content === "string" ? packet.data.content : typeof packet.data.message === "string" ? packet.data.message : void 0;
364
+ const blocks = Array.isArray(packet.data.blocks) ? packet.data.blocks : void 0;
365
+ if (content === void 0 && blocks === void 0) {
366
+ return null;
367
+ }
368
+ return {
369
+ ...content !== void 0 ? { content } : {},
370
+ ...blocks !== void 0 ? { blocks } : {}
371
+ };
372
+ };
373
+ var startChatStream = async ({
374
+ apiBaseUrl,
375
+ endpointPath = CHAT_COMPLETIONS_PATH,
376
+ sessionId,
377
+ authToken,
378
+ requestHeaders,
379
+ model,
380
+ mode,
381
+ content,
382
+ signal,
383
+ onPacket,
384
+ onSessionId,
385
+ onDone,
386
+ onError
387
+ }) => {
388
+ try {
389
+ const headers = {
390
+ Authorization: authToken,
391
+ "Content-Type": "application/json",
392
+ ...requestHeaders
393
+ };
394
+ if (sessionId) {
395
+ headers["X-Session-ID"] = sessionId;
396
+ }
397
+ const response = await fetch(`${apiBaseUrl}${endpointPath}`, {
398
+ method: "POST",
399
+ headers,
400
+ body: JSON.stringify({
401
+ model,
402
+ mode,
403
+ stream: true,
404
+ messages: [{ role: "user", content }]
405
+ }),
406
+ signal
407
+ });
408
+ const contentType = response.headers.get("content-type") ?? "";
409
+ if (!response.ok) {
410
+ throw new Error(`HTTP ${response.status}`);
411
+ }
412
+ if (!response.body || !contentType.includes(SSE_CONTENT_TYPE)) {
413
+ throw new Error("Expected SSE response");
414
+ }
415
+ const responseSessionId = response.headers.get("X-Session-ID")?.trim();
416
+ if (responseSessionId) {
417
+ onSessionId?.(responseSessionId);
418
+ }
419
+ const reader = response.body.getReader();
420
+ const decoder = new TextDecoder("utf-8");
421
+ let buffer = "";
422
+ let isStreamClosed = false;
423
+ while (!isStreamClosed) {
424
+ const { value, done } = await reader.read();
425
+ if (done) {
426
+ isStreamClosed = true;
427
+ continue;
428
+ }
429
+ buffer += decoder.decode(value, { stream: true });
430
+ const events = buffer.split("\n\n");
431
+ buffer = events.pop() ?? "";
432
+ for (const eventText of events) {
433
+ const packet = parseSseEvent(eventText);
434
+ if (!packet)
435
+ continue;
436
+ if (packet.type === "stream_end" && packet.data === "[DONE]") {
437
+ onDone?.();
438
+ return;
439
+ }
440
+ if (packet.type === "error") {
441
+ throw new Error(
442
+ typeof packet.data === "string" ? packet.data : "message" in packet.data ? packet.data.message || "stream error" : "stream error"
443
+ );
444
+ }
445
+ onPacket(packet);
446
+ }
447
+ }
448
+ } catch (error) {
449
+ if (error instanceof DOMException && error.name === "AbortError") {
450
+ return;
451
+ }
452
+ const normalizedError = error instanceof Error ? error : new Error("stream error");
453
+ onError?.(normalizedError);
454
+ throw normalizedError;
455
+ }
456
+ };
457
+
458
+ // src/transport/default-chat-transport.ts
459
+ var DEFAULT_CHAT_TRANSPORT_ENDPOINTS = {
460
+ models: "/models",
461
+ completions: "/chat/completions",
462
+ terminate: "/chat/terminate"
463
+ };
464
+ var createDefaultChatTransport = ({
465
+ apiBaseUrl,
466
+ authToken,
467
+ streamHeaders,
468
+ transformStreamPacket,
469
+ endpoints,
470
+ axiosInstance
471
+ }) => {
472
+ const client = axiosInstance ?? axios.create({ baseURL: apiBaseUrl, headers: { Authorization: authToken } });
473
+ const resolvedEndpoints = {
474
+ ...DEFAULT_CHAT_TRANSPORT_ENDPOINTS,
475
+ ...endpoints
476
+ };
477
+ return {
478
+ getModels: () => getChatModels(client, resolvedEndpoints.models),
479
+ startStream: async ({
480
+ sessionId,
481
+ model,
482
+ mode,
483
+ content,
484
+ signal,
485
+ onUpdate,
486
+ onSessionId,
487
+ onDone,
488
+ onError
489
+ }) => {
490
+ await startChatStream({
491
+ apiBaseUrl,
492
+ endpointPath: resolvedEndpoints.completions,
493
+ sessionId,
494
+ authToken,
495
+ requestHeaders: streamHeaders,
496
+ model,
497
+ mode,
498
+ content,
499
+ signal,
500
+ onSessionId,
501
+ onDone,
502
+ onError,
503
+ onPacket: (packet) => {
504
+ const defaultUpdate = extractChatStreamUpdate(packet);
505
+ const nextUpdate = transformStreamPacket?.({ packet, defaultUpdate }) ?? defaultUpdate;
506
+ if (!nextUpdate) {
507
+ return;
508
+ }
509
+ onUpdate(nextUpdate);
510
+ }
511
+ });
512
+ },
513
+ terminateStream: (sessionId) => terminateChat(client, sessionId, resolvedEndpoints.terminate)
514
+ };
515
+ };
516
+
517
+ // src/components/ai-chat-provider/index.tsx
518
+ import { jsx } from "@emotion/react/jsx-runtime";
519
+ var AiChatProvider = (props) => {
520
+ const { defaultMode, labels, renderMessageBlock, enableImageAttachments = true, children } = props;
521
+ const [store] = useState(
522
+ () => createChatStore(defaultMode ? { preferredMode: defaultMode } : void 0)
523
+ );
524
+ const sendRef = useRef(async () => {
525
+ });
526
+ const retryRef = useRef(async () => {
527
+ });
528
+ const defaultApiBaseUrl = "apiBaseUrl" in props ? props.apiBaseUrl : void 0;
529
+ const defaultAuthToken = "authToken" in props ? props.authToken : void 0;
530
+ const defaultTransformStreamPacket = "transformStreamPacket" in props ? props.transformStreamPacket : void 0;
531
+ const customTransport = "transport" in props ? props.transport : void 0;
532
+ const axiosInstance = useMemo(() => {
533
+ if (!defaultApiBaseUrl || !defaultAuthToken) {
534
+ return void 0;
535
+ }
536
+ return axios2.create({
537
+ baseURL: defaultApiBaseUrl,
538
+ headers: { Authorization: defaultAuthToken }
539
+ });
540
+ }, [defaultApiBaseUrl, defaultAuthToken]);
541
+ const transport = useMemo(() => {
542
+ if (customTransport) {
543
+ return customTransport;
544
+ }
545
+ if (!defaultApiBaseUrl || !defaultAuthToken) {
546
+ throw new Error("AiChatProvider requires either transport or apiBaseUrl + authToken");
547
+ }
548
+ return createDefaultChatTransport({
549
+ apiBaseUrl: defaultApiBaseUrl,
550
+ authToken: defaultAuthToken,
551
+ transformStreamPacket: defaultTransformStreamPacket,
552
+ axiosInstance
553
+ });
554
+ }, [
555
+ axiosInstance,
556
+ customTransport,
557
+ defaultApiBaseUrl,
558
+ defaultAuthToken,
559
+ defaultTransformStreamPacket
560
+ ]);
561
+ const contextValue = useMemo(
562
+ () => ({
563
+ store,
564
+ transport,
565
+ axios: axiosInstance,
566
+ apiBaseUrl: defaultApiBaseUrl,
567
+ authToken: defaultAuthToken,
568
+ labels: { ...DEFAULT_AI_CHAT_LABELS, ...labels },
569
+ sendRef,
570
+ retryRef,
571
+ renderMessageBlock,
572
+ transformStreamPacket: defaultTransformStreamPacket,
573
+ enableImageAttachments
574
+ }),
575
+ [
576
+ axiosInstance,
577
+ defaultApiBaseUrl,
578
+ defaultAuthToken,
579
+ defaultTransformStreamPacket,
580
+ enableImageAttachments,
581
+ labels,
582
+ renderMessageBlock,
583
+ sendRef,
584
+ retryRef,
585
+ store,
586
+ transport
587
+ ]
588
+ );
589
+ return /* @__PURE__ */ jsx(ChatContext.Provider, { value: contextValue, children });
590
+ };
591
+
592
+ // src/components/chat-thread/index.tsx
593
+ import { useCallback as useCallback2, useLayoutEffect, useMemo as useMemo3, useRef as useRef4, useState as useState5 } from "react";
594
+ import styled10 from "@emotion/styled";
595
+
596
+ // src/context/use-chat-context.ts
597
+ import { useContext } from "react";
598
+ import { useStore } from "zustand/react";
599
+ var useChatContext = () => {
600
+ const ctx = useContext(ChatContext);
601
+ if (!ctx)
602
+ throw new Error("useChatContext must be used inside AiChatProvider");
603
+ return ctx;
604
+ };
605
+ var useChatStore = (selector) => {
606
+ const { store } = useChatContext();
607
+ return useStore(store, selector);
608
+ };
609
+
610
+ // src/components/chat-thread/lib/chat-thread.ts
611
+ var CHAT_THREAD_SCROLL_TOP_GAP = 16;
612
+ var findLatestUserMessageId = (historyMessages) => {
613
+ for (let index = historyMessages.length - 1; index >= 0; index -= 1) {
614
+ if (historyMessages[index]?.role === "user") {
615
+ return historyMessages[index]?.id;
616
+ }
617
+ }
618
+ return void 0;
619
+ };
620
+ var calculateChatThreadScrollSpacerHeight = ({
621
+ containerClientHeight,
622
+ containerScrollHeight,
623
+ targetOffsetTop
624
+ }) => Math.max(
625
+ 0,
626
+ targetOffsetTop - CHAT_THREAD_SCROLL_TOP_GAP - (containerScrollHeight - containerClientHeight)
627
+ );
628
+
629
+ // src/components/chat-thread/components/chat-message-item.tsx
630
+ import { Fragment, memo, useState as useState4 } from "react";
631
+ import styled7 from "@emotion/styled";
632
+ import { keyframes } from "@emotion/react";
633
+ import ReactMarkdown from "react-markdown";
634
+ import remarkGfm from "remark-gfm";
635
+ import remarkMath from "remark-math";
636
+ import rehypeKatex from "rehype-katex";
637
+
638
+ // src/components/chat-thread/hooks/use-chat-message-reveal.ts
639
+ import { useCallback, useEffect, useMemo as useMemo2, useRef as useRef2, useState as useState2 } from "react";
640
+
641
+ // src/components/chat-thread/lib/message-reveal.ts
642
+ var STREAM_REVEAL_TICK_MS = 36;
643
+ var STREAM_FRESH_BLOCK_SETTLE_MS = 180;
644
+ var STREAM_INPUT_BATCH_WINDOW_MS = 48;
645
+ var getRevealStep = ({
646
+ backlogUnits,
647
+ isStreaming,
648
+ totalUnits
649
+ }) => {
650
+ if (!isStreaming) {
651
+ return Math.max(24, Math.ceil(backlogUnits / 2));
652
+ }
653
+ if (backlogUnits >= 240) {
654
+ return Math.max(24, Math.ceil(backlogUnits / 8));
655
+ }
656
+ if (backlogUnits >= 120) {
657
+ return Math.max(10, Math.ceil(backlogUnits / 12));
658
+ }
659
+ if (backlogUnits >= 48) {
660
+ return Math.max(2, Math.ceil(backlogUnits / 24));
661
+ }
662
+ if (totalUnits >= 200 && backlogUnits >= 24) {
663
+ return 4;
664
+ }
665
+ return 1;
666
+ };
667
+ var getNextDisplayedUnitCount = ({
668
+ currentUnits,
669
+ targetUnits,
670
+ isStreaming,
671
+ minimumStep = 1
672
+ }) => {
673
+ if (currentUnits >= targetUnits) {
674
+ return currentUnits;
675
+ }
676
+ const revealStep = getRevealStep({
677
+ backlogUnits: targetUnits - currentUnits,
678
+ isStreaming,
679
+ totalUnits: targetUnits
680
+ });
681
+ return Math.min(targetUnits, currentUnits + Math.max(minimumStep, revealStep));
682
+ };
683
+ var splitMarkdownBlocks = (content) => content.split(/\n{2,}/).map((block) => block.trim()).filter(Boolean);
684
+
685
+ // src/components/chat-thread/hooks/use-chat-message-reveal.ts
686
+ var useChatMessageReveal = (message) => {
687
+ const isAssistantStreaming = message.role === "assistant" && message.status === "streaming";
688
+ const targetContent = message.content || "";
689
+ const targetUnits = useMemo2(() => Array.from(targetContent), [targetContent]);
690
+ const resetSnapshot = useMemo2(
691
+ () => ({
692
+ messageId: message.id,
693
+ initialTargetUnitCount: targetUnits.length,
694
+ initialBatchedTargetUnitCount: isAssistantStreaming ? 0 : targetUnits.length
695
+ }),
696
+ [isAssistantStreaming, message.id, targetUnits.length]
697
+ );
698
+ const pendingTargetUnitCountRef = useRef2(targetUnits.length);
699
+ const batchedTargetUnitCountRef = useRef2(isAssistantStreaming ? 0 : targetUnits.length);
700
+ const inputBatchTimeoutRef = useRef2(null);
701
+ const commitAnimationFrameRef = useRef2(null);
702
+ const freshBlockActivationFrameRef = useRef2(null);
703
+ const displayedUnitSyncFrameRef = useRef2(null);
704
+ const resetAnimationFrameRef = useRef2(null);
705
+ const [batchedTargetUnitCount, setBatchedTargetUnitCount] = useState2(
706
+ () => isAssistantStreaming ? 0 : targetUnits.length
707
+ );
708
+ const lastDisplayedBlockCountRef = useRef2(0);
709
+ const [displayedUnitCount, setDisplayedUnitCount] = useState2(
710
+ () => isAssistantStreaming ? 0 : targetUnits.length
711
+ );
712
+ const [isFreshBlockActive, setIsFreshBlockActive] = useState2(false);
713
+ const commitBatchedTargetUnitCount = useCallback(
714
+ (nextTargetUnitCount) => {
715
+ batchedTargetUnitCountRef.current = nextTargetUnitCount;
716
+ setBatchedTargetUnitCount(nextTargetUnitCount);
717
+ setDisplayedUnitCount(
718
+ (current) => message.role === "assistant" ? getNextDisplayedUnitCount({
719
+ currentUnits: current,
720
+ targetUnits: nextTargetUnitCount,
721
+ isStreaming: isAssistantStreaming,
722
+ minimumStep: current > 0 && isAssistantStreaming ? 2 : 1
723
+ }) : nextTargetUnitCount
724
+ );
725
+ },
726
+ [isAssistantStreaming, message.role]
727
+ );
728
+ const scheduleBatchedTargetUnitCountCommit = useCallback(
729
+ (nextTargetUnitCount) => {
730
+ if (commitAnimationFrameRef.current !== null) {
731
+ window.cancelAnimationFrame(commitAnimationFrameRef.current);
732
+ }
733
+ commitAnimationFrameRef.current = window.requestAnimationFrame(() => {
734
+ commitAnimationFrameRef.current = null;
735
+ commitBatchedTargetUnitCount(nextTargetUnitCount);
736
+ });
737
+ },
738
+ [commitBatchedTargetUnitCount]
739
+ );
740
+ useEffect(() => {
741
+ pendingTargetUnitCountRef.current = resetSnapshot.initialTargetUnitCount;
742
+ batchedTargetUnitCountRef.current = resetSnapshot.initialBatchedTargetUnitCount;
743
+ lastDisplayedBlockCountRef.current = 0;
744
+ if (inputBatchTimeoutRef.current !== null) {
745
+ window.clearTimeout(inputBatchTimeoutRef.current);
746
+ inputBatchTimeoutRef.current = null;
747
+ }
748
+ if (commitAnimationFrameRef.current !== null) {
749
+ window.cancelAnimationFrame(commitAnimationFrameRef.current);
750
+ commitAnimationFrameRef.current = null;
751
+ }
752
+ if (freshBlockActivationFrameRef.current !== null) {
753
+ window.cancelAnimationFrame(freshBlockActivationFrameRef.current);
754
+ freshBlockActivationFrameRef.current = null;
755
+ }
756
+ if (displayedUnitSyncFrameRef.current !== null) {
757
+ window.cancelAnimationFrame(displayedUnitSyncFrameRef.current);
758
+ displayedUnitSyncFrameRef.current = null;
759
+ }
760
+ if (resetAnimationFrameRef.current !== null) {
761
+ window.cancelAnimationFrame(resetAnimationFrameRef.current);
762
+ }
763
+ resetAnimationFrameRef.current = window.requestAnimationFrame(() => {
764
+ resetAnimationFrameRef.current = null;
765
+ setBatchedTargetUnitCount(resetSnapshot.initialBatchedTargetUnitCount);
766
+ setDisplayedUnitCount(resetSnapshot.initialBatchedTargetUnitCount);
767
+ setIsFreshBlockActive(false);
768
+ });
769
+ return () => {
770
+ if (resetAnimationFrameRef.current !== null) {
771
+ window.cancelAnimationFrame(resetAnimationFrameRef.current);
772
+ resetAnimationFrameRef.current = null;
773
+ }
774
+ };
775
+ }, [resetSnapshot]);
776
+ useEffect(() => {
777
+ pendingTargetUnitCountRef.current = targetUnits.length;
778
+ if (message.role !== "assistant" || !isAssistantStreaming) {
779
+ if (inputBatchTimeoutRef.current !== null) {
780
+ window.clearTimeout(inputBatchTimeoutRef.current);
781
+ inputBatchTimeoutRef.current = null;
782
+ }
783
+ scheduleBatchedTargetUnitCountCommit(targetUnits.length);
784
+ return;
785
+ }
786
+ if (targetUnits.length <= batchedTargetUnitCountRef.current) {
787
+ return;
788
+ }
789
+ if (batchedTargetUnitCountRef.current === 0) {
790
+ scheduleBatchedTargetUnitCountCommit(targetUnits.length);
791
+ return;
792
+ }
793
+ if (inputBatchTimeoutRef.current !== null) {
794
+ return;
795
+ }
796
+ inputBatchTimeoutRef.current = window.setTimeout(() => {
797
+ inputBatchTimeoutRef.current = null;
798
+ commitBatchedTargetUnitCount(pendingTargetUnitCountRef.current);
799
+ }, STREAM_INPUT_BATCH_WINDOW_MS);
800
+ return () => {
801
+ if (inputBatchTimeoutRef.current !== null) {
802
+ window.clearTimeout(inputBatchTimeoutRef.current);
803
+ inputBatchTimeoutRef.current = null;
804
+ }
805
+ };
806
+ }, [
807
+ commitBatchedTargetUnitCount,
808
+ isAssistantStreaming,
809
+ message.role,
810
+ scheduleBatchedTargetUnitCountCommit,
811
+ targetUnits.length
812
+ ]);
813
+ const displayedContent = useMemo2(
814
+ () => targetUnits.slice(0, displayedUnitCount).join(""),
815
+ [displayedUnitCount, targetUnits]
816
+ );
817
+ const contentBlocks = useMemo2(() => splitMarkdownBlocks(displayedContent), [displayedContent]);
818
+ useEffect(() => {
819
+ const hasNewDisplayedBlock = message.role === "assistant" && contentBlocks.length > 1 && contentBlocks.length > lastDisplayedBlockCountRef.current;
820
+ lastDisplayedBlockCountRef.current = contentBlocks.length;
821
+ if (!hasNewDisplayedBlock) {
822
+ return;
823
+ }
824
+ freshBlockActivationFrameRef.current = window.requestAnimationFrame(() => {
825
+ freshBlockActivationFrameRef.current = null;
826
+ setIsFreshBlockActive(true);
827
+ });
828
+ const timer = window.setTimeout(() => {
829
+ setIsFreshBlockActive(false);
830
+ }, STREAM_FRESH_BLOCK_SETTLE_MS);
831
+ return () => {
832
+ if (freshBlockActivationFrameRef.current !== null) {
833
+ window.cancelAnimationFrame(freshBlockActivationFrameRef.current);
834
+ freshBlockActivationFrameRef.current = null;
835
+ }
836
+ window.clearTimeout(timer);
837
+ };
838
+ }, [contentBlocks.length, message.role]);
839
+ useEffect(() => {
840
+ const shouldAnimateReveal = message.role === "assistant" && displayedUnitCount < batchedTargetUnitCount && (isAssistantStreaming || displayedUnitCount > 0);
841
+ if (!shouldAnimateReveal) {
842
+ if (displayedUnitCount !== batchedTargetUnitCount) {
843
+ displayedUnitSyncFrameRef.current = window.requestAnimationFrame(() => {
844
+ displayedUnitSyncFrameRef.current = null;
845
+ setDisplayedUnitCount(batchedTargetUnitCount);
846
+ });
847
+ }
848
+ return () => {
849
+ if (displayedUnitSyncFrameRef.current !== null) {
850
+ window.cancelAnimationFrame(displayedUnitSyncFrameRef.current);
851
+ displayedUnitSyncFrameRef.current = null;
852
+ }
853
+ };
854
+ }
855
+ const timer = window.setInterval(() => {
856
+ setDisplayedUnitCount((current) => {
857
+ if (current >= batchedTargetUnitCount) {
858
+ window.clearInterval(timer);
859
+ return current;
860
+ }
861
+ return Math.min(
862
+ batchedTargetUnitCount,
863
+ getNextDisplayedUnitCount({
864
+ currentUnits: current,
865
+ targetUnits: batchedTargetUnitCount,
866
+ isStreaming: isAssistantStreaming
867
+ })
868
+ );
869
+ });
870
+ }, STREAM_REVEAL_TICK_MS);
871
+ return () => {
872
+ window.clearInterval(timer);
873
+ };
874
+ }, [batchedTargetUnitCount, displayedUnitCount, isAssistantStreaming, message.role]);
875
+ const settledContent = isFreshBlockActive ? contentBlocks.slice(0, -1).join("\n\n") : displayedContent;
876
+ const freshContent = isFreshBlockActive ? contentBlocks.at(-1) ?? "" : "";
877
+ return {
878
+ isAssistantStreaming,
879
+ displayedContent,
880
+ settledContent,
881
+ freshContent
882
+ };
883
+ };
884
+
885
+ // src/components/chat-thread/components/pde-ai-execution-confirmation-card.tsx
886
+ import styled from "@emotion/styled";
887
+ import { jsx as jsx2, jsxs } from "@emotion/react/jsx-runtime";
888
+ var PDEAIExecutionConfirmationCard = ({
889
+ proposal,
890
+ interactive = false,
891
+ onApply,
892
+ onConfirm,
893
+ onRevise
894
+ }) => {
895
+ return /* @__PURE__ */ jsxs(Card, { "data-testid": "pde-ai-execution-confirmation-card", children: [
896
+ /* @__PURE__ */ jsxs(Header, { children: [
897
+ /* @__PURE__ */ jsx2(Eyebrow, { children: "Execution Proposal" }),
898
+ /* @__PURE__ */ jsx2(Title, { children: proposal.equationName }),
899
+ proposal.solverName ? /* @__PURE__ */ jsx2(Subtitle, { children: proposal.solverName }) : null
900
+ ] }),
901
+ /* @__PURE__ */ jsx2(SummaryList, { children: proposal.parameterSummary.map((item) => /* @__PURE__ */ jsxs(SummaryItem, { children: [
902
+ /* @__PURE__ */ jsx2(SummaryLabel, { children: item.label }),
903
+ /* @__PURE__ */ jsx2(SummaryValue, { children: item.value })
904
+ ] }, `${item.fieldPath ?? item.label}-${item.value}`)) }),
905
+ proposal.warnings?.length ? /* @__PURE__ */ jsx2(Warnings, { children: proposal.warnings.map((warning) => /* @__PURE__ */ jsx2(Warning, { children: warning }, warning)) }) : null,
906
+ interactive ? /* @__PURE__ */ jsxs(Actions, { children: [
907
+ onApply && /* @__PURE__ */ jsx2(ActionButton, { type: "button", "data-testid": "pde-ai-confirmation-apply", onClick: onApply, children: "Apply to Parameters" }),
908
+ onConfirm && /* @__PURE__ */ jsx2(
909
+ ActionButton,
910
+ {
911
+ type: "button",
912
+ "data-testid": "pde-ai-confirmation-confirm",
913
+ onClick: onConfirm,
914
+ children: "Confirm Execution"
915
+ }
916
+ ),
917
+ onRevise && /* @__PURE__ */ jsx2(
918
+ SecondaryActionButton,
919
+ {
920
+ type: "button",
921
+ "data-testid": "pde-ai-confirmation-revise",
922
+ onClick: onRevise,
923
+ children: "Revise Plan"
924
+ }
925
+ )
926
+ ] }) : null
927
+ ] });
928
+ };
929
+ var Card = styled.section`
930
+ display: grid;
931
+ gap: 14px;
932
+ padding: 16px;
933
+ border-radius: 20px;
934
+ border: 1px solid rgba(126, 160, 255, 0.2);
935
+ background:
936
+ linear-gradient(180deg, rgba(73, 98, 188, 0.16) 0%, rgba(31, 35, 47, 0.42) 100%),
937
+ rgba(255, 255, 255, 0.03);
938
+ `;
939
+ var Header = styled.div`
940
+ display: grid;
941
+ gap: 4px;
942
+ `;
943
+ var Eyebrow = styled.span`
944
+ color: rgba(255, 255, 255, 0.58);
945
+ font-size: 11px;
946
+ font-weight: 700;
947
+ letter-spacing: 0.08em;
948
+ text-transform: uppercase;
949
+ `;
950
+ var Title = styled.strong`
951
+ color: rgba(255, 255, 255, 0.95);
952
+ font-size: 16px;
953
+ line-height: 1.4;
954
+ `;
955
+ var Subtitle = styled.span`
956
+ color: rgba(255, 255, 255, 0.68);
957
+ font-size: 13px;
958
+ `;
959
+ var SummaryList = styled.div`
960
+ display: grid;
961
+ gap: 8px;
962
+ `;
963
+ var SummaryItem = styled.div`
964
+ display: flex;
965
+ justify-content: space-between;
966
+ gap: 12px;
967
+ padding: 10px 12px;
968
+ border-radius: 14px;
969
+ background: rgba(255, 255, 255, 0.05);
970
+ `;
971
+ var SummaryLabel = styled.span`
972
+ color: rgba(255, 255, 255, 0.72);
973
+ font-size: 13px;
974
+ `;
975
+ var SummaryValue = styled.span`
976
+ color: rgba(255, 255, 255, 0.94);
977
+ font-size: 13px;
978
+ font-weight: 600;
979
+ `;
980
+ var Warnings = styled.div`
981
+ display: grid;
982
+ gap: 8px;
983
+ `;
984
+ var Warning = styled.div`
985
+ padding: 10px 12px;
986
+ border-radius: 14px;
987
+ border: 1px solid rgba(255, 193, 92, 0.24);
988
+ background: rgba(255, 181, 71, 0.12);
989
+ color: rgba(255, 241, 211, 0.92);
990
+ font-size: 13px;
991
+ `;
992
+ var Actions = styled.div`
993
+ display: flex;
994
+ flex-wrap: wrap;
995
+ gap: 10px;
996
+ `;
997
+ var ActionButton = styled.button`
998
+ border: none;
999
+ border-radius: 999px;
1000
+ background: linear-gradient(180deg, #7ea0ff 0%, #4a6fff 100%);
1001
+ color: #081127;
1002
+ font-size: 12px;
1003
+ font-weight: 700;
1004
+ padding: 10px 14px;
1005
+ cursor: pointer;
1006
+
1007
+ &:disabled {
1008
+ cursor: default;
1009
+ opacity: 0.48;
1010
+ }
1011
+ `;
1012
+ var SecondaryActionButton = styled(ActionButton)`
1013
+ background: rgba(255, 255, 255, 0.08);
1014
+ color: rgba(255, 255, 255, 0.9);
1015
+ border: 1px solid rgba(255, 255, 255, 0.14);
1016
+ `;
1017
+
1018
+ // src/components/chat-thread/components/pde-ai-notice-card.tsx
1019
+ import styled2 from "@emotion/styled";
1020
+ import { jsx as jsx3 } from "@emotion/react/jsx-runtime";
1021
+ var PDEAINoticeCard = ({ text, tone }) => /* @__PURE__ */ jsx3(Card2, { "data-testid": "pde-ai-notice-card", "data-tone": tone, children: text });
1022
+ var Card2 = styled2.div`
1023
+ padding: 12px 14px;
1024
+ border-radius: 16px;
1025
+ border: 1px solid rgba(255, 255, 255, 0.1);
1026
+ background: rgba(255, 255, 255, 0.04);
1027
+ color: rgba(255, 255, 255, 0.9);
1028
+ font-size: 13px;
1029
+ line-height: 1.5;
1030
+
1031
+ &[data-tone='info'] {
1032
+ border-color: rgba(105, 170, 255, 0.26);
1033
+ background: rgba(68, 118, 255, 0.12);
1034
+ }
1035
+
1036
+ &[data-tone='warning'] {
1037
+ border-color: rgba(255, 193, 92, 0.28);
1038
+ background: rgba(255, 181, 71, 0.12);
1039
+ }
1040
+
1041
+ &[data-tone='success'] {
1042
+ border-color: rgba(84, 214, 160, 0.28);
1043
+ background: rgba(43, 182, 120, 0.12);
1044
+ }
1045
+ `;
1046
+
1047
+ // src/components/chat-thread/components/pde-ai-parameter-summary-card.tsx
1048
+ import styled3 from "@emotion/styled";
1049
+ import { jsx as jsx4, jsxs as jsxs2 } from "@emotion/react/jsx-runtime";
1050
+ var PDEAIParameterSummaryCard = ({ items }) => /* @__PURE__ */ jsxs2(Card3, { "data-testid": "pde-ai-parameter-summary-card", children: [
1051
+ /* @__PURE__ */ jsx4(Title2, { children: "Parameter Summary" }),
1052
+ /* @__PURE__ */ jsx4(List, { children: items.map((item) => /* @__PURE__ */ jsxs2(ListItem, { children: [
1053
+ /* @__PURE__ */ jsx4(Label, { children: item.label }),
1054
+ /* @__PURE__ */ jsx4(Value, { children: item.value })
1055
+ ] }, `${item.fieldPath ?? item.label}-${item.value}`)) })
1056
+ ] });
1057
+ var Card3 = styled3.section`
1058
+ padding: 14px;
1059
+ border-radius: 18px;
1060
+ border: 1px solid rgba(255, 255, 255, 0.08);
1061
+ background: rgba(255, 255, 255, 0.03);
1062
+ `;
1063
+ var Title2 = styled3.div`
1064
+ margin-bottom: 10px;
1065
+ color: rgba(255, 255, 255, 0.62);
1066
+ font-size: 11px;
1067
+ font-weight: 700;
1068
+ letter-spacing: 0.08em;
1069
+ text-transform: uppercase;
1070
+ `;
1071
+ var List = styled3.div`
1072
+ display: grid;
1073
+ gap: 10px;
1074
+ `;
1075
+ var ListItem = styled3.div`
1076
+ display: flex;
1077
+ justify-content: space-between;
1078
+ gap: 12px;
1079
+ padding: 10px 12px;
1080
+ border-radius: 14px;
1081
+ background: rgba(255, 255, 255, 0.04);
1082
+ `;
1083
+ var Label = styled3.span`
1084
+ color: rgba(255, 255, 255, 0.72);
1085
+ font-size: 13px;
1086
+ `;
1087
+ var Value = styled3.span`
1088
+ color: rgba(255, 255, 255, 0.94);
1089
+ font-size: 13px;
1090
+ font-weight: 600;
1091
+ text-align: right;
1092
+ `;
1093
+
1094
+ // src/components/chat-thread/components/pde-ai-questionnaire-card.tsx
1095
+ import { useState as useState3 } from "react";
1096
+ import styled4 from "@emotion/styled";
1097
+ import { jsx as jsx5, jsxs as jsxs3 } from "@emotion/react/jsx-runtime";
1098
+ var OTHER_OPTION_VALUE = "__other__";
1099
+ var createInitialAnswers = (questionnaire) => ({
1100
+ ...questionnaire.answers ?? {}
1101
+ });
1102
+ var getMultiSelectAnswerValues = (answer) => Array.isArray(answer) ? answer : [];
1103
+ var getSingleSelectDraftState = (question, answer) => {
1104
+ if (typeof answer !== "string") {
1105
+ return {
1106
+ selectedValue: void 0,
1107
+ otherValue: ""
1108
+ };
1109
+ }
1110
+ const matchesOption = question.options.some((option) => option.value === answer);
1111
+ return {
1112
+ selectedValue: matchesOption ? answer : question.allowOther ? OTHER_OPTION_VALUE : void 0,
1113
+ otherValue: matchesOption ? "" : answer
1114
+ };
1115
+ };
1116
+ var updateAnswerValue = (current, questionId, value) => ({
1117
+ ...current,
1118
+ [questionId]: value
1119
+ });
1120
+ var toggleMultiSelectAnswer = (current, questionId, optionValue) => {
1121
+ const currentValues = getMultiSelectAnswerValues(current[questionId]);
1122
+ const nextValues = currentValues.includes(optionValue) ? currentValues.filter((value) => value !== optionValue) : [...currentValues, optionValue];
1123
+ return updateAnswerValue(current, questionId, nextValues);
1124
+ };
1125
+ var getTextInputValue = (answer) => typeof answer === "string" ? String(answer) : "";
1126
+ var getNumberInputValue = (answer) => typeof answer === "number" || typeof answer === "string" ? String(answer) : "";
1127
+ var getOptionChoiceLabel = (index) => {
1128
+ if (index < 26) {
1129
+ return String.fromCharCode(65 + index);
1130
+ }
1131
+ return String(index + 1);
1132
+ };
1133
+ var isMissingRequiredAnswer = (question, answers) => {
1134
+ const answer = answers[question.id];
1135
+ switch (question.kind) {
1136
+ case "boolean":
1137
+ return typeof answer !== "boolean";
1138
+ case "multi_select":
1139
+ return !Array.isArray(answer) || answer.length === 0;
1140
+ case "number":
1141
+ return typeof answer !== "number" || Number.isNaN(answer);
1142
+ case "text":
1143
+ case "single_select":
1144
+ return typeof answer !== "string" || answer.trim() === "";
1145
+ default:
1146
+ return true;
1147
+ }
1148
+ };
1149
+ var formatQuestionAnswer = (question, answer) => {
1150
+ switch (question.kind) {
1151
+ case "multi_select": {
1152
+ if (!Array.isArray(answer)) {
1153
+ return "";
1154
+ }
1155
+ return answer.map((value) => question.options.find((option) => option.value === value)?.label ?? value).join(", ");
1156
+ }
1157
+ case "single_select": {
1158
+ if (typeof answer !== "string") {
1159
+ return "";
1160
+ }
1161
+ const matchedOption = question.options.find((option) => option.value === answer);
1162
+ return matchedOption?.label ?? answer;
1163
+ }
1164
+ case "text":
1165
+ return String(answer);
1166
+ case "number":
1167
+ return `${answer}${question.unit ? ` ${question.unit}` : ""}`;
1168
+ case "boolean":
1169
+ return answer ? question.trueLabel ?? "Yes" : question.falseLabel ?? "No";
1170
+ default:
1171
+ return String(answer);
1172
+ }
1173
+ };
1174
+ var normalizeQuestionAnswer = (question, answer) => {
1175
+ if (answer === void 0) {
1176
+ return void 0;
1177
+ }
1178
+ switch (question.kind) {
1179
+ case "multi_select":
1180
+ return Array.isArray(answer) && answer.length > 0 ? answer : void 0;
1181
+ case "single_select":
1182
+ case "text":
1183
+ return typeof answer === "string" && answer.trim() !== "" ? answer : void 0;
1184
+ case "number":
1185
+ return typeof answer === "number" && !Number.isNaN(answer) ? answer : void 0;
1186
+ case "boolean":
1187
+ return typeof answer === "boolean" ? answer : void 0;
1188
+ default:
1189
+ return answer;
1190
+ }
1191
+ };
1192
+ var PDEAIQuestionnaireCardInner = ({
1193
+ questionnaire,
1194
+ interactive = false,
1195
+ onSubmit
1196
+ }) => {
1197
+ const [answers, setAnswers] = useState3(
1198
+ () => createInitialAnswers(questionnaire)
1199
+ );
1200
+ const [errorMessage, setErrorMessage] = useState3(null);
1201
+ const handleSubmit = () => {
1202
+ const missingQuestions = questionnaire.questions.filter(
1203
+ (question) => question.required && isMissingRequiredAnswer(question, answers)
1204
+ );
1205
+ if (missingQuestions.length > 0) {
1206
+ setErrorMessage(
1207
+ `Please complete: ${missingQuestions.map((question) => question.label).join(", ")}`
1208
+ );
1209
+ return;
1210
+ }
1211
+ setErrorMessage(null);
1212
+ const normalizedAnswers = Object.fromEntries(
1213
+ questionnaire.questions.flatMap((question) => {
1214
+ const value = normalizeQuestionAnswer(question, answers[question.id]);
1215
+ return value === void 0 ? [] : [[question.id, value]];
1216
+ })
1217
+ );
1218
+ const contentLines = [
1219
+ questionnaire.title ?? "Questionnaire responses",
1220
+ ...questionnaire.questions.flatMap((question) => {
1221
+ const value = normalizedAnswers[question.id];
1222
+ if (value === void 0) {
1223
+ return [];
1224
+ }
1225
+ return [`- ${question.label}: ${formatQuestionAnswer(question, value)}`];
1226
+ })
1227
+ ];
1228
+ onSubmit?.({
1229
+ questionnaireId: questionnaire.questionnaireId,
1230
+ answers: normalizedAnswers,
1231
+ content: contentLines.join("\n")
1232
+ });
1233
+ };
1234
+ const renderQuestion = (question) => {
1235
+ const renderOptionChoice = ({
1236
+ questionId,
1237
+ optionLabel,
1238
+ index,
1239
+ isSelected,
1240
+ onClick,
1241
+ inlineInput,
1242
+ tone = "default"
1243
+ }) => /* @__PURE__ */ jsxs3(
1244
+ OptionChoiceItem,
1245
+ {
1246
+ role: "button",
1247
+ tabIndex: interactive ? 0 : -1,
1248
+ "aria-pressed": isSelected,
1249
+ "data-selected": isSelected,
1250
+ "data-tone": tone,
1251
+ "data-testid": `pde-ai-question-option-${questionId}-${index}`,
1252
+ onClick: (event) => {
1253
+ if (!interactive) {
1254
+ return;
1255
+ }
1256
+ if (event.target instanceof HTMLElement && event.target.closest("input")) {
1257
+ return;
1258
+ }
1259
+ onClick();
1260
+ },
1261
+ onKeyDown: (event) => {
1262
+ if (!interactive) {
1263
+ return;
1264
+ }
1265
+ if (event.key !== "Enter" && event.key !== " ") {
1266
+ return;
1267
+ }
1268
+ event.preventDefault();
1269
+ onClick();
1270
+ },
1271
+ children: [
1272
+ /* @__PURE__ */ jsx5(OptionChoiceMarker, { "data-selected": isSelected, children: getOptionChoiceLabel(index) }),
1273
+ /* @__PURE__ */ jsxs3(OptionChoiceContent, { children: [
1274
+ inlineInput ? null : /* @__PURE__ */ jsx5(OptionChoiceLabel, { children: optionLabel }),
1275
+ inlineInput
1276
+ ] })
1277
+ ]
1278
+ },
1279
+ `${questionId}-${optionLabel}`
1280
+ );
1281
+ switch (question.kind) {
1282
+ case "multi_select":
1283
+ return /* @__PURE__ */ jsx5(QuestionBody, { children: /* @__PURE__ */ jsx5(OptionList, { children: question.options.map((option, index) => {
1284
+ const selectedValues = getMultiSelectAnswerValues(answers[question.id]);
1285
+ const isSelected = selectedValues.includes(option.value);
1286
+ return renderOptionChoice({
1287
+ questionId: question.id,
1288
+ optionLabel: option.label,
1289
+ index,
1290
+ isSelected,
1291
+ onClick: () => setAnswers(
1292
+ (current) => toggleMultiSelectAnswer(current, question.id, option.value)
1293
+ )
1294
+ });
1295
+ }) }) });
1296
+ case "single_select": {
1297
+ const singleSelectDraft = getSingleSelectDraftState(question, answers[question.id]);
1298
+ return /* @__PURE__ */ jsx5(QuestionBody, { children: /* @__PURE__ */ jsxs3(OptionList, { children: [
1299
+ question.options.map((option, index) => {
1300
+ const isSelected = singleSelectDraft.selectedValue === option.value;
1301
+ return renderOptionChoice({
1302
+ questionId: question.id,
1303
+ optionLabel: option.label,
1304
+ index,
1305
+ isSelected,
1306
+ onClick: () => setAnswers((current) => updateAnswerValue(current, question.id, option.value))
1307
+ });
1308
+ }),
1309
+ question.allowOther ? renderOptionChoice({
1310
+ questionId: question.id,
1311
+ optionLabel: "Other",
1312
+ index: question.options.length,
1313
+ isSelected: singleSelectDraft.selectedValue === OTHER_OPTION_VALUE,
1314
+ tone: "other",
1315
+ onClick: () => setAnswers(
1316
+ (current) => updateAnswerValue(current, question.id, singleSelectDraft.otherValue)
1317
+ ),
1318
+ inlineInput: singleSelectDraft.selectedValue === OTHER_OPTION_VALUE ? /* @__PURE__ */ jsx5(
1319
+ InlineOtherInput,
1320
+ {
1321
+ "data-testid": `pde-ai-question-input-${question.id}`,
1322
+ type: "text",
1323
+ value: singleSelectDraft.otherValue,
1324
+ placeholder: "Other",
1325
+ readOnly: !interactive,
1326
+ onClick: (event) => {
1327
+ event.stopPropagation();
1328
+ },
1329
+ onKeyDown: (event) => {
1330
+ event.stopPropagation();
1331
+ },
1332
+ onChange: (event) => {
1333
+ setAnswers(
1334
+ (current) => updateAnswerValue(current, question.id, event.target.value)
1335
+ );
1336
+ }
1337
+ }
1338
+ ) : null
1339
+ }) : null
1340
+ ] }) });
1341
+ }
1342
+ case "text":
1343
+ return /* @__PURE__ */ jsx5(QuestionBody, { children: /* @__PURE__ */ jsx5(
1344
+ TextInput,
1345
+ {
1346
+ "data-testid": `pde-ai-question-input-${question.id}`,
1347
+ type: "text",
1348
+ value: getTextInputValue(answers[question.id]),
1349
+ placeholder: question.placeholder,
1350
+ readOnly: !interactive,
1351
+ onChange: (event) => {
1352
+ setAnswers((current) => updateAnswerValue(current, question.id, event.target.value));
1353
+ }
1354
+ }
1355
+ ) });
1356
+ case "number":
1357
+ return /* @__PURE__ */ jsx5(QuestionBody, { children: /* @__PURE__ */ jsxs3(NumberInputRow, { children: [
1358
+ /* @__PURE__ */ jsx5(
1359
+ TextInput,
1360
+ {
1361
+ "data-testid": `pde-ai-question-input-${question.id}`,
1362
+ type: "number",
1363
+ value: getNumberInputValue(answers[question.id]),
1364
+ placeholder: question.placeholder,
1365
+ readOnly: !interactive,
1366
+ onChange: (event) => {
1367
+ setAnswers(
1368
+ (current) => updateAnswerValue(
1369
+ current,
1370
+ question.id,
1371
+ event.target.value === "" ? void 0 : Number(event.target.value)
1372
+ )
1373
+ );
1374
+ }
1375
+ }
1376
+ ),
1377
+ question.unit ? /* @__PURE__ */ jsx5(Unit, { children: question.unit }) : null
1378
+ ] }) });
1379
+ case "boolean":
1380
+ return /* @__PURE__ */ jsx5(QuestionBody, { children: /* @__PURE__ */ jsxs3(OptionList, { children: [
1381
+ renderOptionChoice({
1382
+ questionId: question.id,
1383
+ optionLabel: question.trueLabel ?? "Yes",
1384
+ index: 0,
1385
+ isSelected: answers[question.id] === true,
1386
+ onClick: () => setAnswers((current) => updateAnswerValue(current, question.id, true))
1387
+ }),
1388
+ renderOptionChoice({
1389
+ questionId: question.id,
1390
+ optionLabel: question.falseLabel ?? "No",
1391
+ index: 1,
1392
+ isSelected: answers[question.id] === false,
1393
+ onClick: () => setAnswers((current) => updateAnswerValue(current, question.id, false))
1394
+ })
1395
+ ] }) });
1396
+ default:
1397
+ return null;
1398
+ }
1399
+ };
1400
+ return /* @__PURE__ */ jsxs3(Card4, { "data-testid": "pde-ai-questionnaire-card", children: [
1401
+ questionnaire.title ? /* @__PURE__ */ jsx5(Title3, { children: questionnaire.title }) : null,
1402
+ questionnaire.description ? /* @__PURE__ */ jsx5(Description, { children: questionnaire.description }) : null,
1403
+ /* @__PURE__ */ jsx5(QuestionList, { children: questionnaire.questions.map((question) => /* @__PURE__ */ jsxs3(QuestionCard, { children: [
1404
+ /* @__PURE__ */ jsxs3(QuestionLabel, { children: [
1405
+ question.label,
1406
+ question.required ? /* @__PURE__ */ jsx5(Required, { children: "*" }) : null
1407
+ ] }),
1408
+ renderQuestion(question)
1409
+ ] }, question.id)) }),
1410
+ errorMessage ? /* @__PURE__ */ jsx5(ErrorMessage, { "data-testid": "pde-ai-questionnaire-error", children: errorMessage }) : null,
1411
+ interactive ? /* @__PURE__ */ jsx5(
1412
+ SubmitButton,
1413
+ {
1414
+ type: "button",
1415
+ "data-testid": "pde-ai-questionnaire-submit",
1416
+ onClick: handleSubmit,
1417
+ children: questionnaire.submitLabel ?? "Submit"
1418
+ }
1419
+ ) : null
1420
+ ] });
1421
+ };
1422
+ var getQuestionnaireStateKey = (questionnaire) => JSON.stringify([
1423
+ questionnaire.questionnaireId,
1424
+ questionnaire.answers ?? {},
1425
+ questionnaire.questions
1426
+ ]);
1427
+ var PDEAIQuestionnaireCard = (props) => /* @__PURE__ */ jsx5(PDEAIQuestionnaireCardInner, { ...props }, getQuestionnaireStateKey(props.questionnaire));
1428
+ var Card4 = styled4.section`
1429
+ display: grid;
1430
+ gap: 14px;
1431
+ padding: 16px;
1432
+ border-radius: 20px;
1433
+ border: 1px solid rgba(255, 255, 255, 0.08);
1434
+ background: rgba(255, 255, 255, 0.03);
1435
+ `;
1436
+ var Title3 = styled4.strong`
1437
+ color: rgba(255, 255, 255, 0.94);
1438
+ font-size: 16px;
1439
+ line-height: 1.4;
1440
+ `;
1441
+ var Description = styled4.p`
1442
+ margin: 0;
1443
+ color: rgba(255, 255, 255, 0.72);
1444
+ font-size: 13px;
1445
+ `;
1446
+ var QuestionList = styled4.div`
1447
+ display: grid;
1448
+ gap: 12px;
1449
+ `;
1450
+ var QuestionCard = styled4.div`
1451
+ display: grid;
1452
+ gap: 10px;
1453
+ padding: 12px;
1454
+ border-radius: 16px;
1455
+ background: rgba(255, 255, 255, 0.04);
1456
+ `;
1457
+ var QuestionLabel = styled4.div`
1458
+ color: rgba(255, 255, 255, 0.9);
1459
+ font-size: 13px;
1460
+ font-weight: 600;
1461
+ `;
1462
+ var Required = styled4.span`
1463
+ margin-left: 4px;
1464
+ color: rgba(255, 122, 122, 0.9);
1465
+ `;
1466
+ var QuestionBody = styled4.div`
1467
+ display: grid;
1468
+ gap: 10px;
1469
+ `;
1470
+ var OptionList = styled4.div`
1471
+ display: grid;
1472
+ gap: 10px;
1473
+ `;
1474
+ var OptionChoiceItem = styled4.div`
1475
+ display: flex;
1476
+ align-items: center;
1477
+ gap: 12px;
1478
+ width: 100%;
1479
+ text-align: left;
1480
+ border: 1px solid rgba(255, 255, 255, 0.1);
1481
+ border-radius: 14px;
1482
+ background: rgba(255, 255, 255, 0.03);
1483
+ padding: 12px 14px;
1484
+ color: rgba(255, 255, 255, 0.9);
1485
+ cursor: pointer;
1486
+ transition:
1487
+ border-color 140ms ease,
1488
+ background 140ms ease,
1489
+ transform 140ms ease;
1490
+ outline: none;
1491
+
1492
+ &[data-selected='true'] {
1493
+ border-color: rgba(126, 160, 255, 0.42);
1494
+ background: linear-gradient(180deg, rgba(82, 114, 255, 0.18) 0%, rgba(82, 114, 255, 0.1) 100%);
1495
+ transform: translateY(-1px);
1496
+ }
1497
+
1498
+ &[data-tone='other'] {
1499
+ border-style: dashed;
1500
+ }
1501
+
1502
+ &:focus-visible {
1503
+ border-color: rgba(126, 160, 255, 0.52);
1504
+ box-shadow: 0 0 0 1px rgba(126, 160, 255, 0.18);
1505
+ }
1506
+
1507
+ &[tabindex='-1'] {
1508
+ cursor: default;
1509
+ opacity: 0.72;
1510
+ transform: none;
1511
+ }
1512
+ `;
1513
+ var OptionChoiceMarker = styled4.span`
1514
+ display: inline-flex;
1515
+ align-items: center;
1516
+ justify-content: center;
1517
+ flex: 0 0 28px;
1518
+ width: 28px;
1519
+ height: 28px;
1520
+ border-radius: 8px;
1521
+ border: 1px solid rgba(255, 255, 255, 0.14);
1522
+ background: rgba(255, 255, 255, 0.04);
1523
+ color: rgba(255, 255, 255, 0.65);
1524
+ font-size: 12px;
1525
+ font-weight: 700;
1526
+
1527
+ &[data-selected='true'] {
1528
+ border-color: rgba(126, 160, 255, 0.38);
1529
+ background: rgba(126, 160, 255, 0.2);
1530
+ color: rgba(255, 255, 255, 0.96);
1531
+ }
1532
+ `;
1533
+ var OptionChoiceContent = styled4.span`
1534
+ display: grid;
1535
+ gap: 2px;
1536
+ min-width: 0;
1537
+ flex: 1;
1538
+ `;
1539
+ var OptionChoiceLabel = styled4.span`
1540
+ color: inherit;
1541
+ font-size: 13px;
1542
+ font-weight: 600;
1543
+ line-height: 1.4;
1544
+ `;
1545
+ var TextInput = styled4.input`
1546
+ width: 100%;
1547
+ border: 1px solid rgba(255, 255, 255, 0.1);
1548
+ border-radius: 12px;
1549
+ background: rgba(13, 15, 21, 0.55);
1550
+ color: rgba(255, 255, 255, 0.92);
1551
+ font-size: 13px;
1552
+ padding: 10px 12px;
1553
+
1554
+ &::placeholder {
1555
+ color: rgba(255, 255, 255, 0.34);
1556
+ }
1557
+ `;
1558
+ var InlineOtherInput = styled4(TextInput)`
1559
+ margin-top: 0;
1560
+ `;
1561
+ var NumberInputRow = styled4.div`
1562
+ display: flex;
1563
+ align-items: center;
1564
+ gap: 10px;
1565
+ `;
1566
+ var Unit = styled4.span`
1567
+ color: rgba(255, 255, 255, 0.58);
1568
+ font-size: 12px;
1569
+ white-space: nowrap;
1570
+ `;
1571
+ var ErrorMessage = styled4.div`
1572
+ color: rgba(255, 145, 145, 0.96);
1573
+ font-size: 12px;
1574
+ `;
1575
+ var SubmitButton = styled4.button`
1576
+ justify-self: flex-start;
1577
+ border: none;
1578
+ border-radius: 999px;
1579
+ background: linear-gradient(180deg, #7ea0ff 0%, #4a6fff 100%);
1580
+ color: #081127;
1581
+ font-size: 12px;
1582
+ font-weight: 700;
1583
+ padding: 10px 14px;
1584
+ cursor: pointer;
1585
+ `;
1586
+
1587
+ // src/components/chat-thread/components/pde-ai-result-summary-card.tsx
1588
+ import styled5 from "@emotion/styled";
1589
+ import { jsx as jsx6, jsxs as jsxs4 } from "@emotion/react/jsx-runtime";
1590
+ var PDEAIResultSummaryCard = ({ summary }) => /* @__PURE__ */ jsxs4(Card5, { "data-testid": "pde-ai-result-summary-card", "data-status": summary.status, children: [
1591
+ /* @__PURE__ */ jsx6(Eyebrow2, { children: summary.status }),
1592
+ /* @__PURE__ */ jsx6(Headline, { children: summary.headline }),
1593
+ /* @__PURE__ */ jsx6(Details, { children: summary.details.map((detail) => /* @__PURE__ */ jsx6(Detail, { children: detail }, detail)) })
1594
+ ] });
1595
+ var Card5 = styled5.section`
1596
+ display: grid;
1597
+ gap: 10px;
1598
+ padding: 14px;
1599
+ border-radius: 18px;
1600
+ border: 1px solid rgba(255, 255, 255, 0.08);
1601
+ background: rgba(255, 255, 255, 0.03);
1602
+
1603
+ &[data-status='completed'] {
1604
+ border-color: rgba(84, 214, 160, 0.26);
1605
+ }
1606
+
1607
+ &[data-status='failed'] {
1608
+ border-color: rgba(255, 122, 122, 0.24);
1609
+ }
1610
+ `;
1611
+ var Eyebrow2 = styled5.span`
1612
+ color: rgba(255, 255, 255, 0.58);
1613
+ font-size: 11px;
1614
+ font-weight: 700;
1615
+ letter-spacing: 0.08em;
1616
+ text-transform: uppercase;
1617
+ `;
1618
+ var Headline = styled5.strong`
1619
+ color: rgba(255, 255, 255, 0.94);
1620
+ font-size: 15px;
1621
+ `;
1622
+ var Details = styled5.ul`
1623
+ margin: 0;
1624
+ padding-left: 18px;
1625
+ color: rgba(255, 255, 255, 0.8);
1626
+ font-size: 13px;
1627
+ `;
1628
+ var Detail = styled5.li`
1629
+ & + & {
1630
+ margin-top: 6px;
1631
+ }
1632
+ `;
1633
+
1634
+ // src/components/chat-thread/components/image-viewer.tsx
1635
+ import styled6 from "@emotion/styled";
1636
+ import { useEffect as useEffect2, useRef as useRef3 } from "react";
1637
+ import { jsx as jsx7 } from "@emotion/react/jsx-runtime";
1638
+ var Overlay = styled6.div`
1639
+ position: fixed;
1640
+ inset: 0;
1641
+ background: rgba(0, 0, 0, 0.85);
1642
+ display: flex;
1643
+ align-items: center;
1644
+ justify-content: center;
1645
+ z-index: 9999;
1646
+ cursor: zoom-out;
1647
+ `;
1648
+ var Img = styled6.img`
1649
+ max-width: 90vw;
1650
+ max-height: 90vh;
1651
+ object-fit: contain;
1652
+ border-radius: 4px;
1653
+ `;
1654
+ var ImageViewer = ({ src, alt, onClose }) => {
1655
+ const overlayRef = useRef3(null);
1656
+ useEffect2(() => {
1657
+ const handleKey = (e) => {
1658
+ if (e.key === "Escape")
1659
+ onClose();
1660
+ };
1661
+ document.addEventListener("keydown", handleKey);
1662
+ return () => document.removeEventListener("keydown", handleKey);
1663
+ }, [onClose]);
1664
+ useEffect2(() => {
1665
+ overlayRef.current?.focus();
1666
+ }, []);
1667
+ const stopPropagation = (e) => e.stopPropagation();
1668
+ return /* @__PURE__ */ jsx7(
1669
+ Overlay,
1670
+ {
1671
+ ref: overlayRef,
1672
+ role: "dialog",
1673
+ "aria-modal": "true",
1674
+ "aria-label": "Image viewer",
1675
+ tabIndex: -1,
1676
+ onClick: onClose,
1677
+ children: /* @__PURE__ */ jsx7(Img, { src, alt: alt ?? "", onClick: stopPropagation })
1678
+ }
1679
+ );
1680
+ };
1681
+
1682
+ // src/components/chat-thread/components/chat-message-item.tsx
1683
+ import { Fragment as Fragment2, jsx as jsx8, jsxs as jsxs5 } from "@emotion/react/jsx-runtime";
1684
+ var MARKDOWN_REMARK_PLUGINS = [remarkGfm, remarkMath];
1685
+ var MARKDOWN_REHYPE_PLUGINS = [rehypeKatex];
1686
+ var MARKDOWN_COMPONENTS = {
1687
+ table: ({ node: _node, ...props }) => /* @__PURE__ */ jsx8(TableWrapper, { children: /* @__PURE__ */ jsx8("table", { ...props }) })
1688
+ };
1689
+ var renderMarkdownContent = (content) => /* @__PURE__ */ jsx8(
1690
+ ReactMarkdown,
1691
+ {
1692
+ remarkPlugins: MARKDOWN_REMARK_PLUGINS,
1693
+ rehypePlugins: MARKDOWN_REHYPE_PLUGINS,
1694
+ components: MARKDOWN_COMPONENTS,
1695
+ children: content
1696
+ }
1697
+ );
1698
+ var createExecutionConfirmationContent = (proposal) => [
1699
+ "Execution confirmed",
1700
+ `- Equation: ${proposal.equationName}`,
1701
+ ...proposal.solverName ? [`- Solver: ${proposal.solverName}`] : [],
1702
+ `- Proposal ID: ${proposal.proposalId}`
1703
+ ].join("\n");
1704
+ var areChatAttachmentsEqual = (previousAttachments, nextAttachments) => {
1705
+ if (previousAttachments === nextAttachments) {
1706
+ return true;
1707
+ }
1708
+ if (!previousAttachments || !nextAttachments) {
1709
+ return previousAttachments === nextAttachments;
1710
+ }
1711
+ if (previousAttachments.length !== nextAttachments.length) {
1712
+ return false;
1713
+ }
1714
+ return previousAttachments.every((attachment, index) => {
1715
+ const nextAttachment = nextAttachments[index];
1716
+ return attachment.id === nextAttachment?.id && attachment.name === nextAttachment.name && attachment.mimeType === nextAttachment.mimeType && attachment.size === nextAttachment.size && attachment.previewUrl === nextAttachment.previewUrl;
1717
+ });
1718
+ };
1719
+ var areParameterSummaryItemsEqual = (previousItems, nextItems) => previousItems.length === nextItems.length && previousItems.every((item, index) => {
1720
+ const nextItem = nextItems[index];
1721
+ return item.label === nextItem?.label && item.value === nextItem.value && item.fieldPath === nextItem.fieldPath;
1722
+ });
1723
+ var areExecutionProposalsEqual = (previousProposal, nextProposal) => previousProposal.proposalId === nextProposal.proposalId && previousProposal.equationKey === nextProposal.equationKey && previousProposal.equationName === nextProposal.equationName && previousProposal.solverName === nextProposal.solverName && previousProposal.requiresConfirmation === nextProposal.requiresConfirmation && areParameterSummaryItemsEqual(previousProposal.parameterSummary, nextProposal.parameterSummary) && (previousProposal.warnings ?? []).length === (nextProposal.warnings ?? []).length && (previousProposal.warnings ?? []).every(
1724
+ (warning, index) => warning === nextProposal.warnings?.[index]
1725
+ );
1726
+ var areResultSummariesEqual = (previousSummary, nextSummary) => previousSummary.taskId === nextSummary.taskId && previousSummary.status === nextSummary.status && previousSummary.headline === nextSummary.headline && previousSummary.details.length === nextSummary.details.length && previousSummary.details.every((detail, index) => detail === nextSummary.details[index]);
1727
+ var areStringArraysEqual = (previousValues, nextValues) => previousValues.length === nextValues.length && previousValues.every((value, index) => value === nextValues[index]);
1728
+ var areQuestionAnswersEqual = (previousAnswer, nextAnswer) => {
1729
+ if (Array.isArray(previousAnswer) || Array.isArray(nextAnswer)) {
1730
+ return Array.isArray(previousAnswer) && Array.isArray(nextAnswer) && areStringArraysEqual(previousAnswer, nextAnswer);
1731
+ }
1732
+ return previousAnswer === nextAnswer;
1733
+ };
1734
+ var areQuestionAnswerMapsEqual = (previousAnswers, nextAnswers) => {
1735
+ if (previousAnswers === nextAnswers) {
1736
+ return true;
1737
+ }
1738
+ if (!previousAnswers || !nextAnswers) {
1739
+ return previousAnswers === nextAnswers;
1740
+ }
1741
+ const previousKeys = Object.keys(previousAnswers);
1742
+ const nextKeys = Object.keys(nextAnswers);
1743
+ return previousKeys.length === nextKeys.length && previousKeys.every((key) => areQuestionAnswersEqual(previousAnswers[key], nextAnswers[key]));
1744
+ };
1745
+ var areQuestionOptionsEqual = (previousOptions, nextOptions) => previousOptions.length === nextOptions.length && previousOptions.every(
1746
+ (option, index) => option.label === nextOptions[index]?.label && option.value === nextOptions[index]?.value
1747
+ );
1748
+ var arePlanQuestionsEqual = (previousQuestion, nextQuestion) => {
1749
+ if (previousQuestion.id !== nextQuestion.id || previousQuestion.label !== nextQuestion.label || previousQuestion.required !== nextQuestion.required || previousQuestion.kind !== nextQuestion.kind) {
1750
+ return false;
1751
+ }
1752
+ switch (previousQuestion.kind) {
1753
+ case "single_select":
1754
+ return nextQuestion.kind === "single_select" && previousQuestion.allowOther === nextQuestion.allowOther && areQuestionOptionsEqual(previousQuestion.options, nextQuestion.options);
1755
+ case "multi_select":
1756
+ return nextQuestion.kind === "multi_select" && areQuestionOptionsEqual(previousQuestion.options, nextQuestion.options);
1757
+ case "text":
1758
+ return nextQuestion.kind === "text" && previousQuestion.placeholder === nextQuestion.placeholder;
1759
+ case "number":
1760
+ return nextQuestion.kind === "number" && previousQuestion.placeholder === nextQuestion.placeholder && previousQuestion.unit === nextQuestion.unit;
1761
+ case "boolean":
1762
+ return nextQuestion.kind === "boolean" && previousQuestion.trueLabel === nextQuestion.trueLabel && previousQuestion.falseLabel === nextQuestion.falseLabel;
1763
+ default:
1764
+ return false;
1765
+ }
1766
+ };
1767
+ var areQuestionnairesEqual = (previousQuestionnaire, nextQuestionnaire) => previousQuestionnaire.questionnaireId === nextQuestionnaire.questionnaireId && previousQuestionnaire.title === nextQuestionnaire.title && previousQuestionnaire.description === nextQuestionnaire.description && previousQuestionnaire.submitLabel === nextQuestionnaire.submitLabel && previousQuestionnaire.questions.length === nextQuestionnaire.questions.length && previousQuestionnaire.questions.every(
1768
+ (question, index) => arePlanQuestionsEqual(question, nextQuestionnaire.questions[index])
1769
+ ) && areQuestionAnswerMapsEqual(previousQuestionnaire.answers, nextQuestionnaire.answers);
1770
+ var areMessageBlocksEqual = (previousBlocks, nextBlocks) => {
1771
+ if (previousBlocks === nextBlocks) {
1772
+ return true;
1773
+ }
1774
+ if (!previousBlocks || !nextBlocks) {
1775
+ return previousBlocks === nextBlocks;
1776
+ }
1777
+ if (previousBlocks.length !== nextBlocks.length) {
1778
+ return false;
1779
+ }
1780
+ return previousBlocks.every((block, index) => {
1781
+ const nextBlock = nextBlocks[index];
1782
+ if (!nextBlock || block.type !== nextBlock.type) {
1783
+ return false;
1784
+ }
1785
+ switch (block.type) {
1786
+ case "markdown": {
1787
+ const comparableBlock = nextBlock;
1788
+ return block.text === comparableBlock.text;
1789
+ }
1790
+ case "notice": {
1791
+ const comparableBlock = nextBlock;
1792
+ return block.tone === comparableBlock.tone && block.text === comparableBlock.text;
1793
+ }
1794
+ case "parameter_summary": {
1795
+ const comparableBlock = nextBlock;
1796
+ return areParameterSummaryItemsEqual(block.items, comparableBlock.items);
1797
+ }
1798
+ case "confirmation_card": {
1799
+ const comparableBlock = nextBlock;
1800
+ return areExecutionProposalsEqual(block.proposal, comparableBlock.proposal);
1801
+ }
1802
+ case "result_summary": {
1803
+ const comparableBlock = nextBlock;
1804
+ return areResultSummariesEqual(block.summary, comparableBlock.summary);
1805
+ }
1806
+ case "questionnaire": {
1807
+ const comparableBlock = nextBlock;
1808
+ return areQuestionnairesEqual(block.questionnaire, comparableBlock.questionnaire);
1809
+ }
1810
+ case "custom": {
1811
+ const comparableBlock = nextBlock;
1812
+ return block.kind === comparableBlock.kind && block.data === comparableBlock.data;
1813
+ }
1814
+ default:
1815
+ return false;
1816
+ }
1817
+ });
1818
+ };
1819
+ var isSameMessage = (previousMessage, nextMessage, previousMode, nextMode, previousConfirmationSubmit, nextConfirmationSubmit, previousQuestionnaireSubmit, nextQuestionnaireSubmit, previousRenderMessageBlock, nextRenderMessageBlock) => previousMessage.id === nextMessage.id && previousMessage.sessionId === nextMessage.sessionId && previousMessage.role === nextMessage.role && previousMessage.content === nextMessage.content && areMessageBlocksEqual(previousMessage.blocks, nextMessage.blocks) && previousMessage.localOnly === nextMessage.localOnly && areChatAttachmentsEqual(previousMessage.attachments, nextMessage.attachments) && previousMessage.status === nextMessage.status && previousMessage.createdAt === nextMessage.createdAt && previousMode === nextMode && previousConfirmationSubmit === nextConfirmationSubmit && previousQuestionnaireSubmit === nextQuestionnaireSubmit && previousRenderMessageBlock === nextRenderMessageBlock;
1820
+ var ChatMessageItemView = ({
1821
+ message,
1822
+ mode = "agent",
1823
+ onConfirmationSubmit,
1824
+ onQuestionnaireSubmit,
1825
+ renderMessageBlock
1826
+ }) => {
1827
+ const { labels } = useChatContext();
1828
+ const [activeImage, setActiveImage] = useState4(void 0);
1829
+ const { displayedContent, freshContent, isAssistantStreaming, settledContent } = useChatMessageReveal(message);
1830
+ const isStoppedAssistant = message.role === "assistant" && message.status === "stopped";
1831
+ const attachments = message.attachments ?? [];
1832
+ const blocks = message.blocks ?? [];
1833
+ const hasStructuredBlocks = blocks.length > 0;
1834
+ const hasTextContent = Boolean(settledContent || freshContent || displayedContent);
1835
+ const isPlanMode = mode === "plan";
1836
+ const canSubmitConfirmation = isPlanMode && typeof onConfirmationSubmit === "function";
1837
+ const canSubmitQuestionnaire = isPlanMode && typeof onQuestionnaireSubmit === "function";
1838
+ const shouldShowStreamingCaret = isAssistantStreaming && (!hasStructuredBlocks || hasTextContent);
1839
+ const renderChatMessageBlock = (block, index) => {
1840
+ switch (block.type) {
1841
+ case "markdown":
1842
+ return /* @__PURE__ */ jsx8(
1843
+ ContentBlock,
1844
+ {
1845
+ "data-testid": `chat-message-block-${index}`,
1846
+ "data-block-tone": "settled",
1847
+ children: renderMarkdownContent(block.text)
1848
+ },
1849
+ `markdown-${index}`
1850
+ );
1851
+ case "notice":
1852
+ return /* @__PURE__ */ jsx8(Fragment, { children: /* @__PURE__ */ jsx8(PDEAINoticeCard, { text: block.text, tone: block.tone }) }, `notice-${index}`);
1853
+ case "parameter_summary":
1854
+ return /* @__PURE__ */ jsx8(Fragment, { children: /* @__PURE__ */ jsx8(PDEAIParameterSummaryCard, { items: block.items }) }, `parameter-summary-${index}`);
1855
+ case "confirmation_card":
1856
+ return /* @__PURE__ */ jsx8(Fragment, { children: /* @__PURE__ */ jsx8(
1857
+ PDEAIExecutionConfirmationCard,
1858
+ {
1859
+ proposal: block.proposal,
1860
+ interactive: isPlanMode,
1861
+ onConfirm: canSubmitConfirmation ? () => onConfirmationSubmit({
1862
+ proposalId: block.proposal.proposalId,
1863
+ content: createExecutionConfirmationContent(block.proposal),
1864
+ sourceMessageId: message.id
1865
+ }) : void 0
1866
+ }
1867
+ ) }, `confirmation-card-${index}`);
1868
+ case "result_summary":
1869
+ return /* @__PURE__ */ jsx8(Fragment, { children: /* @__PURE__ */ jsx8(PDEAIResultSummaryCard, { summary: block.summary }) }, `result-summary-${index}`);
1870
+ case "questionnaire":
1871
+ return /* @__PURE__ */ jsx8(Fragment, { children: /* @__PURE__ */ jsx8(
1872
+ PDEAIQuestionnaireCard,
1873
+ {
1874
+ questionnaire: block.questionnaire,
1875
+ interactive: canSubmitQuestionnaire,
1876
+ onSubmit: canSubmitQuestionnaire ? (submission) => onQuestionnaireSubmit({
1877
+ ...submission,
1878
+ sourceMessageId: message.id
1879
+ }) : void 0
1880
+ }
1881
+ ) }, `questionnaire-${index}`);
1882
+ case "custom":
1883
+ return /* @__PURE__ */ jsx8(Fragment, { children: renderMessageBlock?.({
1884
+ block,
1885
+ index,
1886
+ message,
1887
+ mode,
1888
+ onConfirmationSubmit,
1889
+ onQuestionnaireSubmit
1890
+ }) ?? null }, `custom-${block.kind}-${index}`);
1891
+ default:
1892
+ return null;
1893
+ }
1894
+ };
1895
+ const renderTextContent = () => /* @__PURE__ */ jsxs5(Fragment2, { children: [
1896
+ settledContent ? /* @__PURE__ */ jsx8(ContentBlock, { "data-testid": "chat-message-settled-block", "data-block-tone": "settled", children: renderMarkdownContent(settledContent) }) : null,
1897
+ freshContent ? /* @__PURE__ */ jsx8(ContentBlock, { "data-testid": "chat-message-fresh-block", "data-block-tone": "fresh", children: renderMarkdownContent(freshContent) }) : null,
1898
+ !settledContent && !freshContent && hasTextContent ? /* @__PURE__ */ jsx8(ContentBlock, { "data-testid": "chat-message-settled-block", "data-block-tone": "settled", children: renderMarkdownContent(displayedContent) }) : null
1899
+ ] });
1900
+ return /* @__PURE__ */ jsxs5(Fragment2, { children: [
1901
+ /* @__PURE__ */ jsxs5(Bubble, { "data-role": message.role, "data-status": message.status ?? "done", children: [
1902
+ /* @__PURE__ */ jsxs5(Header2, { children: [
1903
+ isAssistantStreaming ? /* @__PURE__ */ jsxs5(
1904
+ StreamingIndicator,
1905
+ {
1906
+ "data-testid": "chat-streaming-indicator",
1907
+ "aria-label": labels.assistantStreamingAriaLabel,
1908
+ children: [
1909
+ /* @__PURE__ */ jsx8(IndicatorSpark, {}),
1910
+ /* @__PURE__ */ jsx8(IndicatorSpark, { "data-secondary": true })
1911
+ ]
1912
+ }
1913
+ ) : null,
1914
+ /* @__PURE__ */ jsx8(Role, { children: message.role === "user" ? labels.userRoleLabel : labels.assistantRoleLabel }),
1915
+ isStoppedAssistant ? /* @__PURE__ */ jsx8(StatusTag, { "data-testid": "chat-message-stopped-tag", children: labels.stoppedResponse }) : null
1916
+ ] }),
1917
+ /* @__PURE__ */ jsxs5(Content, { "data-testid": "chat-message-content", children: [
1918
+ hasStructuredBlocks || hasTextContent ? /* @__PURE__ */ jsxs5(ContentStack, { "data-testid": "chat-message-body-stack", children: [
1919
+ hasStructuredBlocks ? blocks.map((block, index) => /* @__PURE__ */ jsx8(
1920
+ ContentSegment,
1921
+ {
1922
+ "data-testid": "chat-message-content-segment",
1923
+ children: renderChatMessageBlock(block, index)
1924
+ },
1925
+ `${block.type}-${index}`
1926
+ )) : null,
1927
+ hasTextContent ? /* @__PURE__ */ jsx8(ContentSegment, { "data-testid": "chat-message-content-segment", children: renderTextContent() }) : null
1928
+ ] }) : null,
1929
+ attachments.length ? /* @__PURE__ */ jsx8(AttachmentGrid, { "data-testid": "chat-message-attachment-grid", children: attachments.map((attachment) => /* @__PURE__ */ jsx8(
1930
+ AttachmentButton,
1931
+ {
1932
+ type: "button",
1933
+ "aria-label": attachment.name,
1934
+ onClick: () => setActiveImage({
1935
+ name: attachment.name,
1936
+ previewUrl: attachment.previewUrl
1937
+ }),
1938
+ children: /* @__PURE__ */ jsx8(AttachmentImage, { src: attachment.previewUrl, alt: attachment.name })
1939
+ },
1940
+ attachment.id
1941
+ )) }) : null,
1942
+ shouldShowStreamingCaret ? /* @__PURE__ */ jsx8(StreamingCaret, { "data-testid": "chat-streaming-caret", "aria-hidden": "true" }) : null
1943
+ ] })
1944
+ ] }),
1945
+ activeImage ? /* @__PURE__ */ jsx8(
1946
+ ImageViewer,
1947
+ {
1948
+ src: activeImage.previewUrl,
1949
+ alt: activeImage.name,
1950
+ onClose: () => setActiveImage(void 0)
1951
+ }
1952
+ ) : null
1953
+ ] });
1954
+ };
1955
+ var ChatMessageItem = memo(
1956
+ ChatMessageItemView,
1957
+ (previousProps, nextProps) => isSameMessage(
1958
+ previousProps.message,
1959
+ nextProps.message,
1960
+ previousProps.mode,
1961
+ nextProps.mode,
1962
+ previousProps.onConfirmationSubmit,
1963
+ nextProps.onConfirmationSubmit,
1964
+ previousProps.onQuestionnaireSubmit,
1965
+ nextProps.onQuestionnaireSubmit,
1966
+ previousProps.renderMessageBlock,
1967
+ nextProps.renderMessageBlock
1968
+ )
1969
+ );
1970
+ var Bubble = styled7.article`
1971
+ width: 100%;
1972
+ max-width: 100%;
1973
+ padding: 14px 16px;
1974
+ border-radius: 22px;
1975
+ background: rgba(255, 255, 255, 0.04);
1976
+ border: 1px solid rgba(255, 255, 255, 0.07);
1977
+ box-shadow:
1978
+ inset 0 1px 0 rgba(255, 255, 255, 0.03),
1979
+ 0 12px 30px rgba(0, 0, 0, 0.18);
1980
+
1981
+ &[data-role='user'] {
1982
+ width: auto;
1983
+ max-width: min(760px, 100%);
1984
+ margin-left: auto;
1985
+ background: linear-gradient(180deg, rgba(59, 59, 63, 0.9) 0%, rgba(42, 43, 46, 0.92) 100%);
1986
+ }
1987
+ `;
1988
+ var Header2 = styled7.div`
1989
+ display: flex;
1990
+ align-items: center;
1991
+ gap: 10px;
1992
+ margin-bottom: 10px;
1993
+ `;
1994
+ var Role = styled7.div`
1995
+ font-size: 12px;
1996
+ color: rgba(255, 255, 255, 0.42);
1997
+ text-transform: capitalize;
1998
+ letter-spacing: 0.08em;
1999
+ `;
2000
+ var StatusTag = styled7.span`
2001
+ display: inline-flex;
2002
+ align-items: center;
2003
+ padding: 3px 10px;
2004
+ border-radius: 999px;
2005
+ background: rgba(255, 255, 255, 0.14);
2006
+ border: 1px solid rgba(255, 255, 255, 0.16);
2007
+ color: rgba(255, 255, 255, 0.84);
2008
+ font-size: 11px;
2009
+ font-weight: 600;
2010
+ letter-spacing: 0.02em;
2011
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08);
2012
+ `;
2013
+ var Content = styled7.div`
2014
+ color: rgba(255, 255, 255, 0.92);
2015
+ line-height: 1.6;
2016
+
2017
+ p {
2018
+ margin: 0;
2019
+ }
2020
+
2021
+ p + p {
2022
+ margin-top: 12px;
2023
+ }
2024
+
2025
+ table {
2026
+ width: 100%;
2027
+ border-collapse: collapse;
2028
+ margin: 0;
2029
+ overflow: hidden;
2030
+ border-radius: 14px;
2031
+ border: 1px solid rgba(255, 255, 255, 0.08);
2032
+ background: rgba(255, 255, 255, 0.03);
2033
+ }
2034
+
2035
+ th,
2036
+ td {
2037
+ padding: 10px 12px;
2038
+ text-align: left;
2039
+ vertical-align: top;
2040
+ border-bottom: 1px solid rgba(255, 255, 255, 0.08);
2041
+ }
2042
+
2043
+ th {
2044
+ color: rgba(255, 255, 255, 0.82);
2045
+ font-weight: 600;
2046
+ background: rgba(255, 255, 255, 0.04);
2047
+ }
2048
+
2049
+ tbody tr:last-of-type td {
2050
+ border-bottom: none;
2051
+ }
2052
+ `;
2053
+ var ContentStack = styled7.div`
2054
+ display: flex;
2055
+ flex-direction: column;
2056
+ gap: 16px;
2057
+ `;
2058
+ var ContentSegment = styled7.div`
2059
+ min-width: 0;
2060
+ `;
2061
+ var ContentBlock = styled7.div`
2062
+ transition:
2063
+ opacity 180ms ease-out,
2064
+ filter 180ms ease-out;
2065
+
2066
+ & + & {
2067
+ margin-top: 16px;
2068
+ }
2069
+
2070
+ &[data-block-tone='fresh'] {
2071
+ opacity: 0.85;
2072
+ filter: brightness(0.82) saturate(0.88);
2073
+ }
2074
+ `;
2075
+ var AttachmentGrid = styled7.div`
2076
+ display: flex;
2077
+ flex-wrap: wrap;
2078
+ gap: 10px;
2079
+ margin-top: 12px;
2080
+ `;
2081
+ var AttachmentButton = styled7.button`
2082
+ width: 116px;
2083
+ height: 86px;
2084
+ overflow: hidden;
2085
+ padding: 0;
2086
+ border: 1px solid rgba(255, 255, 255, 0.12);
2087
+ border-radius: 14px;
2088
+ background: rgba(255, 255, 255, 0.05);
2089
+ cursor: pointer;
2090
+ `;
2091
+ var AttachmentImage = styled7.img`
2092
+ width: 100%;
2093
+ height: 100%;
2094
+ display: block;
2095
+ object-fit: cover;
2096
+ `;
2097
+ var TableWrapper = styled7.div`
2098
+ overflow-x: auto;
2099
+ `;
2100
+ var caretBlink = keyframes`
2101
+ 0%, 49% {
2102
+ opacity: 1;
2103
+ }
2104
+
2105
+ 50%, 100% {
2106
+ opacity: 0.2;
2107
+ }
2108
+ `;
2109
+ var shimmer = keyframes`
2110
+ 0%, 100% {
2111
+ transform: scale(0.9) rotate(0deg);
2112
+ opacity: 0.55;
2113
+ }
2114
+
2115
+ 50% {
2116
+ transform: scale(1.05) rotate(8deg);
2117
+ opacity: 1;
2118
+ }
2119
+ `;
2120
+ var StreamingIndicator = styled7.div`
2121
+ position: relative;
2122
+ width: 26px;
2123
+ height: 26px;
2124
+ flex-shrink: 0;
2125
+ display: grid;
2126
+ place-items: center;
2127
+ border-radius: 8px;
2128
+ background:
2129
+ radial-gradient(circle at 50% 50%, rgba(78, 102, 255, 0.18), transparent 70%),
2130
+ rgba(255, 255, 255, 0.01);
2131
+ border: 1px dashed rgba(255, 255, 255, 0.14);
2132
+ `;
2133
+ var IndicatorSpark = styled7.span`
2134
+ position: absolute;
2135
+ width: 10px;
2136
+ height: 10px;
2137
+ background: linear-gradient(180deg, #7ea0ff 0%, #4a6fff 100%);
2138
+ clip-path: polygon(50% 0%, 68% 32%, 100% 50%, 68% 68%, 50% 100%, 32% 68%, 0% 50%, 32% 32%);
2139
+ box-shadow: 0 0 12px rgba(78, 102, 255, 0.55);
2140
+ animation: ${shimmer} 1.2s ease-in-out infinite;
2141
+
2142
+ &[data-secondary] {
2143
+ width: 6px;
2144
+ height: 6px;
2145
+ top: 4px;
2146
+ left: 5px;
2147
+ animation-delay: 0.2s;
2148
+ }
2149
+ `;
2150
+ var StreamingCaret = styled7.span`
2151
+ display: inline-block;
2152
+ width: 8px;
2153
+ height: 1.1em;
2154
+ margin-left: 2px;
2155
+ vertical-align: text-bottom;
2156
+ border-radius: 999px;
2157
+ background: linear-gradient(180deg, rgba(126, 160, 255, 0.95) 0%, rgba(74, 111, 255, 0.75) 100%);
2158
+ box-shadow: 0 0 10px rgba(78, 102, 255, 0.35);
2159
+ animation: ${caretBlink} 0.9s steps(1) infinite;
2160
+ `;
2161
+
2162
+ // src/components/chat-thread/components/chat-thread-history-list.tsx
2163
+ import { memo as memo2 } from "react";
2164
+ import styled8 from "@emotion/styled";
2165
+ import { jsx as jsx9 } from "@emotion/react/jsx-runtime";
2166
+ var ChatThreadHistoryList = memo2(
2167
+ ({
2168
+ mode,
2169
+ historyMessages,
2170
+ latestUserMessageId,
2171
+ latestUserMessageRef,
2172
+ onConfirmationSubmit,
2173
+ onQuestionnaireSubmit,
2174
+ renderMessageBlock
2175
+ }) => /* @__PURE__ */ jsx9(HistoryGroup, { "data-testid": "chat-thread-history", children: historyMessages.map((message) => /* @__PURE__ */ jsx9(
2176
+ MessageSlot,
2177
+ {
2178
+ ref: message.id === latestUserMessageId ? latestUserMessageRef : null,
2179
+ "data-testid": message.id === latestUserMessageId ? "chat-latest-user-anchor" : void 0,
2180
+ style: message.id === latestUserMessageId ? {
2181
+ scrollMarginTop: `${CHAT_THREAD_SCROLL_TOP_GAP}px`
2182
+ } : void 0,
2183
+ children: /* @__PURE__ */ jsx9(
2184
+ ChatMessageItem,
2185
+ {
2186
+ mode,
2187
+ message,
2188
+ onConfirmationSubmit,
2189
+ onQuestionnaireSubmit,
2190
+ renderMessageBlock
2191
+ }
2192
+ )
2193
+ },
2194
+ message.id
2195
+ )) })
2196
+ );
2197
+ ChatThreadHistoryList.displayName = "ChatThreadHistoryList";
2198
+ var HistoryGroup = styled8.div`
2199
+ display: contents;
2200
+ `;
2201
+ var MessageSlot = styled8.div`
2202
+ display: flex;
2203
+ align-items: flex-start;
2204
+ `;
2205
+
2206
+ // src/components/chat-thread/components/chat-thread-empty-state.tsx
2207
+ import styled9 from "@emotion/styled";
2208
+ import { jsx as jsx10, jsxs as jsxs6 } from "@emotion/react/jsx-runtime";
2209
+ var ChatThreadEmptyState = () => {
2210
+ const { labels } = useChatContext();
2211
+ return /* @__PURE__ */ jsxs6(EmptyShell, { "data-testid": "chat-empty-hero", children: [
2212
+ /* @__PURE__ */ jsxs6(HeroMark, { children: [
2213
+ /* @__PURE__ */ jsx10(HeroOrbit, {}),
2214
+ /* @__PURE__ */ jsx10(HeroCore, { children: "AI" })
2215
+ ] }),
2216
+ /* @__PURE__ */ jsx10(HeroTitle, { children: labels.emptyStateTitle }),
2217
+ /* @__PURE__ */ jsx10(HeroSubtitle, { children: labels.emptyStateSubtitle })
2218
+ ] });
2219
+ };
2220
+ var EmptyShell = styled9.div`
2221
+ flex: 1;
2222
+ min-height: 0;
2223
+ display: flex;
2224
+ align-items: center;
2225
+ justify-content: center;
2226
+ flex-direction: column;
2227
+ gap: 16px;
2228
+ padding: 48px 24px 24px;
2229
+ `;
2230
+ var HeroMark = styled9.div`
2231
+ position: relative;
2232
+ width: 108px;
2233
+ height: 108px;
2234
+ border-radius: 24px;
2235
+ display: grid;
2236
+ place-items: center;
2237
+ `;
2238
+ var HeroOrbit = styled9.div`
2239
+ position: absolute;
2240
+ inset: 20px;
2241
+ border-radius: 50%;
2242
+ border: 1px solid rgba(255, 255, 255, 0.22);
2243
+ box-shadow:
2244
+ 0 0 24px rgba(78, 102, 255, 0.3),
2245
+ inset 0 0 16px rgba(255, 255, 255, 0.08);
2246
+
2247
+ &::before,
2248
+ &::after {
2249
+ content: '';
2250
+ position: absolute;
2251
+ border-radius: 50%;
2252
+ inset: -7px;
2253
+ border: 1px solid rgba(78, 102, 255, 0.12);
2254
+ }
2255
+
2256
+ &::after {
2257
+ inset: 8px -10px;
2258
+ transform: rotate(-22deg);
2259
+ }
2260
+ `;
2261
+ var HeroCore = styled9.div`
2262
+ position: relative;
2263
+ z-index: 1;
2264
+ font-size: 28px;
2265
+ line-height: 1;
2266
+ font-weight: 600;
2267
+ letter-spacing: 0.08em;
2268
+ color: rgba(242, 244, 255, 0.96);
2269
+ text-shadow: 0 0 16px rgba(98, 116, 255, 0.65);
2270
+ `;
2271
+ var HeroTitle = styled9.p`
2272
+ margin: 0;
2273
+ color: rgba(255, 255, 255, 0.88);
2274
+ font-size: 16px;
2275
+ line-height: 24px;
2276
+ `;
2277
+ var HeroSubtitle = styled9.p`
2278
+ margin: 0;
2279
+ color: rgba(255, 255, 255, 0.72);
2280
+ font-size: 14px;
2281
+ line-height: 20px;
2282
+ `;
2283
+
2284
+ // src/components/chat-thread/index.tsx
2285
+ import { jsx as jsx11, jsxs as jsxs7 } from "@emotion/react/jsx-runtime";
2286
+ var ChatThreadView = ({
2287
+ activeSessionMode = DEFAULT_CHAT_AGENT_MODE,
2288
+ historyMessages,
2289
+ streamingMessage,
2290
+ error,
2291
+ retryButtonLabel,
2292
+ onRetry,
2293
+ onConfirmationSubmit,
2294
+ onQuestionnaireSubmit,
2295
+ renderMessageBlock
2296
+ }) => {
2297
+ const containerRef = useRef4(null);
2298
+ const latestUserMessageId = useMemo3(
2299
+ () => findLatestUserMessageId(historyMessages),
2300
+ [historyMessages]
2301
+ );
2302
+ const latestUserMessageRef = useRef4(null);
2303
+ const pendingScrollUserMessageIdRef = useRef4(void 0);
2304
+ const reservedSpaceFrameRef = useRef4(null);
2305
+ const [latestUserMessageReservedSpace, setLatestUserMessageReservedSpace] = useState5({ messageId: void 0, value: 0 });
2306
+ const reservedPaddingBottom = 24 + (latestUserMessageReservedSpace.messageId === latestUserMessageId ? latestUserMessageReservedSpace.value : 0);
2307
+ const measureLatestUserMessageReservedSpace = useCallback2((messageId) => {
2308
+ const container = containerRef.current;
2309
+ const target = latestUserMessageRef.current;
2310
+ if (!container || !target)
2311
+ return;
2312
+ const reservedHeight = calculateChatThreadScrollSpacerHeight({
2313
+ containerClientHeight: container.clientHeight,
2314
+ containerScrollHeight: container.scrollHeight,
2315
+ targetOffsetTop: target.offsetTop
2316
+ });
2317
+ setLatestUserMessageReservedSpace((current) => {
2318
+ const next = reservedHeight > 0 ? reservedHeight : 0;
2319
+ if (current.messageId === messageId && current.value === next)
2320
+ return current;
2321
+ return { messageId, value: next };
2322
+ });
2323
+ }, []);
2324
+ useLayoutEffect(() => {
2325
+ if (reservedSpaceFrameRef.current !== null) {
2326
+ window.cancelAnimationFrame(reservedSpaceFrameRef.current);
2327
+ reservedSpaceFrameRef.current = null;
2328
+ }
2329
+ if (!latestUserMessageId) {
2330
+ pendingScrollUserMessageIdRef.current = void 0;
2331
+ reservedSpaceFrameRef.current = window.requestAnimationFrame(() => {
2332
+ reservedSpaceFrameRef.current = null;
2333
+ setLatestUserMessageReservedSpace(
2334
+ (current) => current.messageId === void 0 && current.value === 0 ? current : { messageId: void 0, value: 0 }
2335
+ );
2336
+ });
2337
+ return () => {
2338
+ if (reservedSpaceFrameRef.current !== null) {
2339
+ window.cancelAnimationFrame(reservedSpaceFrameRef.current);
2340
+ reservedSpaceFrameRef.current = null;
2341
+ }
2342
+ };
2343
+ }
2344
+ pendingScrollUserMessageIdRef.current = latestUserMessageId;
2345
+ reservedSpaceFrameRef.current = window.requestAnimationFrame(() => {
2346
+ reservedSpaceFrameRef.current = null;
2347
+ measureLatestUserMessageReservedSpace(latestUserMessageId);
2348
+ });
2349
+ return () => {
2350
+ if (reservedSpaceFrameRef.current !== null) {
2351
+ window.cancelAnimationFrame(reservedSpaceFrameRef.current);
2352
+ reservedSpaceFrameRef.current = null;
2353
+ }
2354
+ };
2355
+ }, [latestUserMessageId, measureLatestUserMessageReservedSpace]);
2356
+ useLayoutEffect(() => {
2357
+ if (!latestUserMessageId || pendingScrollUserMessageIdRef.current !== latestUserMessageId)
2358
+ return;
2359
+ if (latestUserMessageReservedSpace.messageId !== latestUserMessageId)
2360
+ return;
2361
+ latestUserMessageRef.current?.scrollIntoView({ block: "start", behavior: "smooth" });
2362
+ pendingScrollUserMessageIdRef.current = void 0;
2363
+ }, [latestUserMessageId, latestUserMessageReservedSpace]);
2364
+ return /* @__PURE__ */ jsxs7(
2365
+ Container,
2366
+ {
2367
+ ref: containerRef,
2368
+ "data-testid": "chat-thread",
2369
+ style: { paddingBottom: `${reservedPaddingBottom}px` },
2370
+ children: [
2371
+ /* @__PURE__ */ jsx11(
2372
+ ChatThreadHistoryList,
2373
+ {
2374
+ mode: activeSessionMode,
2375
+ historyMessages,
2376
+ latestUserMessageId,
2377
+ latestUserMessageRef,
2378
+ onConfirmationSubmit,
2379
+ onQuestionnaireSubmit,
2380
+ renderMessageBlock
2381
+ }
2382
+ ),
2383
+ streamingMessage ? /* @__PURE__ */ jsx11(StreamingGroup, { "data-testid": "chat-thread-streaming", children: /* @__PURE__ */ jsx11(MessageSlot2, { children: /* @__PURE__ */ jsx11(
2384
+ ChatMessageItem,
2385
+ {
2386
+ mode: activeSessionMode,
2387
+ message: streamingMessage,
2388
+ onConfirmationSubmit,
2389
+ onQuestionnaireSubmit,
2390
+ renderMessageBlock
2391
+ }
2392
+ ) }) }) : null,
2393
+ error ? /* @__PURE__ */ jsxs7(ErrorState, { "data-testid": "chat-thread-error-state", children: [
2394
+ /* @__PURE__ */ jsx11(ErrorText, { children: error }),
2395
+ onRetry ? /* @__PURE__ */ jsx11(ErrorActions, { children: /* @__PURE__ */ jsx11(RetryButton, { type: "button", "data-testid": "chat-thread-retry", onClick: onRetry, children: retryButtonLabel }) }) : null
2396
+ ] }) : null
2397
+ ]
2398
+ }
2399
+ );
2400
+ };
2401
+ var EMPTY_MESSAGES = [];
2402
+ var ChatThread = () => {
2403
+ const activeSessionId = useChatStore((s) => s.activeSessionId);
2404
+ const hasSessions = useChatStore((s) => s.sessions.length > 0);
2405
+ const activeSessionMode = useChatStore(
2406
+ (s) => s.sessions.find((x) => x.sessionId === s.activeSessionId)?.mode ?? DEFAULT_CHAT_AGENT_MODE
2407
+ );
2408
+ const messages = useChatStore(
2409
+ (s) => s.messagesBySession[s.activeSessionId ?? ""] ?? EMPTY_MESSAGES
2410
+ );
2411
+ const streamingMessage = useChatStore((s) => s.streamingMessageBySession[s.activeSessionId ?? ""]);
2412
+ const error = useChatStore((s) => s.errorBySession[s.activeSessionId ?? ""]);
2413
+ const updateQA = useChatStore((s) => s.updateQuestionnaireAnswers);
2414
+ const clearSessionError = useChatStore((s) => s.clearSessionError);
2415
+ const { sendRef, retryRef, renderMessageBlock, labels } = useChatContext();
2416
+ const handleRetry = useCallback2(() => {
2417
+ if (!activeSessionId)
2418
+ return;
2419
+ clearSessionError(activeSessionId);
2420
+ void retryRef.current();
2421
+ }, [activeSessionId, clearSessionError, retryRef]);
2422
+ const handleQuestionnaireSubmit = useCallback2(
2423
+ (submission) => {
2424
+ if (activeSessionId && submission.sourceMessageId) {
2425
+ updateQA(
2426
+ activeSessionId,
2427
+ submission.sourceMessageId,
2428
+ submission.questionnaireId,
2429
+ submission.answers
2430
+ );
2431
+ }
2432
+ void sendRef.current(submission.content);
2433
+ },
2434
+ [activeSessionId, updateQA, sendRef]
2435
+ );
2436
+ const handleConfirmationSubmit = useCallback2(
2437
+ (submission) => {
2438
+ void sendRef.current(submission.content);
2439
+ },
2440
+ [sendRef]
2441
+ );
2442
+ if (!hasSessions || messages.length === 0 && !streamingMessage) {
2443
+ return /* @__PURE__ */ jsx11(ChatThreadEmptyState, {});
2444
+ }
2445
+ return /* @__PURE__ */ jsx11(
2446
+ ChatThreadView,
2447
+ {
2448
+ activeSessionMode,
2449
+ historyMessages: messages,
2450
+ streamingMessage,
2451
+ error,
2452
+ retryButtonLabel: labels.retryButton,
2453
+ onRetry: handleRetry,
2454
+ onConfirmationSubmit: handleConfirmationSubmit,
2455
+ onQuestionnaireSubmit: handleQuestionnaireSubmit,
2456
+ renderMessageBlock
2457
+ }
2458
+ );
2459
+ };
2460
+ var Container = styled10.div`
2461
+ display: flex;
2462
+ flex: 1;
2463
+ flex-direction: column;
2464
+ gap: 18px;
2465
+ min-height: 0;
2466
+ overflow: auto;
2467
+ padding: 24px;
2468
+ overscroll-behavior: contain;
2469
+
2470
+ &::-webkit-scrollbar {
2471
+ width: 8px;
2472
+ }
2473
+
2474
+ &::-webkit-scrollbar-thumb {
2475
+ background: rgba(255, 255, 255, 0.18);
2476
+ border-radius: 999px;
2477
+ }
2478
+
2479
+ &::-webkit-scrollbar-track {
2480
+ background: transparent;
2481
+ }
2482
+ `;
2483
+ var MessageSlot2 = styled10.div`
2484
+ display: flex;
2485
+ `;
2486
+ var StreamingGroup = styled10.div`
2487
+ display: contents;
2488
+ `;
2489
+ var ErrorText = styled10.div`
2490
+ color: #ff7b72;
2491
+ font-size: 14px;
2492
+ `;
2493
+ var ErrorState = styled10.div`
2494
+ display: flex;
2495
+ flex-direction: column;
2496
+ align-items: flex-start;
2497
+ gap: 10px;
2498
+ `;
2499
+ var ErrorActions = styled10.div`
2500
+ display: flex;
2501
+ align-items: center;
2502
+ `;
2503
+ var RetryButton = styled10.button`
2504
+ border: 1px solid rgba(255, 255, 255, 0.14);
2505
+ border-radius: 999px;
2506
+ background: rgba(255, 255, 255, 0.04);
2507
+ color: rgba(255, 255, 255, 0.82);
2508
+ font-size: 12px;
2509
+ line-height: 1;
2510
+ padding: 5px 10px;
2511
+ cursor: pointer;
2512
+
2513
+ &:hover {
2514
+ background: rgba(255, 255, 255, 0.08);
2515
+ }
2516
+ `;
2517
+
2518
+ // src/components/chat-composer/index.tsx
2519
+ import { useEffect as useEffect5, useRef as useRef7 } from "react";
2520
+ import styled15 from "@emotion/styled";
2521
+
2522
+ // src/components/chat-composer/lib/chat-composer.ts
2523
+ var DRAFT_CHAT_SESSION_ID_PREFIX = "draft-session-";
2524
+ var createDraftChatSessionId = () => `${DRAFT_CHAT_SESSION_ID_PREFIX}${Date.now()}`;
2525
+ var isDraftChatSessionId = (sessionId) => Boolean(sessionId?.startsWith(DRAFT_CHAT_SESSION_ID_PREFIX));
2526
+ var createDraftChatSession = ({
2527
+ model,
2528
+ mode = DEFAULT_CHAT_AGENT_MODE,
2529
+ nowIso: nowIso2,
2530
+ createSessionId
2531
+ }) => {
2532
+ const iso = nowIso2();
2533
+ return {
2534
+ sessionId: createSessionId(),
2535
+ title: "New Chat",
2536
+ createdAt: iso,
2537
+ updatedAt: iso,
2538
+ model,
2539
+ mode
2540
+ };
2541
+ };
2542
+ var createUserMessage = ({
2543
+ sessionId,
2544
+ content,
2545
+ attachments,
2546
+ localOnly,
2547
+ createdAt,
2548
+ createMessageId
2549
+ }) => ({
2550
+ id: createMessageId(),
2551
+ sessionId,
2552
+ role: "user",
2553
+ content,
2554
+ attachments,
2555
+ localOnly,
2556
+ createdAt
2557
+ });
2558
+ var createAssistantStreamingMessage = ({
2559
+ sessionId,
2560
+ createdAt,
2561
+ createMessageId
2562
+ }) => ({
2563
+ id: createMessageId(),
2564
+ sessionId,
2565
+ role: "assistant",
2566
+ content: "",
2567
+ status: "streaming",
2568
+ createdAt
2569
+ });
2570
+ var canSendChatMessage = ({
2571
+ value,
2572
+ attachmentCount = 0,
2573
+ isModelsLoading,
2574
+ isModelsError,
2575
+ hasModels
2576
+ }) => {
2577
+ const hasText = Boolean(value.trim());
2578
+ const hasAttachments = attachmentCount > 0;
2579
+ if (!hasText && !hasAttachments)
2580
+ return false;
2581
+ if (!hasText && hasAttachments)
2582
+ return true;
2583
+ return !isModelsLoading && !isModelsError && hasModels;
2584
+ };
2585
+ var shouldSubmitChatComposer = ({
2586
+ key,
2587
+ shiftKey,
2588
+ canSend
2589
+ }) => key === "Enter" && !shiftKey && canSend;
2590
+ var shouldStopChatComposer = ({
2591
+ key,
2592
+ shiftKey,
2593
+ isStreaming
2594
+ }) => key === "Enter" && !shiftKey && isStreaming;
2595
+ var resolveSelectedChatModel = ({
2596
+ currentModel,
2597
+ availableModels,
2598
+ isModelsLoading
2599
+ }) => {
2600
+ if (!availableModels.length) {
2601
+ return isModelsLoading ? currentModel : "";
2602
+ }
2603
+ if (currentModel && availableModels.some((model) => model.id === currentModel)) {
2604
+ return currentModel;
2605
+ }
2606
+ return availableModels[0].id;
2607
+ };
2608
+ var resolveSendSession = ({
2609
+ activeSessionId,
2610
+ selectedModel,
2611
+ selectedMode,
2612
+ nowIso: nowIso2,
2613
+ createSessionId
2614
+ }) => {
2615
+ if (activeSessionId) {
2616
+ return {
2617
+ localSessionId: activeSessionId,
2618
+ sessionId: isDraftChatSessionId(activeSessionId) ? void 0 : activeSessionId
2619
+ };
2620
+ }
2621
+ const session = createDraftChatSession({
2622
+ model: selectedModel,
2623
+ mode: selectedMode ?? DEFAULT_CHAT_AGENT_MODE,
2624
+ nowIso: nowIso2,
2625
+ createSessionId
2626
+ });
2627
+ return { session, localSessionId: session.sessionId, sessionId: void 0 };
2628
+ };
2629
+
2630
+ // src/components/chat-composer/hooks/use-chat-composer.ts
2631
+ import { useCallback as useCallback3, useEffect as useEffect4, useRef as useRef6, useState as useState7 } from "react";
2632
+
2633
+ // src/components/chat-composer/hooks/use-composer-attachments.ts
2634
+ import { useEffect as useEffect3, useRef as useRef5, useState as useState6 } from "react";
2635
+ var SUPPORTED_IMAGE_MIME_TYPES = /* @__PURE__ */ new Set(["image/png", "image/jpeg", "image/webp"]);
2636
+ var MAX_COMPOSER_ATTACHMENTS = 10;
2637
+ var createObjectUrl = (file) => typeof URL !== "undefined" && typeof URL.createObjectURL === "function" ? URL.createObjectURL(file) : "";
2638
+ var revokeObjectUrl = (url) => {
2639
+ if (url && typeof URL !== "undefined" && typeof URL.revokeObjectURL === "function") {
2640
+ URL.revokeObjectURL(url);
2641
+ }
2642
+ };
2643
+ var createAttachmentId = () => `attachment-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
2644
+ var releaseComposerAttachments = (attachments) => {
2645
+ attachments.forEach((attachment) => revokeObjectUrl(attachment.previewUrl));
2646
+ };
2647
+ var useComposerAttachments = () => {
2648
+ const [attachments, setAttachments] = useState6([]);
2649
+ const attachmentsRef = useRef5([]);
2650
+ useEffect3(() => {
2651
+ attachmentsRef.current = attachments;
2652
+ }, [attachments]);
2653
+ useEffect3(
2654
+ () => () => {
2655
+ releaseComposerAttachments(attachmentsRef.current);
2656
+ },
2657
+ []
2658
+ );
2659
+ const appendFiles = (files) => {
2660
+ const validFiles = Array.from(files).filter((file) => SUPPORTED_IMAGE_MIME_TYPES.has(file.type));
2661
+ if (!validFiles.length)
2662
+ return { addedCount: 0, limitExceeded: false };
2663
+ const currentAttachments = attachmentsRef.current;
2664
+ const remainingSlots = MAX_COMPOSER_ATTACHMENTS - currentAttachments.length;
2665
+ if (remainingSlots <= 0)
2666
+ return { addedCount: 0, limitExceeded: true };
2667
+ const filesToAppend = validFiles.slice(0, remainingSlots);
2668
+ const newAttachments = filesToAppend.map((file) => ({
2669
+ id: createAttachmentId(),
2670
+ file,
2671
+ name: file.name,
2672
+ mimeType: file.type,
2673
+ size: file.size,
2674
+ previewUrl: createObjectUrl(file)
2675
+ }));
2676
+ setAttachments((current) => [...current, ...newAttachments]);
2677
+ return {
2678
+ addedCount: filesToAppend.length,
2679
+ limitExceeded: validFiles.length > filesToAppend.length
2680
+ };
2681
+ };
2682
+ const removeAttachment = (attachmentId) => {
2683
+ setAttachments((current) => {
2684
+ const target = current.find((a) => a.id === attachmentId);
2685
+ if (target)
2686
+ releaseComposerAttachments([target]);
2687
+ return current.filter((a) => a.id !== attachmentId);
2688
+ });
2689
+ };
2690
+ const clearAttachments = () => {
2691
+ setAttachments((current) => {
2692
+ releaseComposerAttachments(current);
2693
+ return [];
2694
+ });
2695
+ };
2696
+ const takeMessageAttachments = () => {
2697
+ const currentAttachments = attachmentsRef.current;
2698
+ if (!currentAttachments.length) {
2699
+ return [];
2700
+ }
2701
+ const nextMessageAttachments = currentAttachments.map(({ file: _file, ...attachment }) => ({
2702
+ ...attachment
2703
+ }));
2704
+ attachmentsRef.current = [];
2705
+ setAttachments([]);
2706
+ return nextMessageAttachments;
2707
+ };
2708
+ return {
2709
+ attachments,
2710
+ appendFiles,
2711
+ removeAttachment,
2712
+ clearAttachments,
2713
+ takeMessageAttachments
2714
+ };
2715
+ };
2716
+
2717
+ // src/components/chat-composer/hooks/use-chat-composer.ts
2718
+ var nowIso = () => (/* @__PURE__ */ new Date()).toISOString();
2719
+ var ATTACHMENT_NOTICE_DURATION_MS = 3e3;
2720
+ var STOP_WAIT_TIMEOUT_MS = 3e3;
2721
+ var resolveAccumulatedContent = (currentContent, update) => {
2722
+ if (update.content !== void 0) {
2723
+ return update.content;
2724
+ }
2725
+ if (update.contentDelta) {
2726
+ return currentContent + update.contentDelta;
2727
+ }
2728
+ return currentContent;
2729
+ };
2730
+ var normalizeChatErrorMessage = (message, labels) => {
2731
+ const trimmedMessage = message?.trim();
2732
+ if (!trimmedMessage) {
2733
+ return labels.genericError;
2734
+ }
2735
+ if (trimmedMessage === "Failed to fetch") {
2736
+ return labels.networkError;
2737
+ }
2738
+ return trimmedMessage;
2739
+ };
2740
+ var useChatComposer = () => {
2741
+ const { transport, enableImageAttachments, labels } = useChatContext();
2742
+ const activeSessionId = useChatStore((s) => s.activeSessionId);
2743
+ const activeSession = useChatStore(
2744
+ (s) => s.sessions.find((x) => x.sessionId === s.activeSessionId) ?? null
2745
+ );
2746
+ const preferredMode = useChatStore((s) => s.preferredMode);
2747
+ const streamingSessionId = useChatStore(
2748
+ (s) => Object.entries(s.isStreamingBySession).find(([, v]) => v)?.[0] ?? null
2749
+ );
2750
+ const isStreaming = Boolean(streamingSessionId);
2751
+ const isStopping = useChatStore(
2752
+ (s) => streamingSessionId ? s.isStoppingBySession[streamingSessionId] ?? false : false
2753
+ );
2754
+ const createSession = useChatStore((s) => s.createSession);
2755
+ const replaceSessionId = useChatStore((s) => s.replaceSessionId);
2756
+ const appendMessage = useChatStore((s) => s.appendMessage);
2757
+ const startStreamingMessage = useChatStore((s) => s.startStreamingMessage);
2758
+ const patchStreamingMessage = useChatStore((s) => s.patchStreamingMessage);
2759
+ const completeStreamingMessage = useChatStore((s) => s.completeStreamingMessage);
2760
+ const requestStopStreaming = useChatStore((s) => s.requestStopStreaming);
2761
+ const finalizeStoppedStreamingMessage = useChatStore((s) => s.finalizeStoppedStreamingMessage);
2762
+ const setSessionError = useChatStore((s) => s.setSessionError);
2763
+ const clearSessionError = useChatStore((s) => s.clearSessionError);
2764
+ const setPreferredMode = useChatStore((s) => s.setPreferredMode);
2765
+ const setSessionMode = useChatStore((s) => s.setSessionMode);
2766
+ const [availableModels, setAvailableModels] = useState7([]);
2767
+ const [isModelsLoading, setIsModelsLoading] = useState7(true);
2768
+ const [isModelsError, setIsModelsError] = useState7(false);
2769
+ const fetchModels = useCallback3(async () => {
2770
+ setIsModelsLoading(true);
2771
+ setIsModelsError(false);
2772
+ try {
2773
+ const modelsResponse = await transport.getModels();
2774
+ setAvailableModels(modelsResponse.data);
2775
+ } catch {
2776
+ setIsModelsError(true);
2777
+ } finally {
2778
+ setIsModelsLoading(false);
2779
+ }
2780
+ }, [transport]);
2781
+ useEffect4(() => {
2782
+ void fetchModels();
2783
+ }, [fetchModels]);
2784
+ const hasModels = availableModels.length > 0;
2785
+ const [value, setValue] = useState7("");
2786
+ const [selectedModel, setSelectedModel] = useState7("");
2787
+ const [selectedMode, setSelectedModeLocal] = useState7(DEFAULT_CHAT_AGENT_MODE);
2788
+ const [attachmentNotice, setAttachmentNotice] = useState7(null);
2789
+ const { attachments, appendFiles, removeAttachment, takeMessageAttachments } = useComposerAttachments();
2790
+ const abortControllerRef = useRef6(null);
2791
+ const stopRequestRef = useRef6(null);
2792
+ const lastRequestRef = useRef6(null);
2793
+ useEffect4(() => {
2794
+ setSelectedModel(
2795
+ (current) => resolveSelectedChatModel({ currentModel: current, availableModels, isModelsLoading })
2796
+ );
2797
+ }, [availableModels, isModelsLoading]);
2798
+ useEffect4(() => {
2799
+ if (activeSession) {
2800
+ setSelectedModeLocal(activeSession.mode ?? DEFAULT_CHAT_AGENT_MODE);
2801
+ return;
2802
+ }
2803
+ setSelectedModeLocal(preferredMode ?? DEFAULT_CHAT_AGENT_MODE);
2804
+ }, [activeSession, preferredMode]);
2805
+ useEffect4(() => {
2806
+ if (!attachmentNotice)
2807
+ return;
2808
+ const timeoutId = window.setTimeout(
2809
+ () => setAttachmentNotice(null),
2810
+ ATTACHMENT_NOTICE_DURATION_MS
2811
+ );
2812
+ return () => window.clearTimeout(timeoutId);
2813
+ }, [attachmentNotice]);
2814
+ const clearStopTimeout = (sessionId) => {
2815
+ if (!stopRequestRef.current)
2816
+ return;
2817
+ if (sessionId && stopRequestRef.current.sessionId !== sessionId)
2818
+ return;
2819
+ if (stopRequestRef.current.timeoutId !== null) {
2820
+ window.clearTimeout(stopRequestRef.current.timeoutId);
2821
+ stopRequestRef.current.timeoutId = null;
2822
+ }
2823
+ };
2824
+ const clearStopRequest = useCallback3((sessionId) => {
2825
+ if (!stopRequestRef.current)
2826
+ return;
2827
+ if (sessionId && stopRequestRef.current.sessionId !== sessionId)
2828
+ return;
2829
+ clearStopTimeout(sessionId);
2830
+ stopRequestRef.current = null;
2831
+ }, []);
2832
+ const finalizeStop = useCallback3(
2833
+ (sessionId) => {
2834
+ if (stopRequestRef.current?.sessionId === sessionId) {
2835
+ if (stopRequestRef.current.finalized)
2836
+ return;
2837
+ stopRequestRef.current.finalized = true;
2838
+ }
2839
+ clearStopTimeout(sessionId);
2840
+ abortControllerRef.current?.abort();
2841
+ abortControllerRef.current = null;
2842
+ finalizeStoppedStreamingMessage(sessionId);
2843
+ clearStopRequest(sessionId);
2844
+ },
2845
+ [clearStopRequest, finalizeStoppedStreamingMessage]
2846
+ );
2847
+ const runStream = useCallback3(
2848
+ async ({
2849
+ localSessionId,
2850
+ sessionId,
2851
+ content,
2852
+ model,
2853
+ mode
2854
+ }) => {
2855
+ clearStopRequest();
2856
+ let currentSessionId = localSessionId;
2857
+ clearSessionError(currentSessionId);
2858
+ const assistantMessage = createAssistantStreamingMessage({
2859
+ sessionId: currentSessionId,
2860
+ createdAt: nowIso(),
2861
+ createMessageId: () => `assistant-${Date.now()}`
2862
+ });
2863
+ startStreamingMessage(currentSessionId, assistantMessage);
2864
+ abortControllerRef.current?.abort();
2865
+ abortControllerRef.current = new AbortController();
2866
+ lastRequestRef.current = { localSessionId, sessionId, content, model, mode };
2867
+ let accumulated = "";
2868
+ try {
2869
+ await transport.startStream({
2870
+ sessionId,
2871
+ model,
2872
+ mode,
2873
+ content,
2874
+ signal: abortControllerRef.current.signal,
2875
+ onSessionId: (nextSessionId) => {
2876
+ if (!nextSessionId || nextSessionId === currentSessionId)
2877
+ return;
2878
+ replaceSessionId(currentSessionId, nextSessionId);
2879
+ currentSessionId = nextSessionId;
2880
+ lastRequestRef.current = {
2881
+ localSessionId: nextSessionId,
2882
+ sessionId: nextSessionId,
2883
+ content,
2884
+ model,
2885
+ mode
2886
+ };
2887
+ },
2888
+ onUpdate: (update) => {
2889
+ accumulated = resolveAccumulatedContent(accumulated, update);
2890
+ patchStreamingMessage(currentSessionId, {
2891
+ ...update.blocks ? { blocks: update.blocks } : {},
2892
+ ...update.content !== void 0 || update.contentDelta ? { content: accumulated } : {}
2893
+ });
2894
+ },
2895
+ onDone: () => {
2896
+ if (stopRequestRef.current?.sessionId === currentSessionId) {
2897
+ finalizeStop(currentSessionId);
2898
+ return;
2899
+ }
2900
+ completeStreamingMessage(currentSessionId);
2901
+ abortControllerRef.current = null;
2902
+ clearStopRequest(currentSessionId);
2903
+ },
2904
+ onError: (streamError) => {
2905
+ if (stopRequestRef.current?.sessionId === currentSessionId) {
2906
+ finalizeStop(currentSessionId);
2907
+ return;
2908
+ }
2909
+ finalizeStoppedStreamingMessage(currentSessionId);
2910
+ setSessionError(
2911
+ currentSessionId,
2912
+ normalizeChatErrorMessage(streamError.message, labels)
2913
+ );
2914
+ abortControllerRef.current = null;
2915
+ clearStopRequest(currentSessionId);
2916
+ }
2917
+ });
2918
+ } catch {
2919
+ abortControllerRef.current = null;
2920
+ }
2921
+ },
2922
+ [
2923
+ transport,
2924
+ clearSessionError,
2925
+ clearStopRequest,
2926
+ finalizeStop,
2927
+ labels,
2928
+ startStreamingMessage,
2929
+ replaceSessionId,
2930
+ patchStreamingMessage,
2931
+ completeStreamingMessage,
2932
+ finalizeStoppedStreamingMessage,
2933
+ setSessionError
2934
+ ]
2935
+ );
2936
+ const send = useCallback3(
2937
+ async (contentOverride) => {
2938
+ const content = (contentOverride ?? value).trim();
2939
+ const hasText = Boolean(content);
2940
+ const hasAttachments = attachments.length > 0;
2941
+ if (!canSendChatMessage({
2942
+ value: content,
2943
+ attachmentCount: attachments.length,
2944
+ isModelsLoading,
2945
+ isModelsError,
2946
+ hasModels
2947
+ })) {
2948
+ return;
2949
+ }
2950
+ if (hasText && !(selectedModel || activeSession?.model || availableModels[0]?.id)) {
2951
+ return;
2952
+ }
2953
+ const resolvedModel = selectedModel || activeSession?.model || availableModels[0]?.id || "local-image";
2954
+ const { localSessionId, sessionId, session } = resolveSendSession({
2955
+ activeSessionId,
2956
+ selectedModel: resolvedModel,
2957
+ selectedMode,
2958
+ nowIso,
2959
+ createSessionId: createDraftChatSessionId
2960
+ });
2961
+ if (session)
2962
+ createSession(session);
2963
+ const messageAttachments = takeMessageAttachments();
2964
+ const userMessage = createUserMessage({
2965
+ sessionId: localSessionId,
2966
+ content,
2967
+ attachments: messageAttachments,
2968
+ localOnly: hasAttachments,
2969
+ createdAt: nowIso(),
2970
+ createMessageId: () => `user-${Date.now()}`
2971
+ });
2972
+ appendMessage(localSessionId, userMessage);
2973
+ setAttachmentNotice(null);
2974
+ setValue("");
2975
+ if (!hasText)
2976
+ return;
2977
+ await runStream({
2978
+ localSessionId,
2979
+ sessionId,
2980
+ content,
2981
+ model: resolvedModel,
2982
+ mode: selectedMode
2983
+ });
2984
+ },
2985
+ [
2986
+ value,
2987
+ attachments,
2988
+ isModelsLoading,
2989
+ isModelsError,
2990
+ hasModels,
2991
+ selectedModel,
2992
+ activeSession,
2993
+ availableModels,
2994
+ activeSessionId,
2995
+ selectedMode,
2996
+ createSession,
2997
+ takeMessageAttachments,
2998
+ appendMessage,
2999
+ runStream
3000
+ ]
3001
+ );
3002
+ return {
3003
+ state: {
3004
+ value,
3005
+ attachments,
3006
+ attachmentNotice,
3007
+ isStreaming,
3008
+ isStopping,
3009
+ selectedModel,
3010
+ selectedMode,
3011
+ availableModels,
3012
+ isModelsLoading,
3013
+ isModelsError,
3014
+ hasModels
3015
+ },
3016
+ actions: {
3017
+ setValue,
3018
+ send,
3019
+ pickImages: (files) => {
3020
+ if (!enableImageAttachments)
3021
+ return;
3022
+ const result = appendFiles(files);
3023
+ setAttachmentNotice(result.limitExceeded ? "limit_reached" : null);
3024
+ },
3025
+ pasteImages: (files) => {
3026
+ if (!enableImageAttachments)
3027
+ return;
3028
+ const result = appendFiles(files);
3029
+ setAttachmentNotice(result.limitExceeded ? "limit_reached" : null);
3030
+ },
3031
+ removeAttachment: (attachmentId) => {
3032
+ removeAttachment(attachmentId);
3033
+ setAttachmentNotice(null);
3034
+ },
3035
+ setSelectedModel,
3036
+ setSelectedMode: (mode) => {
3037
+ setSelectedModeLocal(mode);
3038
+ setPreferredMode(mode);
3039
+ if (activeSessionId)
3040
+ setSessionMode(activeSessionId, mode);
3041
+ if (lastRequestRef.current && activeSessionId && (lastRequestRef.current.localSessionId === activeSessionId || lastRequestRef.current.sessionId === activeSessionId)) {
3042
+ lastRequestRef.current = { ...lastRequestRef.current, mode };
3043
+ }
3044
+ },
3045
+ reloadModels: () => void fetchModels(),
3046
+ stop: async () => {
3047
+ if (!streamingSessionId)
3048
+ return;
3049
+ if (isStopping)
3050
+ return;
3051
+ if (isDraftChatSessionId(streamingSessionId)) {
3052
+ finalizeStop(streamingSessionId);
3053
+ return;
3054
+ }
3055
+ requestStopStreaming(streamingSessionId);
3056
+ stopRequestRef.current = {
3057
+ sessionId: streamingSessionId,
3058
+ timeoutId: window.setTimeout(() => {
3059
+ finalizeStop(streamingSessionId);
3060
+ }, STOP_WAIT_TIMEOUT_MS),
3061
+ finalized: false
3062
+ };
3063
+ try {
3064
+ const result = await transport.terminateStream(streamingSessionId);
3065
+ if (!result.terminated) {
3066
+ console.error("Failed to terminate chat session: server returned not terminated");
3067
+ }
3068
+ finalizeStop(streamingSessionId);
3069
+ } catch (err) {
3070
+ console.error("Failed to terminate chat session", err);
3071
+ finalizeStop(streamingSessionId);
3072
+ }
3073
+ },
3074
+ retry: () => {
3075
+ if (!lastRequestRef.current)
3076
+ return;
3077
+ void runStream(lastRequestRef.current);
3078
+ }
3079
+ }
3080
+ };
3081
+ };
3082
+
3083
+ // src/components/chat-composer/components/chat-composer-attachment-list.tsx
3084
+ import { useState as useState8 } from "react";
3085
+ import styled11 from "@emotion/styled";
3086
+ import { Fragment as Fragment3, jsx as jsx12, jsxs as jsxs8 } from "@emotion/react/jsx-runtime";
3087
+ var ChatComposerAttachmentList = ({
3088
+ attachments,
3089
+ onRemoveAttachment
3090
+ }) => {
3091
+ const [activeImage, setActiveImage] = useState8(null);
3092
+ if (!attachments.length) {
3093
+ return null;
3094
+ }
3095
+ return /* @__PURE__ */ jsxs8(Fragment3, { children: [
3096
+ /* @__PURE__ */ jsx12(AttachmentList, { "data-testid": "chat-composer-attachment-list", children: attachments.map((attachment) => /* @__PURE__ */ jsxs8(AttachmentCard, { children: [
3097
+ /* @__PURE__ */ jsx12(
3098
+ AttachmentPreviewButton,
3099
+ {
3100
+ type: "button",
3101
+ "aria-label": `${attachment.name} preview`,
3102
+ onClick: () => setActiveImage(attachment),
3103
+ children: /* @__PURE__ */ jsx12(AttachmentThumb, { src: attachment.previewUrl, alt: attachment.name })
3104
+ }
3105
+ ),
3106
+ /* @__PURE__ */ jsx12(
3107
+ AttachmentRemoveButton,
3108
+ {
3109
+ type: "button",
3110
+ "aria-label": `Remove ${attachment.name}`,
3111
+ onClick: (event) => {
3112
+ event.stopPropagation();
3113
+ onRemoveAttachment(attachment.id);
3114
+ },
3115
+ children: /* @__PURE__ */ jsx12(CloseGlyph, { "aria-hidden": "true" })
3116
+ }
3117
+ )
3118
+ ] }, attachment.id)) }),
3119
+ activeImage ? /* @__PURE__ */ jsx12(
3120
+ ImageViewer,
3121
+ {
3122
+ src: activeImage.previewUrl,
3123
+ alt: activeImage.name,
3124
+ onClose: () => setActiveImage(null)
3125
+ }
3126
+ ) : null
3127
+ ] });
3128
+ };
3129
+ var AttachmentList = styled11.div`
3130
+ display: flex;
3131
+ flex-wrap: wrap;
3132
+ gap: 10px;
3133
+ padding: 12px 12px 0;
3134
+ max-height: 238px;
3135
+ overflow-y: auto;
3136
+ overscroll-behavior: contain;
3137
+
3138
+ &::-webkit-scrollbar {
3139
+ width: 8px;
3140
+ }
3141
+
3142
+ &::-webkit-scrollbar-thumb {
3143
+ background: rgba(255, 255, 255, 0.2);
3144
+ border-radius: 999px;
3145
+ }
3146
+
3147
+ &::-webkit-scrollbar-track {
3148
+ background: transparent;
3149
+ }
3150
+ `;
3151
+ var AttachmentCard = styled11.div`
3152
+ position: relative;
3153
+ width: 108px;
3154
+ height: 72px;
3155
+ overflow: hidden;
3156
+ border-radius: 14px;
3157
+ border: 1px solid rgba(255, 255, 255, 0.12);
3158
+ background: rgba(255, 255, 255, 0.04);
3159
+ `;
3160
+ var AttachmentPreviewButton = styled11.button`
3161
+ width: 100%;
3162
+ height: 100%;
3163
+ padding: 0;
3164
+ border: 0;
3165
+ background: transparent;
3166
+ cursor: zoom-in;
3167
+ `;
3168
+ var AttachmentThumb = styled11.img`
3169
+ width: 100%;
3170
+ height: 100%;
3171
+ object-fit: cover;
3172
+ display: block;
3173
+ `;
3174
+ var AttachmentRemoveButton = styled11.button`
3175
+ position: absolute;
3176
+ top: 6px;
3177
+ right: 6px;
3178
+ width: 22px;
3179
+ height: 22px;
3180
+ display: grid;
3181
+ place-items: center;
3182
+ padding: 0;
3183
+ border: 1px solid rgba(255, 255, 255, 0.16);
3184
+ border-radius: 999px;
3185
+ background: rgba(20, 20, 24, 0.92);
3186
+ color: rgba(255, 255, 255, 0.94);
3187
+ cursor: pointer;
3188
+ line-height: 0;
3189
+ box-shadow:
3190
+ inset 0 1px 0 rgba(255, 255, 255, 0.08),
3191
+ 0 4px 10px rgba(0, 0, 0, 0.24);
3192
+ transition:
3193
+ background-color 140ms ease,
3194
+ border-color 140ms ease,
3195
+ transform 140ms ease,
3196
+ box-shadow 140ms ease;
3197
+
3198
+ &:hover {
3199
+ background: rgba(37, 37, 44, 0.98);
3200
+ border-color: rgba(255, 255, 255, 0.28);
3201
+ transform: scale(1.06);
3202
+ box-shadow:
3203
+ inset 0 1px 0 rgba(255, 255, 255, 0.12),
3204
+ 0 8px 18px rgba(0, 0, 0, 0.32);
3205
+ }
3206
+
3207
+ &:active {
3208
+ transform: scale(0.96);
3209
+ background: rgba(30, 30, 35, 0.98);
3210
+ }
3211
+ `;
3212
+ var CloseGlyph = styled11.span`
3213
+ position: relative;
3214
+ width: 11px;
3215
+ height: 11px;
3216
+ display: block;
3217
+
3218
+ &::before,
3219
+ &::after {
3220
+ content: '';
3221
+ position: absolute;
3222
+ top: 50%;
3223
+ left: 50%;
3224
+ width: 11px;
3225
+ height: 1.75px;
3226
+ border-radius: 999px;
3227
+ background: currentColor;
3228
+ transform-origin: center;
3229
+ }
3230
+
3231
+ &::before {
3232
+ transform: translate(-50%, -50%) rotate(45deg);
3233
+ }
3234
+
3235
+ &::after {
3236
+ transform: translate(-50%, -50%) rotate(-45deg);
3237
+ }
3238
+ `;
3239
+
3240
+ // src/components/chat-composer/components/chat-model-control.tsx
3241
+ import styled12 from "@emotion/styled";
3242
+ import { Select } from "@xinghunm/compass-ui";
3243
+ import { jsx as jsx13, jsxs as jsxs9 } from "@emotion/react/jsx-runtime";
3244
+ var ChatModelControl = ({
3245
+ selectedModel,
3246
+ availableModels,
3247
+ isModelsLoading,
3248
+ isModelsError,
3249
+ hasModels,
3250
+ onSelectedModelChange,
3251
+ onReloadModels
3252
+ }) => {
3253
+ if (isModelsError) {
3254
+ return /* @__PURE__ */ jsxs9(
3255
+ ModelReloadButton,
3256
+ {
3257
+ type: "button",
3258
+ "data-testid": "chat-model-reload",
3259
+ "aria-label": "Reload",
3260
+ onClick: onReloadModels,
3261
+ children: [
3262
+ /* @__PURE__ */ jsx13("span", { children: "Failed to load models" }),
3263
+ /* @__PURE__ */ jsxs9(
3264
+ ReloadIcon,
3265
+ {
3266
+ "data-testid": "chat-model-reload-icon",
3267
+ "aria-hidden": "true",
3268
+ width: "16",
3269
+ height: "16",
3270
+ viewBox: "0 0 1024 1024",
3271
+ fill: "currentColor",
3272
+ xmlns: "http://www.w3.org/2000/svg",
3273
+ children: [
3274
+ /* @__PURE__ */ jsx13("path", { d: "M895.469672 511.745197c0-146.498562-82.099856-273.805016-202.788589-338.470805l22.072715-46.630017c-4.50664-12.609179-18.382673-19.176758-30.991852-14.670118l-92.436272 33.040511c-12.609179 4.50664-19.176758 18.382673-14.670118 30.991852l33.040511 92.436272c4.50664 12.609179 18.382673 19.176758 30.991852 14.670118l24.581861-51.92972c99.069343 54.335513 166.240185 159.596881 166.240185 280.561907 0 165.56685-125.817544 301.747415-287.057855 318.14692l0 0.022513c-17.730826 0-32.105209 14.374382-32.105209 32.105209 0 17.730826 14.374382 32.105209 32.105209 32.105209 2.098801 0 4.149507-0.207731 6.135744-0.592494C744.270041 874.039593 895.469672 710.564381 895.469672 511.745197z" }),
3275
+ /* @__PURE__ */ jsx13("path", { d: "M480.616222 129.23948c-0.041956 0-0.082888 0.00307-0.124843 0.00307l0-0.00307c-0.01535 0.001023-0.031722 0.00307-0.047072 0.004093-1.892093 0.010233-3.744277 0.189312-5.545296 0.5137-194.674794 18.529005-346.957083 182.459588-346.957083 381.987924 0 147.431817 83.146699 275.42798 205.097168 339.700819l-24.814152 52.419883c4.50664 12.609179 18.382673 19.176758 30.991852 14.670118l92.436272-33.040511c12.609179-4.50664 19.176758-18.382673 14.670118-30.991852l-33.040511-92.436272c-4.50664-12.609179-18.382673-19.176758-30.991852-14.670118l-21.853727 46.167482c-100.326986-53.964052-168.535461-159.920246-168.535461-281.81955 0-166.089759 126.616746-302.591643 288.588721-318.284043l0-0.014326c0.041956 0 0.082888 0.00307 0.124843 0.00307 17.730826 0 32.105209-14.374382 32.105209-32.105209C512.721431 143.613862 498.347049 129.23948 480.616222 129.23948z" })
3276
+ ]
3277
+ }
3278
+ )
3279
+ ]
3280
+ }
3281
+ );
3282
+ }
3283
+ if (isModelsLoading) {
3284
+ return /* @__PURE__ */ jsx13(ModelBadge, { children: "Loading models..." });
3285
+ }
3286
+ if (hasModels && selectedModel) {
3287
+ if (availableModels.length > 1) {
3288
+ return /* @__PURE__ */ jsx13(
3289
+ ModelSelect,
3290
+ {
3291
+ "data-testid": "chat-model-select",
3292
+ "aria-label": "Select model",
3293
+ value: selectedModel,
3294
+ onChange: (value) => onSelectedModelChange(String(value)),
3295
+ options: availableModels.map((model) => ({
3296
+ label: model.id,
3297
+ value: model.id
3298
+ }))
3299
+ }
3300
+ );
3301
+ }
3302
+ return /* @__PURE__ */ jsx13(ModelBadge, { children: selectedModel });
3303
+ }
3304
+ return /* @__PURE__ */ jsx13(ModelBadge, { children: "No model available" });
3305
+ };
3306
+ var ModelBadge = styled12.span`
3307
+ border-radius: 999px;
3308
+ border: 1px solid var(--border-hover);
3309
+ padding: 5px 12px;
3310
+ font-weight: 400;
3311
+ font-size: 12px;
3312
+ color: var(--text-secondary);
3313
+ line-height: 12px;
3314
+ `;
3315
+ var ModelReloadButton = styled12.button`
3316
+ display: inline-flex;
3317
+ align-items: center;
3318
+ gap: 8px;
3319
+ border-radius: 999px;
3320
+ min-height: 28px;
3321
+ border: 1px solid rgba(255, 255, 255, 0.14);
3322
+ background: rgba(255, 255, 255, 0.04);
3323
+ padding: 6px 12px;
3324
+ color: rgba(255, 255, 255, 0.72);
3325
+ font-size: 12px;
3326
+ line-height: 1;
3327
+ cursor: pointer;
3328
+
3329
+ transition:
3330
+ background 160ms ease,
3331
+ border-color 160ms ease,
3332
+ color 160ms ease;
3333
+
3334
+ &:hover {
3335
+ background: rgba(255, 255, 255, 0.08);
3336
+ border-color: rgba(255, 255, 255, 0.22);
3337
+ color: rgba(255, 255, 255, 0.88);
3338
+ }
3339
+ `;
3340
+ var ReloadIcon = styled12.svg`
3341
+ flex-shrink: 0;
3342
+ `;
3343
+ var ModelSelect = styled12(Select)`
3344
+ && {
3345
+ width: auto;
3346
+ min-width: 0;
3347
+ max-width: 100%;
3348
+
3349
+ .compass-select-selector {
3350
+ line-height: 1;
3351
+ border-radius: 999px;
3352
+ min-height: 24px;
3353
+ height: 24px;
3354
+ background: rgba(255, 255, 255, 0.04);
3355
+ border-color: rgba(255, 255, 255, 0.12);
3356
+ color: rgba(255, 255, 255, 0.82);
3357
+ padding: 4px 12px;
3358
+ }
3359
+ }
3360
+ `;
3361
+
3362
+ // src/components/chat-composer/components/chat-mode-control.tsx
3363
+ import styled13 from "@emotion/styled";
3364
+ import { Select as Select2 } from "@xinghunm/compass-ui";
3365
+ import { jsx as jsx14 } from "@emotion/react/jsx-runtime";
3366
+ var ChatModeControl = ({
3367
+ value,
3368
+ disabled = false,
3369
+ labels,
3370
+ onChange
3371
+ }) => {
3372
+ return /* @__PURE__ */ jsx14(
3373
+ ModeSelect,
3374
+ {
3375
+ "data-testid": "chat-mode-select",
3376
+ "aria-label": "Select mode",
3377
+ value,
3378
+ disabled,
3379
+ onChange: (v) => onChange(String(v)),
3380
+ options: CHAT_AGENT_MODES.map((mode) => ({
3381
+ label: labels[mode],
3382
+ value: mode
3383
+ }))
3384
+ }
3385
+ );
3386
+ };
3387
+ var ModeSelect = styled13(Select2)`
3388
+ && {
3389
+ flex: 0 1 auto;
3390
+ width: auto;
3391
+ min-width: 0;
3392
+ max-width: 100%;
3393
+
3394
+ .compass-select-selector {
3395
+ line-height: 1;
3396
+ border-radius: 999px;
3397
+ min-height: 24px;
3398
+ height: 24px;
3399
+ background: rgba(255, 255, 255, 0.04);
3400
+ border-color: rgba(255, 255, 255, 0.12);
3401
+ color: rgba(255, 255, 255, 0.82);
3402
+ padding: 4px 12px;
3403
+ }
3404
+ }
3405
+ `;
3406
+
3407
+ // src/components/chat-composer/components/chat-send-actions.tsx
3408
+ import styled14 from "@emotion/styled";
3409
+ import { Button } from "@xinghunm/compass-ui";
3410
+ import { Fragment as Fragment4, jsx as jsx15 } from "@emotion/react/jsx-runtime";
3411
+ var ArrowUpIcon = () => /* @__PURE__ */ jsx15(
3412
+ "svg",
3413
+ {
3414
+ "aria-hidden": "true",
3415
+ width: "12",
3416
+ height: "12",
3417
+ viewBox: "0 0 12 12",
3418
+ fill: "none",
3419
+ xmlns: "http://www.w3.org/2000/svg",
3420
+ children: /* @__PURE__ */ jsx15(
3421
+ "path",
3422
+ {
3423
+ d: "M6 10V2M6 2L2 6M6 2L10 6",
3424
+ stroke: "currentColor",
3425
+ strokeWidth: "1.75",
3426
+ strokeLinecap: "round",
3427
+ strokeLinejoin: "round"
3428
+ }
3429
+ )
3430
+ }
3431
+ );
3432
+ var ChatSendActions = ({
3433
+ canSend,
3434
+ isStreaming,
3435
+ isStopping,
3436
+ onStop,
3437
+ onSend
3438
+ }) => /* @__PURE__ */ jsx15(Fragment4, { children: isStreaming ? /* @__PURE__ */ jsx15(
3439
+ StopButton,
3440
+ {
3441
+ type: "button",
3442
+ "aria-label": "Stop",
3443
+ "aria-busy": isStopping,
3444
+ "data-testid": "chat-composer-stop",
3445
+ disabled: isStopping,
3446
+ shape: "circle",
3447
+ onClick: () => void onStop(),
3448
+ children: /* @__PURE__ */ jsx15(StopGlyph, { "aria-hidden": "true" })
3449
+ }
3450
+ ) : /* @__PURE__ */ jsx15(
3451
+ PrimaryButton,
3452
+ {
3453
+ $canSend: canSend,
3454
+ type: "button",
3455
+ icon: /* @__PURE__ */ jsx15(ArrowUpIcon, {}),
3456
+ "aria-label": "Send",
3457
+ "data-testid": "chat-composer-send",
3458
+ disabled: !canSend,
3459
+ shape: "circle",
3460
+ onClick: () => void onSend()
3461
+ }
3462
+ ) });
3463
+ var PrimaryButton = styled14(Button)`
3464
+ && {
3465
+ min-width: 24px;
3466
+ width: 24px;
3467
+ height: 24px;
3468
+ background: ${({ $canSend }) => $canSend ? "#fcfbf8" : "rgba(255, 255, 255, 0.3)"};
3469
+ color: ${({ $canSend }) => $canSend ? "#5b5448" : "rgba(255, 255, 255, 0.72)"};
3470
+ border-radius: 12px;
3471
+ border: 1px solid ${({ $canSend }) => $canSend ? "rgba(198, 188, 170, 0.38)" : "transparent"};
3472
+ box-shadow: ${({ $canSend }) => $canSend ? "inset 0 1px 0 rgba(255, 255, 255, 0.92), 0 8px 18px rgba(0, 0, 0, 0.18)" : "none"};
3473
+ svg {
3474
+ color: currentColor;
3475
+ stroke: currentColor;
3476
+ }
3477
+
3478
+ &:hover:not(:disabled) {
3479
+ background: ${({ $canSend }) => $canSend ? "#f7f4ec" : "rgba(255, 255, 255, 0.3)"};
3480
+ color: ${({ $canSend }) => $canSend ? "#4f493f" : "rgba(255, 255, 255, 0.72)"};
3481
+ border-color: ${({ $canSend }) => $canSend ? "rgba(198, 188, 170, 0.46)" : "transparent"};
3482
+ }
3483
+
3484
+ &:disabled {
3485
+ cursor: not-allowed;
3486
+ }
3487
+ }
3488
+ `;
3489
+ var StopButton = styled14(Button)`
3490
+ && {
3491
+ min-width: 24px;
3492
+ width: 24px;
3493
+ height: 24px;
3494
+ padding: 0;
3495
+ background: radial-gradient(
3496
+ circle at 30% 30%,
3497
+ rgba(255, 255, 255, 0.98) 0%,
3498
+ rgba(244, 244, 244, 0.96) 45%,
3499
+ rgba(230, 230, 230, 0.95) 100%
3500
+ );
3501
+ border-radius: 999px;
3502
+ border: 1px solid rgba(255, 255, 255, 0.72);
3503
+ box-shadow:
3504
+ inset 0 1px 0 rgba(255, 255, 255, 0.95),
3505
+ 0 8px 18px rgba(0, 0, 0, 0.18);
3506
+
3507
+ &:hover:not(:disabled) {
3508
+ background: radial-gradient(
3509
+ circle at 30% 30%,
3510
+ rgba(255, 255, 255, 1) 0%,
3511
+ rgba(249, 249, 249, 0.98) 45%,
3512
+ rgba(236, 236, 236, 0.98) 100%
3513
+ );
3514
+ }
3515
+ }
3516
+ `;
3517
+ var StopGlyph = styled14.span`
3518
+ width: 8px;
3519
+ height: 8px;
3520
+ border-radius: 2px;
3521
+ background: #1b1b1b;
3522
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.18);
3523
+ `;
3524
+
3525
+ // src/components/chat-composer/index.tsx
3526
+ import { jsx as jsx16, jsxs as jsxs10 } from "@emotion/react/jsx-runtime";
3527
+ var PlusIcon = () => /* @__PURE__ */ jsx16(
3528
+ "svg",
3529
+ {
3530
+ "aria-hidden": "true",
3531
+ width: "16",
3532
+ height: "16",
3533
+ viewBox: "0 0 16 16",
3534
+ fill: "none",
3535
+ xmlns: "http://www.w3.org/2000/svg",
3536
+ children: /* @__PURE__ */ jsx16("path", { d: "M8 3v10M3 8h10", stroke: "currentColor", strokeWidth: "1.75", strokeLinecap: "round" })
3537
+ }
3538
+ );
3539
+ var ChatComposerView = ({
3540
+ value,
3541
+ placeholder,
3542
+ attachments,
3543
+ attachmentNotice,
3544
+ attachmentLimitNotice,
3545
+ selectedModel,
3546
+ selectedMode,
3547
+ availableModels,
3548
+ isModelsLoading,
3549
+ isModelsError,
3550
+ hasModels,
3551
+ isStreaming,
3552
+ isStopping,
3553
+ enableImageAttachments,
3554
+ modeLabels,
3555
+ onValueChange,
3556
+ onPickImages,
3557
+ onPasteImages,
3558
+ onRemoveAttachment,
3559
+ onSelectedModelChange,
3560
+ onSelectedModeChange,
3561
+ onReloadModels,
3562
+ onStop,
3563
+ onSend
3564
+ }) => {
3565
+ const imageInputRef = useRef7(null);
3566
+ const canSend = canSendChatMessage({
3567
+ value,
3568
+ attachmentCount: attachments.length,
3569
+ isModelsLoading,
3570
+ isModelsError,
3571
+ hasModels
3572
+ });
3573
+ const handleKeyDown = (event) => {
3574
+ if (shouldStopChatComposer({ key: event.key, shiftKey: event.shiftKey, isStreaming })) {
3575
+ event.preventDefault();
3576
+ void onStop();
3577
+ return;
3578
+ }
3579
+ if (!shouldSubmitChatComposer({ key: event.key, shiftKey: event.shiftKey, canSend }))
3580
+ return;
3581
+ event.preventDefault();
3582
+ void onSend();
3583
+ };
3584
+ const handlePickImages = (event) => {
3585
+ if (event.target.files?.length)
3586
+ onPickImages(event.target.files);
3587
+ event.target.value = "";
3588
+ };
3589
+ const handlePaste = (event) => {
3590
+ const imageFiles = Array.from(event.clipboardData.items).filter((item) => item.kind === "file" && item.type.startsWith("image/")).map((item) => item.getAsFile()).filter((file) => Boolean(file));
3591
+ if (!imageFiles.length)
3592
+ return;
3593
+ event.preventDefault();
3594
+ onPasteImages(imageFiles);
3595
+ };
3596
+ return /* @__PURE__ */ jsx16(Container2, { children: /* @__PURE__ */ jsxs10(Surface, { "data-testid": "chat-composer-surface", children: [
3597
+ enableImageAttachments ? /* @__PURE__ */ jsx16(
3598
+ "input",
3599
+ {
3600
+ ref: imageInputRef,
3601
+ type: "file",
3602
+ accept: "image/png,image/jpeg,image/webp",
3603
+ multiple: true,
3604
+ hidden: true,
3605
+ "data-testid": "chat-composer-image-input",
3606
+ onChange: handlePickImages
3607
+ }
3608
+ ) : null,
3609
+ /* @__PURE__ */ jsx16(
3610
+ ChatComposerAttachmentList,
3611
+ {
3612
+ attachments,
3613
+ onRemoveAttachment
3614
+ }
3615
+ ),
3616
+ attachmentNotice === "limit_reached" ? /* @__PURE__ */ jsx16(AttachmentNotice, { "data-testid": "chat-composer-attachment-notice", children: attachmentLimitNotice }) : null,
3617
+ /* @__PURE__ */ jsx16(
3618
+ Input,
3619
+ {
3620
+ "data-testid": "chat-composer-input",
3621
+ value,
3622
+ onChange: (event) => onValueChange(event.target.value),
3623
+ onKeyDown: handleKeyDown,
3624
+ onPaste: enableImageAttachments ? handlePaste : void 0,
3625
+ placeholder
3626
+ }
3627
+ ),
3628
+ /* @__PURE__ */ jsx16(Footer, { children: /* @__PURE__ */ jsxs10(Actions2, { "data-testid": "chat-composer-actions", children: [
3629
+ enableImageAttachments ? /* @__PURE__ */ jsx16(
3630
+ AttachButton,
3631
+ {
3632
+ type: "button",
3633
+ "data-testid": "chat-composer-attach-image",
3634
+ "aria-label": "Attach image",
3635
+ onClick: () => imageInputRef.current?.click(),
3636
+ children: /* @__PURE__ */ jsx16(PlusIcon, {})
3637
+ }
3638
+ ) : null,
3639
+ /* @__PURE__ */ jsx16(
3640
+ ChatModeControl,
3641
+ {
3642
+ value: selectedMode,
3643
+ disabled: isStreaming,
3644
+ labels: modeLabels,
3645
+ onChange: onSelectedModeChange
3646
+ }
3647
+ ),
3648
+ /* @__PURE__ */ jsx16(
3649
+ ChatModelControl,
3650
+ {
3651
+ selectedModel,
3652
+ availableModels,
3653
+ isModelsLoading,
3654
+ isModelsError,
3655
+ hasModels,
3656
+ onSelectedModelChange,
3657
+ onReloadModels
3658
+ }
3659
+ ),
3660
+ /* @__PURE__ */ jsx16(
3661
+ ChatSendActions,
3662
+ {
3663
+ canSend,
3664
+ isStreaming,
3665
+ isStopping,
3666
+ onStop,
3667
+ onSend
3668
+ }
3669
+ )
3670
+ ] }) })
3671
+ ] }) });
3672
+ };
3673
+ var ChatComposer = () => {
3674
+ const { labels, sendRef, retryRef, enableImageAttachments } = useChatContext();
3675
+ const { state, actions } = useChatComposer();
3676
+ const { send, retry } = actions;
3677
+ useEffect5(() => {
3678
+ sendRef.current = send;
3679
+ retryRef.current = async () => {
3680
+ retry();
3681
+ };
3682
+ }, [retry, retryRef, send, sendRef]);
3683
+ const modeLabels = {
3684
+ ask: labels.modeLabelAsk,
3685
+ plan: labels.modeLabelPlan,
3686
+ agent: labels.modeLabelAgent
3687
+ };
3688
+ return /* @__PURE__ */ jsx16(
3689
+ ChatComposerView,
3690
+ {
3691
+ value: state.value,
3692
+ attachments: state.attachments,
3693
+ attachmentNotice: state.attachmentNotice,
3694
+ attachmentLimitNotice: labels.attachmentLimitNotice,
3695
+ placeholder: labels.placeholder,
3696
+ selectedModel: state.selectedModel,
3697
+ selectedMode: state.selectedMode,
3698
+ availableModels: state.availableModels,
3699
+ isModelsLoading: state.isModelsLoading,
3700
+ isModelsError: state.isModelsError,
3701
+ hasModels: state.hasModels,
3702
+ isStreaming: state.isStreaming,
3703
+ isStopping: state.isStopping,
3704
+ enableImageAttachments,
3705
+ modeLabels,
3706
+ onValueChange: actions.setValue,
3707
+ onPickImages: actions.pickImages,
3708
+ onPasteImages: actions.pasteImages,
3709
+ onRemoveAttachment: actions.removeAttachment,
3710
+ onSelectedModelChange: actions.setSelectedModel,
3711
+ onSelectedModeChange: actions.setSelectedMode,
3712
+ onReloadModels: actions.reloadModels,
3713
+ onStop: actions.stop,
3714
+ onSend: send
3715
+ }
3716
+ );
3717
+ };
3718
+ var Container2 = styled15.div`
3719
+ padding: 0 16px 16px;
3720
+ `;
3721
+ var Surface = styled15.div`
3722
+ background: var(--border-color);
3723
+ border-radius: 20px;
3724
+ border: 1px solid var(--border-hover);
3725
+ box-shadow:
3726
+ inset 0 1px 0 rgba(255, 255, 255, 0.04),
3727
+ 0 12px 36px rgba(0, 0, 0, 0.3);
3728
+ backdrop-filter: blur(10px);
3729
+ `;
3730
+ var AttachmentNotice = styled15.div`
3731
+ margin: 10px 12px 0;
3732
+ padding: 8px 10px;
3733
+ border-radius: 10px;
3734
+ background: rgba(255, 196, 87, 0.12);
3735
+ border: 1px solid rgba(255, 196, 87, 0.28);
3736
+ color: rgba(255, 220, 148, 0.95);
3737
+ font-size: 12px;
3738
+ line-height: 1.4;
3739
+ `;
3740
+ var Input = styled15.textarea`
3741
+ width: 100%;
3742
+ min-height: 96px;
3743
+ resize: none;
3744
+ appearance: none;
3745
+ border: 0;
3746
+ outline: 0;
3747
+ background: transparent;
3748
+ padding: 8px 12px 12px 12px;
3749
+ font-weight: 400;
3750
+ font-size: 14px;
3751
+ color: var(--text-primary);
3752
+ line-height: 20px;
3753
+
3754
+ &::placeholder {
3755
+ color: var(--text-secondary);
3756
+ }
3757
+
3758
+ &::-webkit-resizer {
3759
+ display: none;
3760
+ }
3761
+ `;
3762
+ var Footer = styled15.div`
3763
+ display: flex;
3764
+ align-items: flex-end;
3765
+ justify-content: stretch;
3766
+ gap: 16px;
3767
+ padding: 0 14px 14px;
3768
+ `;
3769
+ var Actions2 = styled15.div`
3770
+ display: flex;
3771
+ align-items: center;
3772
+ flex-wrap: wrap;
3773
+ width: 100%;
3774
+ min-width: 0;
3775
+ justify-content: flex-end;
3776
+ gap: 8px;
3777
+ `;
3778
+ var AttachButton = styled15.button`
3779
+ width: 28px;
3780
+ height: 28px;
3781
+ display: grid;
3782
+ place-items: center;
3783
+ border: none;
3784
+ border-radius: 999px;
3785
+ background: transparent;
3786
+ color: rgba(255, 255, 255, 0.82);
3787
+ cursor: pointer;
3788
+
3789
+ &:hover {
3790
+ background: rgba(255, 255, 255, 0.08);
3791
+ }
3792
+ `;
3793
+
3794
+ // src/components/chat-conversation-list/index.tsx
3795
+ import styled17 from "@emotion/styled";
3796
+
3797
+ // src/components/chat-conversation-list/components/chat-session-item.tsx
3798
+ import { memo as memo3 } from "react";
3799
+ import styled16 from "@emotion/styled";
3800
+ import { jsx as jsx17, jsxs as jsxs11 } from "@emotion/react/jsx-runtime";
3801
+ var ChatSessionItem = memo3(
3802
+ ({ session, isActive, modeLabel, onClick }) => {
3803
+ return /* @__PURE__ */ jsx17(
3804
+ SessionButton,
3805
+ {
3806
+ type: "button",
3807
+ "data-active": isActive,
3808
+ onClick: () => onClick(session.sessionId),
3809
+ children: /* @__PURE__ */ jsxs11(SessionMeta, { children: [
3810
+ /* @__PURE__ */ jsx17(SessionTitle, { children: session.title }),
3811
+ /* @__PURE__ */ jsx17(ModeBadge, { children: modeLabel })
3812
+ ] })
3813
+ }
3814
+ );
3815
+ }
3816
+ );
3817
+ ChatSessionItem.displayName = "ChatSessionItem";
3818
+ var SessionMeta = styled16.div`
3819
+ display: flex;
3820
+ align-items: center;
3821
+ justify-content: space-between;
3822
+ gap: 8px;
3823
+ `;
3824
+ var SessionTitle = styled16.span`
3825
+ min-width: 0;
3826
+ overflow: hidden;
3827
+ text-overflow: ellipsis;
3828
+ white-space: nowrap;
3829
+ `;
3830
+ var SessionButton = styled16.button`
3831
+ border: 1px solid transparent;
3832
+ border-radius: 12px;
3833
+ padding: 12px;
3834
+ text-align: left;
3835
+ color: var(--text-primary);
3836
+ background: rgba(255, 255, 255, 0.03);
3837
+ cursor: pointer;
3838
+
3839
+ &[data-active='true'] {
3840
+ border-color: rgba(255, 255, 255, 0.2);
3841
+ background: rgba(255, 255, 255, 0.08);
3842
+ }
3843
+ `;
3844
+ var ModeBadge = styled16.span`
3845
+ flex-shrink: 0;
3846
+ border-radius: 999px;
3847
+ border: 1px solid rgba(255, 255, 255, 0.1);
3848
+ padding: 4px 10px;
3849
+ font-size: 11px;
3850
+ line-height: 1;
3851
+ color: var(--text-secondary);
3852
+ background: rgba(255, 255, 255, 0.04);
3853
+ `;
3854
+
3855
+ // src/components/chat-conversation-list/index.tsx
3856
+ import { jsx as jsx18, jsxs as jsxs12 } from "@emotion/react/jsx-runtime";
3857
+ var ChatConversationList = () => {
3858
+ const { labels } = useChatContext();
3859
+ const sessions = useChatStore((s) => s.sessions);
3860
+ const activeSessionId = useChatStore((s) => s.activeSessionId);
3861
+ const preferredMode = useChatStore((s) => s.preferredMode);
3862
+ const createSession = useChatStore((s) => s.createSession);
3863
+ const setActiveSession = useChatStore((s) => s.setActiveSession);
3864
+ const modeLabels = {
3865
+ ask: labels.modeLabelAsk,
3866
+ plan: labels.modeLabelPlan,
3867
+ agent: labels.modeLabelAgent
3868
+ };
3869
+ const handleCreateSession = () => {
3870
+ const session = createDraftChatSession({
3871
+ // Model is intentionally deferred: ChatComposer resolves selectedModel at send time.
3872
+ model: "",
3873
+ mode: preferredMode,
3874
+ nowIso: () => (/* @__PURE__ */ new Date()).toISOString(),
3875
+ createSessionId: createDraftChatSessionId
3876
+ });
3877
+ createSession(session);
3878
+ };
3879
+ return /* @__PURE__ */ jsxs12(Container3, { children: [
3880
+ /* @__PURE__ */ jsxs12(Toolbar, { children: [
3881
+ /* @__PURE__ */ jsx18(Title4, { children: "Sessions" }),
3882
+ /* @__PURE__ */ jsx18(CreateButton, { type: "button", "data-testid": "chat-create-session", onClick: handleCreateSession, children: labels.newChat })
3883
+ ] }),
3884
+ /* @__PURE__ */ jsx18(List2, { "data-testid": "chat-session-list", children: sessions.map((session) => /* @__PURE__ */ jsx18(
3885
+ ChatSessionItem,
3886
+ {
3887
+ session,
3888
+ isActive: activeSessionId === session.sessionId,
3889
+ modeLabel: modeLabels[session.mode ?? DEFAULT_CHAT_AGENT_MODE] ?? "",
3890
+ onClick: setActiveSession
3891
+ },
3892
+ session.sessionId
3893
+ )) })
3894
+ ] });
3895
+ };
3896
+ var Container3 = styled17.aside`
3897
+ width: 280px;
3898
+ min-width: 280px;
3899
+ border-right: 1px solid var(--border-default, rgba(255, 255, 255, 0.08));
3900
+ display: flex;
3901
+ flex-direction: column;
3902
+ background: rgba(255, 255, 255, 0.02);
3903
+ `;
3904
+ var Toolbar = styled17.div`
3905
+ padding: 20px 16px 12px;
3906
+ display: flex;
3907
+ flex-direction: column;
3908
+ gap: 12px;
3909
+ `;
3910
+ var Title4 = styled17.h2`
3911
+ margin: 0;
3912
+ font-size: 14px;
3913
+ color: var(--text-secondary);
3914
+ `;
3915
+ var CreateButton = styled17.button`
3916
+ border: none;
3917
+ border-radius: 12px;
3918
+ padding: 12px 14px;
3919
+ background: var(--text-primary);
3920
+ color: var(--bg-primary);
3921
+ text-align: left;
3922
+ cursor: pointer;
3923
+ `;
3924
+ var List2 = styled17.div`
3925
+ padding: 0 12px 16px;
3926
+ display: flex;
3927
+ flex-direction: column;
3928
+ gap: 8px;
3929
+ overflow: auto;
3930
+ `;
3931
+
3932
+ // src/components/ai-chat/index.tsx
3933
+ import { jsx as jsx19, jsxs as jsxs13 } from "@emotion/react/jsx-runtime";
3934
+ var AiChat = ({ showConversationList = false, ...providerProps }) => /* @__PURE__ */ jsx19(
3935
+ ConfigProvider,
3936
+ {
3937
+ theme: {
3938
+ token: {
3939
+ spacing: {
3940
+ lg: 16
3941
+ },
3942
+ colors: {
3943
+ primary: "#1f52f0",
3944
+ background: "#1c1c1c",
3945
+ text: "#fcfbf8",
3946
+ textSecondary: "#c5c1ba"
3947
+ },
3948
+ components: {
3949
+ select: {
3950
+ optionColor: "#fcfbf8",
3951
+ optionSelectedBg: "transparent",
3952
+ optionHoverBg: "#41413f",
3953
+ optionSelectedColor: "#fcfbf8",
3954
+ backgroundColor: "#1c1c1c",
3955
+ dropdownBg: "#1c1c1c",
3956
+ placeholderColor: "#c5c1ba",
3957
+ borderRadius: "12px",
3958
+ dropdownPadding: "4px"
3959
+ },
3960
+ modal: { contentBg: "#1c1c1c", padding: "24px" },
3961
+ dropdown: {
3962
+ backgroundColor: "#1c1c1c",
3963
+ borderRadius: "12px",
3964
+ padding: "4px"
3965
+ }
3966
+ }
3967
+ }
3968
+ },
3969
+ children: /* @__PURE__ */ jsx19(AiChatProvider, { ...providerProps, children: /* @__PURE__ */ jsxs13(Root, { "data-testid": "ai-chat", children: [
3970
+ showConversationList ? /* @__PURE__ */ jsx19(ChatConversationList, {}) : null,
3971
+ /* @__PURE__ */ jsxs13(Workspace, { children: [
3972
+ /* @__PURE__ */ jsx19(ChatThread, {}),
3973
+ /* @__PURE__ */ jsx19(ChatComposer, {})
3974
+ ] })
3975
+ ] }) })
3976
+ }
3977
+ );
3978
+ var Root = styled18.div`
3979
+ display: flex;
3980
+ width: 100%;
3981
+ height: 100%;
3982
+ min-height: 0;
3983
+ overflow: hidden;
3984
+ `;
3985
+ var Workspace = styled18.section`
3986
+ flex: 1;
3987
+ display: flex;
3988
+ flex-direction: column;
3989
+ min-height: 0;
3990
+ overflow: hidden;
3991
+ `;
3992
+ export {
3993
+ AiChat,
3994
+ AiChatProvider,
3995
+ CHAT_AGENT_MODES,
3996
+ ChatComposer,
3997
+ ChatConversationList,
3998
+ ChatThread,
3999
+ DEFAULT_AI_CHAT_LABELS,
4000
+ DEFAULT_CHAT_AGENT_MODE,
4001
+ createDefaultChatTransport,
4002
+ useChatContext,
4003
+ useChatStore
4004
+ };