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