@surf-kit/agent 0.2.2 → 0.4.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.
Files changed (51) hide show
  1. package/dist/chat/index.cjs +733 -222
  2. package/dist/chat/index.cjs.map +1 -1
  3. package/dist/chat/index.d.cts +13 -7
  4. package/dist/chat/index.d.ts +13 -7
  5. package/dist/chat/index.js +715 -204
  6. package/dist/chat/index.js.map +1 -1
  7. package/dist/{chat-ChYl2XjV.d.cts → chat-BRY3xGg_.d.cts} +11 -2
  8. package/dist/{chat--OifhIRe.d.ts → chat-CcKc6OAR.d.ts} +11 -2
  9. package/dist/{hooks-BGs8-4GK.d.ts → hooks-BLeiVk-x.d.ts} +22 -5
  10. package/dist/{hooks-DLfF18IU.d.cts → hooks-CSGGLd7j.d.cts} +22 -5
  11. package/dist/hooks.cjs +160 -85
  12. package/dist/hooks.cjs.map +1 -1
  13. package/dist/hooks.d.cts +3 -3
  14. package/dist/hooks.d.ts +3 -3
  15. package/dist/hooks.js +160 -85
  16. package/dist/hooks.js.map +1 -1
  17. package/dist/index.cjs +794 -283
  18. package/dist/index.cjs.map +1 -1
  19. package/dist/index.d.cts +3 -3
  20. package/dist/index.d.ts +3 -3
  21. package/dist/index.js +758 -247
  22. package/dist/index.js.map +1 -1
  23. package/dist/layouts/index.cjs +754 -243
  24. package/dist/layouts/index.cjs.map +1 -1
  25. package/dist/layouts/index.d.cts +1 -1
  26. package/dist/layouts/index.d.ts +1 -1
  27. package/dist/layouts/index.js +733 -222
  28. package/dist/layouts/index.js.map +1 -1
  29. package/dist/mcp/index.cjs +1 -1
  30. package/dist/mcp/index.cjs.map +1 -1
  31. package/dist/mcp/index.js +2 -2
  32. package/dist/mcp/index.js.map +1 -1
  33. package/dist/response/index.cjs +100 -15
  34. package/dist/response/index.cjs.map +1 -1
  35. package/dist/response/index.d.cts +2 -2
  36. package/dist/response/index.d.ts +2 -2
  37. package/dist/response/index.js +99 -14
  38. package/dist/response/index.js.map +1 -1
  39. package/dist/sources/index.cjs +30 -1
  40. package/dist/sources/index.cjs.map +1 -1
  41. package/dist/sources/index.js +30 -1
  42. package/dist/sources/index.js.map +1 -1
  43. package/dist/streaming/index.cjs +213 -93
  44. package/dist/streaming/index.cjs.map +1 -1
  45. package/dist/streaming/index.d.cts +4 -3
  46. package/dist/streaming/index.d.ts +4 -3
  47. package/dist/streaming/index.js +183 -73
  48. package/dist/streaming/index.js.map +1 -1
  49. package/dist/{streaming-DfT22A0z.d.cts → streaming-BHPXnwwo.d.cts} +3 -1
  50. package/dist/{streaming-DbQxScpi.d.ts → streaming-C6mbU7My.d.ts} +3 -1
  51. package/package.json +17 -5
@@ -1,11 +1,11 @@
1
1
  'use client';
2
2
 
3
3
  // src/layouts/AgentFullPage/AgentFullPage.tsx
4
- import { twMerge as twMerge10 } from "tailwind-merge";
5
- import { useState as useState4, useCallback as useCallback3 } from "react";
4
+ import { twMerge as twMerge11 } from "tailwind-merge";
5
+ import { useState as useState4, useCallback as useCallback4 } from "react";
6
6
 
7
7
  // src/chat/AgentChat/AgentChat.tsx
8
- import { twMerge as twMerge8 } from "tailwind-merge";
8
+ import { twMerge as twMerge9 } from "tailwind-merge";
9
9
 
10
10
  // src/hooks/useAgentChat.ts
11
11
  import { useReducer, useCallback, useRef } from "react";
@@ -16,7 +16,8 @@ var initialState = {
16
16
  error: null,
17
17
  inputValue: "",
18
18
  streamPhase: "idle",
19
- streamingContent: ""
19
+ streamingContent: "",
20
+ streamingAgent: null
20
21
  };
21
22
  function reducer(state, action) {
22
23
  switch (action.type) {
@@ -30,12 +31,17 @@ function reducer(state, action) {
30
31
  error: null,
31
32
  inputValue: "",
32
33
  streamPhase: "thinking",
33
- streamingContent: ""
34
+ streamingContent: "",
35
+ streamingAgent: null
34
36
  };
35
37
  case "STREAM_PHASE":
36
38
  return { ...state, streamPhase: action.phase };
37
39
  case "STREAM_CONTENT":
38
40
  return { ...state, streamingContent: state.streamingContent + action.content };
41
+ case "STREAM_CONTENT_RESET":
42
+ return { ...state, streamingContent: "" };
43
+ case "STREAM_AGENT":
44
+ return { ...state, streamingAgent: action.agent };
39
45
  case "SEND_SUCCESS":
40
46
  return {
41
47
  ...state,
@@ -51,7 +57,8 @@ function reducer(state, action) {
51
57
  isLoading: false,
52
58
  error: action.error,
53
59
  streamPhase: "idle",
54
- streamingContent: ""
60
+ streamingContent: "",
61
+ streamingAgent: null
55
62
  };
56
63
  case "LOAD_CONVERSATION":
57
64
  return {
@@ -77,115 +84,172 @@ function useAgentChat(config) {
77
84
  const configRef = useRef(config);
78
85
  configRef.current = config;
79
86
  const lastUserMessageRef = useRef(null);
87
+ const lastUserAttachmentsRef = useRef(void 0);
88
+ const abortControllerRef = useRef(null);
80
89
  const sendMessage = useCallback(
81
- async (content) => {
82
- const { apiUrl, streamPath = "/chat/stream", headers = {}, timeout = 3e4 } = configRef.current;
90
+ async (content, attachments) => {
91
+ const { apiUrl, streamPath = "/chat/stream", headers: headersOrFn, timeout = 3e4, bodyExtra } = configRef.current;
92
+ const headers = typeof headersOrFn === "function" ? await headersOrFn() : headersOrFn ?? {};
83
93
  lastUserMessageRef.current = content;
94
+ lastUserAttachmentsRef.current = attachments;
84
95
  const userMessage = {
85
96
  id: generateMessageId(),
86
97
  role: "user",
87
98
  content,
99
+ attachments,
88
100
  timestamp: /* @__PURE__ */ new Date()
89
101
  };
90
102
  dispatch({ type: "SEND_START", message: userMessage });
91
103
  const controller = new AbortController();
104
+ abortControllerRef.current = controller;
92
105
  const timeoutId = setTimeout(() => controller.abort(), timeout);
106
+ const ctx = {
107
+ accumulatedContent: "",
108
+ agentResponse: null,
109
+ capturedAgent: null,
110
+ capturedConversationId: null,
111
+ hadStreamError: false
112
+ };
93
113
  try {
94
- const response = await fetch(`${apiUrl}${streamPath}`, {
95
- method: "POST",
96
- headers: {
97
- "Content-Type": "application/json",
98
- Accept: "text/event-stream",
99
- ...headers
100
- },
101
- body: JSON.stringify({
102
- message: content,
103
- conversation_id: state.conversationId
104
- }),
105
- signal: controller.signal
106
- });
107
- clearTimeout(timeoutId);
108
- if (!response.ok) {
109
- dispatch({
110
- type: "SEND_ERROR",
111
- error: {
112
- code: "API_ERROR",
113
- message: `HTTP ${response.status}: ${response.statusText}`,
114
- retryable: response.status >= 500
115
- }
116
- });
117
- return;
114
+ const url = `${apiUrl}${streamPath}`;
115
+ const mergedHeaders = {
116
+ "Content-Type": "application/json",
117
+ Accept: "text/event-stream",
118
+ ...headers
119
+ };
120
+ const requestBody = {
121
+ message: content,
122
+ conversation_id: state.conversationId,
123
+ ...bodyExtra
124
+ };
125
+ if (attachments && attachments.length > 0) {
126
+ requestBody.attachments = attachments.map((a) => ({
127
+ filename: a.filename,
128
+ content_type: a.content_type,
129
+ data: a.data
130
+ }));
118
131
  }
119
- const reader = response.body?.getReader();
120
- if (!reader) {
121
- dispatch({
122
- type: "SEND_ERROR",
123
- error: { code: "STREAM_ERROR", message: "No response body", retryable: true }
132
+ const body = JSON.stringify(requestBody);
133
+ const handleEvent = (event) => {
134
+ switch (event.type) {
135
+ case "agent":
136
+ ctx.capturedAgent = event.agent;
137
+ dispatch({ type: "STREAM_AGENT", agent: ctx.capturedAgent });
138
+ break;
139
+ case "phase":
140
+ dispatch({ type: "STREAM_PHASE", phase: event.phase });
141
+ break;
142
+ case "delta":
143
+ ctx.accumulatedContent += event.content;
144
+ dispatch({ type: "STREAM_CONTENT", content: event.content });
145
+ break;
146
+ case "delta_reset":
147
+ ctx.accumulatedContent = "";
148
+ dispatch({ type: "STREAM_CONTENT_RESET" });
149
+ break;
150
+ case "done":
151
+ ctx.agentResponse = event.response;
152
+ ctx.capturedConversationId = event.conversation_id ?? null;
153
+ break;
154
+ case "error":
155
+ ctx.hadStreamError = true;
156
+ dispatch({ type: "SEND_ERROR", error: event.error });
157
+ break;
158
+ }
159
+ };
160
+ const { streamAdapter } = configRef.current;
161
+ if (streamAdapter) {
162
+ await streamAdapter(
163
+ url,
164
+ { method: "POST", headers: mergedHeaders, body, signal: controller.signal },
165
+ handleEvent
166
+ );
167
+ clearTimeout(timeoutId);
168
+ } else {
169
+ const response = await fetch(url, {
170
+ method: "POST",
171
+ headers: mergedHeaders,
172
+ body,
173
+ signal: controller.signal
124
174
  });
125
- return;
126
- }
127
- const decoder = new TextDecoder();
128
- let buffer = "";
129
- let accumulatedContent = "";
130
- let agentResponse = null;
131
- let capturedAgent = null;
132
- let capturedConversationId = null;
133
- while (true) {
134
- const { done, value } = await reader.read();
135
- if (done) break;
136
- buffer += decoder.decode(value, { stream: true });
137
- const lines = buffer.split("\n");
138
- buffer = lines.pop() ?? "";
139
- for (const line of lines) {
140
- if (!line.startsWith("data: ")) continue;
141
- const data = line.slice(6).trim();
142
- if (data === "[DONE]") continue;
143
- try {
144
- const event = JSON.parse(data);
145
- switch (event.type) {
146
- case "agent":
147
- capturedAgent = event.agent;
148
- break;
149
- case "phase":
150
- dispatch({ type: "STREAM_PHASE", phase: event.phase });
151
- break;
152
- case "delta":
153
- accumulatedContent += event.content;
154
- dispatch({ type: "STREAM_CONTENT", content: event.content });
155
- break;
156
- case "done":
157
- agentResponse = event.response;
158
- capturedConversationId = event.conversation_id ?? null;
159
- break;
160
- case "error":
161
- dispatch({ type: "SEND_ERROR", error: event.error });
162
- return;
175
+ clearTimeout(timeoutId);
176
+ if (!response.ok) {
177
+ dispatch({
178
+ type: "SEND_ERROR",
179
+ error: {
180
+ code: "API_ERROR",
181
+ message: `HTTP ${response.status}: ${response.statusText}`,
182
+ retryable: response.status >= 500
183
+ }
184
+ });
185
+ return;
186
+ }
187
+ const reader = response.body?.getReader();
188
+ if (!reader) {
189
+ dispatch({
190
+ type: "SEND_ERROR",
191
+ error: { code: "STREAM_ERROR", message: "No response body", retryable: true }
192
+ });
193
+ return;
194
+ }
195
+ const decoder = new TextDecoder();
196
+ let buffer = "";
197
+ while (true) {
198
+ const { done, value } = await reader.read();
199
+ if (done) break;
200
+ buffer += decoder.decode(value, { stream: true });
201
+ const lines = buffer.split("\n");
202
+ buffer = lines.pop() ?? "";
203
+ for (const line of lines) {
204
+ if (!line.startsWith("data: ")) continue;
205
+ const data = line.slice(6).trim();
206
+ if (data === "[DONE]") continue;
207
+ try {
208
+ const event = JSON.parse(data);
209
+ handleEvent(event);
210
+ } catch {
163
211
  }
164
- } catch {
165
212
  }
166
213
  }
167
214
  }
215
+ if (ctx.hadStreamError) return;
168
216
  const assistantMessage = {
169
217
  id: generateMessageId(),
170
218
  role: "assistant",
171
- content: agentResponse?.message ?? accumulatedContent,
172
- response: agentResponse ?? void 0,
173
- agent: capturedAgent ?? void 0,
219
+ content: ctx.agentResponse?.message ?? ctx.accumulatedContent,
220
+ response: ctx.agentResponse ?? void 0,
221
+ agent: ctx.capturedAgent ?? void 0,
174
222
  timestamp: /* @__PURE__ */ new Date()
175
223
  };
176
224
  dispatch({
177
225
  type: "SEND_SUCCESS",
178
226
  message: assistantMessage,
179
- streamingContent: accumulatedContent,
180
- conversationId: capturedConversationId
227
+ streamingContent: ctx.accumulatedContent,
228
+ conversationId: ctx.capturedConversationId
181
229
  });
182
230
  } catch (err) {
183
231
  clearTimeout(timeoutId);
184
232
  if (err.name === "AbortError") {
185
- dispatch({
186
- type: "SEND_ERROR",
187
- error: { code: "TIMEOUT", message: "Request timed out", retryable: true }
188
- });
233
+ if (ctx.accumulatedContent) {
234
+ const partialMessage = {
235
+ id: generateMessageId(),
236
+ role: "assistant",
237
+ content: ctx.accumulatedContent,
238
+ agent: ctx.capturedAgent ?? void 0,
239
+ timestamp: /* @__PURE__ */ new Date()
240
+ };
241
+ dispatch({
242
+ type: "SEND_SUCCESS",
243
+ message: partialMessage,
244
+ streamingContent: ctx.accumulatedContent,
245
+ conversationId: ctx.capturedConversationId
246
+ });
247
+ } else {
248
+ dispatch({
249
+ type: "SEND_ERROR",
250
+ error: { code: "ABORTED", message: "Request stopped", retryable: true }
251
+ });
252
+ }
189
253
  } else {
190
254
  dispatch({
191
255
  type: "SEND_ERROR",
@@ -196,6 +260,8 @@ function useAgentChat(config) {
196
260
  }
197
261
  });
198
262
  }
263
+ } finally {
264
+ abortControllerRef.current = null;
199
265
  }
200
266
  },
201
267
  [state.conversationId]
@@ -208,7 +274,8 @@ function useAgentChat(config) {
208
274
  }, []);
209
275
  const submitFeedback = useCallback(
210
276
  async (messageId, rating, comment) => {
211
- const { apiUrl, feedbackPath = "/feedback", headers = {} } = configRef.current;
277
+ const { apiUrl, feedbackPath = "/feedback", headers: headersOrFn } = configRef.current;
278
+ const headers = typeof headersOrFn === "function" ? await headersOrFn() : headersOrFn ?? {};
212
279
  await fetch(`${apiUrl}${feedbackPath}`, {
213
280
  method: "POST",
214
281
  headers: { "Content-Type": "application/json", ...headers },
@@ -219,12 +286,16 @@ function useAgentChat(config) {
219
286
  );
220
287
  const retry = useCallback(async () => {
221
288
  if (lastUserMessageRef.current) {
222
- await sendMessage(lastUserMessageRef.current);
289
+ await sendMessage(lastUserMessageRef.current, lastUserAttachmentsRef.current);
223
290
  }
224
291
  }, [sendMessage]);
292
+ const stop = useCallback(() => {
293
+ abortControllerRef.current?.abort();
294
+ }, []);
225
295
  const reset = useCallback(() => {
226
296
  dispatch({ type: "RESET" });
227
297
  lastUserMessageRef.current = null;
298
+ lastUserAttachmentsRef.current = void 0;
228
299
  }, []);
229
300
  const actions = {
230
301
  sendMessage,
@@ -232,6 +303,7 @@ function useAgentChat(config) {
232
303
  loadConversation,
233
304
  submitFeedback,
234
305
  retry,
306
+ stop,
235
307
  reset
236
308
  };
237
309
  return { state, actions };
@@ -239,7 +311,7 @@ function useAgentChat(config) {
239
311
 
240
312
  // src/chat/MessageThread/MessageThread.tsx
241
313
  import { twMerge as twMerge5 } from "tailwind-merge";
242
- import { useEffect, useRef as useRef2 } from "react";
314
+ import { useCallback as useCallback2, useEffect, useRef as useRef2 } from "react";
243
315
 
244
316
  // src/chat/MessageBubble/MessageBubble.tsx
245
317
  import { twMerge as twMerge4 } from "tailwind-merge";
@@ -248,8 +320,10 @@ import { twMerge as twMerge4 } from "tailwind-merge";
248
320
  import { Badge as Badge2 } from "@surf-kit/core";
249
321
 
250
322
  // src/response/ResponseMessage/ResponseMessage.tsx
323
+ import React from "react";
251
324
  import ReactMarkdown from "react-markdown";
252
325
  import rehypeSanitize from "rehype-sanitize";
326
+ import remarkGfm from "remark-gfm";
253
327
  import { twMerge } from "tailwind-merge";
254
328
  import { jsx } from "react/jsx-runtime";
255
329
  function normalizeMarkdownLists(content) {
@@ -272,7 +346,12 @@ function ResponseMessage({ content, className }) {
272
346
  "[&_h3]:text-sm [&_h3]:font-semibold [&_h3]:text-accent [&_h3]:mt-2 [&_h3]:mb-1",
273
347
  "[&_code]:bg-surface-raised [&_code]:text-accent [&_code]:px-1.5 [&_code]:py-0.5 [&_code]:rounded [&_code]:text-xs [&_code]:font-mono",
274
348
  "[&_pre]:bg-surface-raised [&_pre]:border [&_pre]:border-border [&_pre]:rounded-xl [&_pre]:p-4 [&_pre]:overflow-x-auto",
349
+ "[&_hr]:my-3 [&_hr]:border-border",
275
350
  "[&_blockquote]:border-l-2 [&_blockquote]:border-border-strong [&_blockquote]:pl-4 [&_blockquote]:text-text-secondary",
351
+ "[&_table]:w-full [&_table]:text-sm [&_table]:border-collapse [&_table]:my-2",
352
+ "[&_thead]:border-b [&_thead]:border-border",
353
+ "[&_th]:text-left [&_th]:px-2 [&_th]:py-1.5 [&_th]:font-semibold",
354
+ "[&_td]:px-2 [&_td]:py-1.5 [&_td]:border-t [&_td]:border-border/50",
276
355
  "[&_a]:text-accent [&_a]:underline-offset-2 [&_a]:hover:text-accent/80",
277
356
  className
278
357
  ),
@@ -280,6 +359,7 @@ function ResponseMessage({ content, className }) {
280
359
  children: /* @__PURE__ */ jsx(
281
360
  ReactMarkdown,
282
361
  {
362
+ remarkPlugins: [remarkGfm],
283
363
  rehypePlugins: [rehypeSanitize],
284
364
  components: {
285
365
  script: () => null,
@@ -287,12 +367,29 @@ function ResponseMessage({ content, className }) {
287
367
  p: ({ children }) => /* @__PURE__ */ jsx("p", { className: "my-2", children }),
288
368
  ul: ({ children }) => /* @__PURE__ */ jsx("ul", { className: "my-2 list-disc pl-6", children }),
289
369
  ol: ({ children }) => /* @__PURE__ */ jsx("ol", { className: "my-2 list-decimal pl-6", children }),
290
- li: ({ children }) => /* @__PURE__ */ jsx("li", { className: "my-1", children }),
370
+ li: ({ children, ...props }) => {
371
+ let content2 = children;
372
+ if (props.ordered) {
373
+ content2 = React.Children.map(children, (child, i) => {
374
+ if (i === 0 && typeof child === "string") {
375
+ return child.replace(/^\d+[.)]\s*/, "");
376
+ }
377
+ return child;
378
+ });
379
+ }
380
+ return /* @__PURE__ */ jsx("li", { className: "my-1", children: content2 });
381
+ },
291
382
  strong: ({ children }) => /* @__PURE__ */ jsx("strong", { className: "font-semibold", children }),
383
+ em: ({ children }) => /* @__PURE__ */ jsx("em", { className: "italic text-text-secondary", children }),
292
384
  h1: ({ children }) => /* @__PURE__ */ jsx("h1", { className: "text-base font-bold mt-4 mb-2", children }),
293
385
  h2: ({ children }) => /* @__PURE__ */ jsx("h2", { className: "text-sm font-bold mt-3 mb-1", children }),
294
386
  h3: ({ children }) => /* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold mt-2 mb-1", children }),
295
- code: ({ children }) => /* @__PURE__ */ jsx("code", { className: "bg-surface-sunken rounded px-1 py-0.5 text-xs font-mono", children })
387
+ hr: () => /* @__PURE__ */ jsx("hr", { className: "my-3 border-border" }),
388
+ code: ({ children }) => /* @__PURE__ */ jsx("code", { className: "bg-surface-sunken rounded px-1 py-0.5 text-xs font-mono", children }),
389
+ table: ({ children }) => /* @__PURE__ */ jsx("div", { className: "overflow-x-auto my-2", children: /* @__PURE__ */ jsx("table", { className: "w-full text-sm border-collapse", children }) }),
390
+ thead: ({ children }) => /* @__PURE__ */ jsx("thead", { className: "border-b border-border", children }),
391
+ th: ({ children }) => /* @__PURE__ */ jsx("th", { className: "text-left px-2 py-1.5 font-semibold", children }),
392
+ td: ({ children }) => /* @__PURE__ */ jsx("td", { className: "px-2 py-1.5 border-t border-border/50", children })
296
393
  },
297
394
  children: normalizeMarkdownLists(content)
298
395
  }
@@ -302,7 +399,9 @@ function ResponseMessage({ content, className }) {
302
399
  }
303
400
 
304
401
  // src/response/StructuredResponse/StructuredResponse.tsx
305
- import { jsx as jsx2, jsxs } from "react/jsx-runtime";
402
+ import ReactMarkdown2 from "react-markdown";
403
+ import rehypeSanitize2 from "rehype-sanitize";
404
+ import { Fragment, jsx as jsx2, jsxs } from "react/jsx-runtime";
306
405
  function tryParse(value) {
307
406
  if (value === void 0 || value === null) return null;
308
407
  if (typeof value === "string") {
@@ -314,6 +413,25 @@ function tryParse(value) {
314
413
  }
315
414
  return value;
316
415
  }
416
+ function InlineMarkdown({ text }) {
417
+ return /* @__PURE__ */ jsx2(
418
+ ReactMarkdown2,
419
+ {
420
+ rehypePlugins: [rehypeSanitize2],
421
+ components: {
422
+ // Unwrap block-level <p> so content stays inline within its parent
423
+ p: ({ children }) => /* @__PURE__ */ jsx2(Fragment, { children }),
424
+ strong: ({ children }) => /* @__PURE__ */ jsx2("strong", { className: "font-semibold", children }),
425
+ em: ({ children }) => /* @__PURE__ */ jsx2("em", { className: "italic", children }),
426
+ code: ({ children }) => /* @__PURE__ */ jsx2("code", { className: "bg-surface-sunken rounded px-1 py-0.5 text-xs font-mono", children }),
427
+ // Prevent block elements that would break layout
428
+ script: () => null,
429
+ iframe: () => null
430
+ },
431
+ children: text
432
+ }
433
+ );
434
+ }
317
435
  function renderSteps(data) {
318
436
  const steps = tryParse(data.steps);
319
437
  if (!steps || !Array.isArray(steps)) return null;
@@ -326,7 +444,7 @@ function renderSteps(data) {
326
444
  children: i + 1
327
445
  }
328
446
  ),
329
- /* @__PURE__ */ jsx2("span", { className: "text-sm text-text-primary leading-relaxed", children: step })
447
+ /* @__PURE__ */ jsx2("span", { className: "text-sm text-text-primary leading-relaxed", children: /* @__PURE__ */ jsx2(InlineMarkdown, { text: step }) })
330
448
  ] }, i)) });
331
449
  }
332
450
  function renderTable(data) {
@@ -392,7 +510,7 @@ function renderList(data) {
392
510
  title && /* @__PURE__ */ jsx2("p", { className: "text-xs font-semibold uppercase tracking-wider text-text-secondary mb-1", children: title }),
393
511
  /* @__PURE__ */ jsx2("ul", { className: "flex flex-col gap-1.5", children: items.map((item, i) => /* @__PURE__ */ jsxs("li", { className: "flex items-start gap-2.5", children: [
394
512
  /* @__PURE__ */ jsx2("span", { className: "mt-1.5 h-1.5 w-1.5 shrink-0 rounded-full bg-accent", "aria-hidden": "true" }),
395
- /* @__PURE__ */ jsx2("span", { className: "text-sm text-text-primary leading-relaxed", children: item })
513
+ /* @__PURE__ */ jsx2("span", { className: "text-sm text-text-primary leading-relaxed", children: /* @__PURE__ */ jsx2(InlineMarkdown, { text: item }) })
396
514
  ] }, i)) })
397
515
  ] });
398
516
  }
@@ -427,7 +545,14 @@ function renderWarning(data) {
427
545
  }
428
546
  );
429
547
  }
430
- function StructuredResponse({ uiHint, data, className }) {
548
+ function StructuredResponse({ uiHint, data: rawData, className }) {
549
+ const data = typeof rawData === "string" ? (() => {
550
+ try {
551
+ return JSON.parse(rawData);
552
+ } catch {
553
+ return null;
554
+ }
555
+ })() : rawData;
431
556
  if (!data) return null;
432
557
  let content;
433
558
  switch (uiHint) {
@@ -507,7 +632,36 @@ function SourceCard({ source, variant = "compact", onNavigate, className }) {
507
632
  children: [
508
633
  /* @__PURE__ */ jsxs2("div", { className: "flex items-start justify-between gap-2", children: [
509
634
  /* @__PURE__ */ jsxs2("div", { className: "flex-1 min-w-0", children: [
510
- /* @__PURE__ */ jsx3("p", { className: "text-sm font-medium text-text-primary truncate", children: source.title }),
635
+ source.url ? /* @__PURE__ */ jsxs2(
636
+ "a",
637
+ {
638
+ href: source.url,
639
+ target: "_blank",
640
+ rel: "noopener noreferrer",
641
+ className: "text-sm font-medium text-accent hover:underline truncate block",
642
+ onClick: (e) => e.stopPropagation(),
643
+ children: [
644
+ source.title,
645
+ /* @__PURE__ */ jsxs2(
646
+ "svg",
647
+ {
648
+ className: "inline-block ml-1 w-3 h-3 opacity-60",
649
+ viewBox: "0 0 24 24",
650
+ fill: "none",
651
+ stroke: "currentColor",
652
+ strokeWidth: "2",
653
+ strokeLinecap: "round",
654
+ strokeLinejoin: "round",
655
+ children: [
656
+ /* @__PURE__ */ jsx3("path", { d: "M18 13v6a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h6" }),
657
+ /* @__PURE__ */ jsx3("polyline", { points: "15 3 21 3 21 9" }),
658
+ /* @__PURE__ */ jsx3("line", { x1: "10", y1: "14", x2: "21", y2: "3" })
659
+ ]
660
+ }
661
+ )
662
+ ]
663
+ }
664
+ ) : /* @__PURE__ */ jsx3("p", { className: "text-sm font-medium text-text-primary truncate", children: source.title }),
511
665
  source.section && /* @__PURE__ */ jsx3("p", { className: "text-[11px] font-semibold uppercase tracking-wider text-text-secondary truncate mt-0.5", children: source.section })
512
666
  ] }),
513
667
  /* @__PURE__ */ jsx3(
@@ -641,13 +795,16 @@ function AgentResponse({
641
795
  }) {
642
796
  return /* @__PURE__ */ jsxs4("div", { className: `flex flex-col gap-4 ${className ?? ""}`, "data-testid": "agent-response", children: [
643
797
  /* @__PURE__ */ jsx6(ResponseMessage, { content: response.message }),
644
- response.ui_hint !== "text" && response.structured_data && /* @__PURE__ */ jsx6(
645
- StructuredResponse,
646
- {
647
- uiHint: response.ui_hint,
648
- data: response.structured_data
649
- }
650
- ),
798
+ response.ui_hint !== "text" && response.structured_data && (() => {
799
+ const parsed = typeof response.structured_data === "string" ? (() => {
800
+ try {
801
+ return JSON.parse(response.structured_data);
802
+ } catch {
803
+ return null;
804
+ }
805
+ })() : response.structured_data;
806
+ return parsed ? /* @__PURE__ */ jsx6(StructuredResponse, { uiHint: response.ui_hint, data: parsed }) : null;
807
+ })(),
651
808
  (showConfidence || showVerification) && /* @__PURE__ */ jsxs4("div", { className: "flex flex-wrap items-center gap-2 mt-1", "data-testid": "response-meta", children: [
652
809
  showConfidence && /* @__PURE__ */ jsxs4(
653
810
  Badge2,
@@ -698,6 +855,31 @@ function AgentResponse({
698
855
 
699
856
  // src/chat/MessageBubble/MessageBubble.tsx
700
857
  import { jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
858
+ function DocumentIcon() {
859
+ return /* @__PURE__ */ jsxs5("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round", children: [
860
+ /* @__PURE__ */ jsx7("path", { d: "M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z" }),
861
+ /* @__PURE__ */ jsx7("polyline", { points: "14 2 14 8 20 8" }),
862
+ /* @__PURE__ */ jsx7("line", { x1: "16", y1: "13", x2: "8", y2: "13" }),
863
+ /* @__PURE__ */ jsx7("line", { x1: "16", y1: "17", x2: "8", y2: "17" })
864
+ ] });
865
+ }
866
+ function AttachmentThumbnail({ attachment }) {
867
+ const isImage = attachment.content_type.startsWith("image/");
868
+ if (isImage) {
869
+ return /* @__PURE__ */ jsx7("div", { className: "rounded-lg overflow-hidden border border-black/10 max-w-[240px]", children: /* @__PURE__ */ jsx7(
870
+ "img",
871
+ {
872
+ src: attachment.preview_url ?? `data:${attachment.content_type};base64,${attachment.data}`,
873
+ alt: attachment.filename,
874
+ className: "max-w-full max-h-[200px] object-contain"
875
+ }
876
+ ) });
877
+ }
878
+ return /* @__PURE__ */ jsxs5("div", { className: "flex items-center gap-2 px-3 py-2 rounded-lg border border-black/10 bg-black/5", children: [
879
+ /* @__PURE__ */ jsx7(DocumentIcon, {}),
880
+ /* @__PURE__ */ jsx7("span", { className: "text-xs truncate max-w-[160px]", children: attachment.filename })
881
+ ] });
882
+ }
701
883
  function MessageBubble({
702
884
  message,
703
885
  showAgent,
@@ -705,23 +887,29 @@ function MessageBubble({
705
887
  showConfidence = true,
706
888
  showVerification = true,
707
889
  animated = true,
890
+ userBubbleClassName,
708
891
  className
709
892
  }) {
710
893
  const isUser = message.role === "user";
894
+ const hasAttachments = message.attachments && message.attachments.length > 0;
711
895
  if (isUser) {
712
896
  return /* @__PURE__ */ jsx7(
713
897
  "div",
714
898
  {
715
899
  "data-message-id": message.id,
716
900
  className: twMerge4("flex w-full justify-end", className),
717
- children: /* @__PURE__ */ jsx7(
901
+ children: /* @__PURE__ */ jsxs5(
718
902
  "div",
719
903
  {
720
904
  className: twMerge4(
721
- "max-w-[70%] rounded-[18px] rounded-br-[4px] px-4 py-2.5 bg-accent text-brand-cream break-words whitespace-pre-wrap text-sm leading-relaxed",
722
- animated && "motion-safe:animate-slideFromRight"
905
+ "max-w-[70%] rounded-[18px] rounded-br-[4px] px-4 py-2.5 bg-[#e8e8e8] text-[#1a1a1a] break-words whitespace-pre-wrap text-sm leading-relaxed",
906
+ animated && "motion-safe:animate-slideFromRight",
907
+ userBubbleClassName
723
908
  ),
724
- children: message.content
909
+ children: [
910
+ hasAttachments && /* @__PURE__ */ jsx7("div", { className: "flex flex-wrap gap-2 mb-2", children: message.attachments.map((att, i) => /* @__PURE__ */ jsx7(AttachmentThumbnail, { attachment: att }, `${att.filename}-${i}`)) }),
911
+ message.content
912
+ ]
725
913
  }
726
914
  )
727
915
  }
@@ -733,7 +921,7 @@ function MessageBubble({
733
921
  "data-message-id": message.id,
734
922
  className: twMerge4("flex w-full flex-col items-start gap-1.5", className),
735
923
  children: [
736
- showAgent && message.agent && /* @__PURE__ */ jsx7("div", { className: "text-[11px] font-semibold uppercase tracking-[0.08em] text-text-muted px-1", children: message.agent.replace("_agent", "").replace("_", " ") }),
924
+ showAgent && message.agent && /* @__PURE__ */ jsx7("div", { className: "text-[11px] font-display font-semibold uppercase tracking-[0.08em] text-text-muted px-1", children: message.agent.replace("_agent", "").replace("_", " ") }),
737
925
  /* @__PURE__ */ jsx7(
738
926
  "div",
739
927
  {
@@ -759,34 +947,97 @@ function MessageBubble({
759
947
 
760
948
  // src/chat/MessageThread/MessageThread.tsx
761
949
  import { jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
762
- function MessageThread({ messages, streamingSlot, showSources, showConfidence, showVerification, className }) {
763
- const bottomRef = useRef2(null);
950
+ function MessageThread({ messages, streamingSlot, showAgent, showSources, showConfidence, showVerification, hideLastAssistant, userBubbleClassName, className }) {
951
+ const scrollRef = useRef2(null);
952
+ const shouldAutoScroll = useRef2(true);
953
+ const hasStreaming = !!streamingSlot;
954
+ const scrollToBottom = useCallback2(() => {
955
+ const el = scrollRef.current;
956
+ if (el && shouldAutoScroll.current) {
957
+ el.scrollTop = el.scrollHeight;
958
+ }
959
+ }, []);
960
+ useEffect(() => {
961
+ const el = scrollRef.current;
962
+ if (!el) return;
963
+ const onWheel = (e) => {
964
+ if (e.deltaY < 0) {
965
+ shouldAutoScroll.current = false;
966
+ }
967
+ };
968
+ const onPointerDown = () => {
969
+ el.dataset.userPointer = "1";
970
+ };
971
+ const onPointerUp = () => {
972
+ delete el.dataset.userPointer;
973
+ };
974
+ el.addEventListener("wheel", onWheel, { passive: true });
975
+ el.addEventListener("pointerdown", onPointerDown);
976
+ window.addEventListener("pointerup", onPointerUp);
977
+ return () => {
978
+ el.removeEventListener("wheel", onWheel);
979
+ el.removeEventListener("pointerdown", onPointerDown);
980
+ window.removeEventListener("pointerup", onPointerUp);
981
+ };
982
+ }, []);
983
+ const handleScroll = useCallback2(() => {
984
+ const el = scrollRef.current;
985
+ if (!el) return;
986
+ const nearBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 80;
987
+ if (nearBottom) {
988
+ shouldAutoScroll.current = true;
989
+ } else if (el.dataset.userPointer) {
990
+ shouldAutoScroll.current = false;
991
+ }
992
+ }, []);
993
+ useEffect(scrollToBottom, [messages.length, scrollToBottom]);
764
994
  useEffect(() => {
765
- bottomRef.current?.scrollIntoView?.({ behavior: "smooth" });
766
- }, [messages.length, streamingSlot]);
995
+ if (!hasStreaming) return;
996
+ let raf;
997
+ const tick = () => {
998
+ scrollToBottom();
999
+ raf = requestAnimationFrame(tick);
1000
+ };
1001
+ raf = requestAnimationFrame(tick);
1002
+ return () => cancelAnimationFrame(raf);
1003
+ }, [hasStreaming, scrollToBottom]);
1004
+ useEffect(() => {
1005
+ if (!hasStreaming) {
1006
+ shouldAutoScroll.current = true;
1007
+ }
1008
+ }, [hasStreaming]);
767
1009
  return /* @__PURE__ */ jsxs6(
768
1010
  "div",
769
1011
  {
1012
+ ref: scrollRef,
770
1013
  role: "log",
771
1014
  "aria-live": "polite",
772
1015
  "aria-label": "Message thread",
1016
+ onScroll: handleScroll,
773
1017
  className: twMerge5(
774
1018
  "flex flex-col gap-4 overflow-y-auto flex-1 px-4 py-6",
775
1019
  className
776
1020
  ),
777
1021
  children: [
778
- messages.map((message) => /* @__PURE__ */ jsx8(
779
- MessageBubble,
780
- {
781
- message,
782
- showSources,
783
- showConfidence,
784
- showVerification
785
- },
786
- message.id
787
- )),
788
- streamingSlot,
789
- /* @__PURE__ */ jsx8("div", { ref: bottomRef })
1022
+ /* @__PURE__ */ jsx8("div", { className: "flex-1 shrink-0" }),
1023
+ messages.map((message, i) => {
1024
+ if (hideLastAssistant && i === messages.length - 1 && message.role === "assistant") {
1025
+ return null;
1026
+ }
1027
+ return /* @__PURE__ */ jsx8(
1028
+ MessageBubble,
1029
+ {
1030
+ message,
1031
+ showAgent,
1032
+ showSources,
1033
+ showConfidence,
1034
+ showVerification,
1035
+ userBubbleClassName
1036
+ },
1037
+ message.id
1038
+ );
1039
+ }),
1040
+ streamingSlot
790
1041
  ]
791
1042
  }
792
1043
  );
@@ -794,32 +1045,127 @@ function MessageThread({ messages, streamingSlot, showSources, showConfidence, s
794
1045
 
795
1046
  // src/chat/MessageComposer/MessageComposer.tsx
796
1047
  import { twMerge as twMerge6 } from "tailwind-merge";
797
- import { useState as useState2, useRef as useRef3, useCallback as useCallback2 } from "react";
1048
+ import { useState as useState2, useRef as useRef3, useCallback as useCallback3 } from "react";
798
1049
  import { jsx as jsx9, jsxs as jsxs7 } from "react/jsx-runtime";
1050
+ var ALLOWED_TYPES = /* @__PURE__ */ new Set([
1051
+ "image/png",
1052
+ "image/jpeg",
1053
+ "image/gif",
1054
+ "image/webp",
1055
+ "application/pdf"
1056
+ ]);
1057
+ var MAX_FILE_SIZE = 10 * 1024 * 1024;
1058
+ var MAX_ATTACHMENTS = 5;
1059
+ function ArrowUpIcon() {
1060
+ return /* @__PURE__ */ jsxs7("svg", { width: "20", height: "20", viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round", children: [
1061
+ /* @__PURE__ */ jsx9("path", { d: "M10 16V4" }),
1062
+ /* @__PURE__ */ jsx9("path", { d: "M4 10l6-6 6 6" })
1063
+ ] });
1064
+ }
1065
+ function StopIcon() {
1066
+ return /* @__PURE__ */ jsx9("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "currentColor", children: /* @__PURE__ */ jsx9("rect", { x: "3", y: "3", width: "10", height: "10", rx: "2" }) });
1067
+ }
1068
+ function PaperclipIcon() {
1069
+ return /* @__PURE__ */ jsx9("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ jsx9("path", { d: "M21.44 11.05l-9.19 9.19a6 6 0 01-8.49-8.49l9.19-9.19a4 4 0 015.66 5.66l-9.2 9.19a2 2 0 01-2.83-2.83l8.49-8.48" }) });
1070
+ }
1071
+ function XIcon({ size = 14 }) {
1072
+ return /* @__PURE__ */ jsxs7("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round", children: [
1073
+ /* @__PURE__ */ jsx9("path", { d: "M18 6L6 18" }),
1074
+ /* @__PURE__ */ jsx9("path", { d: "M6 6l12 12" })
1075
+ ] });
1076
+ }
1077
+ function DocumentIcon2() {
1078
+ return /* @__PURE__ */ jsxs7("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round", children: [
1079
+ /* @__PURE__ */ jsx9("path", { d: "M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z" }),
1080
+ /* @__PURE__ */ jsx9("polyline", { points: "14 2 14 8 20 8" }),
1081
+ /* @__PURE__ */ jsx9("line", { x1: "16", y1: "13", x2: "8", y2: "13" }),
1082
+ /* @__PURE__ */ jsx9("line", { x1: "16", y1: "17", x2: "8", y2: "17" }),
1083
+ /* @__PURE__ */ jsx9("polyline", { points: "10 9 9 9 8 9" })
1084
+ ] });
1085
+ }
1086
+ function fileToBase64(file) {
1087
+ return new Promise((resolve, reject) => {
1088
+ const reader = new FileReader();
1089
+ reader.onload = () => {
1090
+ const result = reader.result;
1091
+ const base64 = result.split(",")[1];
1092
+ resolve(base64);
1093
+ };
1094
+ reader.onerror = reject;
1095
+ reader.readAsDataURL(file);
1096
+ });
1097
+ }
1098
+ function AttachmentPreview({
1099
+ attachment,
1100
+ onRemove
1101
+ }) {
1102
+ const isImage = attachment.content_type.startsWith("image/");
1103
+ return /* @__PURE__ */ jsxs7("div", { className: "relative group flex-shrink-0", children: [
1104
+ isImage ? /* @__PURE__ */ jsx9("div", { className: "w-16 h-16 rounded-lg overflow-hidden border border-border/60 bg-surface-alt", children: /* @__PURE__ */ jsx9(
1105
+ "img",
1106
+ {
1107
+ src: attachment.preview_url ?? `data:${attachment.content_type};base64,${attachment.data}`,
1108
+ alt: attachment.filename,
1109
+ className: "w-full h-full object-cover"
1110
+ }
1111
+ ) }) : /* @__PURE__ */ jsxs7("div", { className: "h-16 px-3 rounded-lg border border-border/60 bg-surface-alt flex items-center gap-2", children: [
1112
+ /* @__PURE__ */ jsx9("div", { className: "text-text-muted", children: /* @__PURE__ */ jsx9(DocumentIcon2, {}) }),
1113
+ /* @__PURE__ */ jsxs7("div", { className: "flex flex-col min-w-0", children: [
1114
+ /* @__PURE__ */ jsx9("span", { className: "text-xs text-text-primary truncate max-w-[120px]", children: attachment.filename }),
1115
+ /* @__PURE__ */ jsx9("span", { className: "text-[10px] text-text-muted", children: "PDF" })
1116
+ ] })
1117
+ ] }),
1118
+ /* @__PURE__ */ jsx9(
1119
+ "button",
1120
+ {
1121
+ type: "button",
1122
+ onClick: onRemove,
1123
+ className: twMerge6(
1124
+ "absolute -top-1.5 -right-1.5",
1125
+ "w-5 h-5 rounded-full",
1126
+ "bg-text-muted/80 text-white",
1127
+ "flex items-center justify-center",
1128
+ "opacity-0 group-hover:opacity-100",
1129
+ "transition-opacity duration-150",
1130
+ "hover:bg-text-primary"
1131
+ ),
1132
+ "aria-label": `Remove ${attachment.filename}`,
1133
+ children: /* @__PURE__ */ jsx9(XIcon, { size: 10 })
1134
+ }
1135
+ )
1136
+ ] });
1137
+ }
799
1138
  function MessageComposer({
800
1139
  onSend,
1140
+ onStop,
801
1141
  isLoading = false,
802
1142
  placeholder = "Type a message...",
803
1143
  className
804
1144
  }) {
805
1145
  const [value, setValue] = useState2("");
1146
+ const [attachments, setAttachments] = useState2([]);
1147
+ const [dragOver, setDragOver] = useState2(false);
806
1148
  const textareaRef = useRef3(null);
807
- const canSend = value.trim().length > 0 && !isLoading;
808
- const resetHeight = useCallback2(() => {
1149
+ const fileInputRef = useRef3(null);
1150
+ const canSend = (value.trim().length > 0 || attachments.length > 0) && !isLoading;
1151
+ const resetHeight = useCallback3(() => {
809
1152
  const el = textareaRef.current;
810
1153
  if (el) {
811
1154
  el.style.height = "auto";
812
1155
  el.style.overflowY = "hidden";
813
1156
  }
814
1157
  }, []);
815
- const handleSend = useCallback2(() => {
1158
+ const handleSend = useCallback3(() => {
816
1159
  if (!canSend) return;
817
- onSend(value.trim());
1160
+ const message = value.trim() || (attachments.length > 0 ? "Please analyse the attached file(s)." : "");
1161
+ if (!message && attachments.length === 0) return;
1162
+ onSend(message, attachments.length > 0 ? attachments : void 0);
818
1163
  setValue("");
1164
+ setAttachments([]);
819
1165
  resetHeight();
820
1166
  textareaRef.current?.focus();
821
- }, [canSend, onSend, value, resetHeight]);
822
- const handleKeyDown = useCallback2(
1167
+ }, [canSend, onSend, value, attachments, resetHeight]);
1168
+ const handleKeyDown = useCallback3(
823
1169
  (e) => {
824
1170
  if (e.key === "Enter" && !e.shiftKey) {
825
1171
  e.preventDefault();
@@ -828,64 +1174,194 @@ function MessageComposer({
828
1174
  },
829
1175
  [handleSend]
830
1176
  );
831
- const handleChange = useCallback2(
1177
+ const handleChange = useCallback3(
832
1178
  (e) => {
833
1179
  setValue(e.target.value);
834
1180
  const el = e.target;
835
1181
  el.style.height = "auto";
836
- const capped = Math.min(el.scrollHeight, 128);
1182
+ const capped = Math.min(el.scrollHeight, 200);
837
1183
  el.style.height = `${capped}px`;
838
- el.style.overflowY = el.scrollHeight > 128 ? "auto" : "hidden";
1184
+ el.style.overflowY = el.scrollHeight > 200 ? "auto" : "hidden";
839
1185
  },
840
1186
  []
841
1187
  );
1188
+ const addFiles = useCallback3(async (files) => {
1189
+ const fileArray = Array.from(files);
1190
+ for (const file of fileArray) {
1191
+ if (attachments.length >= MAX_ATTACHMENTS) break;
1192
+ if (!ALLOWED_TYPES.has(file.type)) continue;
1193
+ if (file.size > MAX_FILE_SIZE) continue;
1194
+ try {
1195
+ const data = await fileToBase64(file);
1196
+ const previewUrl = file.type.startsWith("image/") ? URL.createObjectURL(file) : void 0;
1197
+ const attachment = {
1198
+ filename: file.name,
1199
+ content_type: file.type,
1200
+ data,
1201
+ preview_url: previewUrl
1202
+ };
1203
+ setAttachments((prev) => {
1204
+ if (prev.length >= MAX_ATTACHMENTS) return prev;
1205
+ return [...prev, attachment];
1206
+ });
1207
+ } catch {
1208
+ }
1209
+ }
1210
+ }, [attachments.length]);
1211
+ const handleFileSelect = useCallback3(() => {
1212
+ fileInputRef.current?.click();
1213
+ }, []);
1214
+ const handleFileInputChange = useCallback3(
1215
+ (e) => {
1216
+ if (e.target.files) {
1217
+ void addFiles(e.target.files);
1218
+ e.target.value = "";
1219
+ }
1220
+ },
1221
+ [addFiles]
1222
+ );
1223
+ const removeAttachment = useCallback3((index) => {
1224
+ setAttachments((prev) => {
1225
+ const removed = prev[index];
1226
+ if (removed?.preview_url) URL.revokeObjectURL(removed.preview_url);
1227
+ return prev.filter((_, i) => i !== index);
1228
+ });
1229
+ }, []);
1230
+ const handlePaste = useCallback3(
1231
+ (e) => {
1232
+ const items = e.clipboardData.items;
1233
+ const files = [];
1234
+ for (const item of items) {
1235
+ if (item.kind === "file" && ALLOWED_TYPES.has(item.type)) {
1236
+ const file = item.getAsFile();
1237
+ if (file) files.push(file);
1238
+ }
1239
+ }
1240
+ if (files.length > 0) {
1241
+ void addFiles(files);
1242
+ }
1243
+ },
1244
+ [addFiles]
1245
+ );
1246
+ const handleDragOver = useCallback3((e) => {
1247
+ e.preventDefault();
1248
+ e.stopPropagation();
1249
+ setDragOver(true);
1250
+ }, []);
1251
+ const handleDragLeave = useCallback3((e) => {
1252
+ e.preventDefault();
1253
+ e.stopPropagation();
1254
+ setDragOver(false);
1255
+ }, []);
1256
+ const handleDrop = useCallback3(
1257
+ (e) => {
1258
+ e.preventDefault();
1259
+ e.stopPropagation();
1260
+ setDragOver(false);
1261
+ if (e.dataTransfer.files.length > 0) {
1262
+ void addFiles(e.dataTransfer.files);
1263
+ }
1264
+ },
1265
+ [addFiles]
1266
+ );
842
1267
  return /* @__PURE__ */ jsxs7(
843
1268
  "div",
844
1269
  {
845
1270
  className: twMerge6(
846
- "flex items-end gap-3 shrink-0 border-t border-border px-4 py-3",
1271
+ "relative shrink-0 rounded-3xl border bg-surface",
1272
+ "shadow-lg shadow-black/10",
1273
+ "transition-all duration-200",
1274
+ "focus-within:border-accent/40 focus-within:shadow-accent/5",
1275
+ dragOver ? "border-accent/60 bg-accent/5" : "border-border/60",
847
1276
  className
848
1277
  ),
1278
+ onDragOver: handleDragOver,
1279
+ onDragLeave: handleDragLeave,
1280
+ onDrop: handleDrop,
849
1281
  children: [
850
1282
  /* @__PURE__ */ jsx9(
851
- "textarea",
1283
+ "input",
852
1284
  {
853
- ref: textareaRef,
854
- value,
855
- onChange: handleChange,
856
- onKeyDown: handleKeyDown,
857
- placeholder,
858
- rows: 1,
859
- disabled: isLoading,
860
- className: twMerge6(
861
- "flex-1 resize-none rounded-xl border border-border bg-surface/80",
862
- "px-4 py-2.5 text-sm text-text-primary placeholder:text-text-muted",
863
- "focus:border-transparent focus:ring-2 focus:ring-accent/40 focus:outline-none",
864
- "disabled:opacity-50 disabled:cursor-not-allowed",
865
- "overflow-hidden",
866
- "transition-all duration-200"
867
- ),
868
- style: { colorScheme: "dark" },
869
- "aria-label": "Message input"
1285
+ ref: fileInputRef,
1286
+ type: "file",
1287
+ multiple: true,
1288
+ accept: "image/png,image/jpeg,image/gif,image/webp,application/pdf",
1289
+ onChange: handleFileInputChange,
1290
+ className: "hidden",
1291
+ "aria-hidden": "true"
870
1292
  }
871
1293
  ),
872
- /* @__PURE__ */ jsx9(
873
- "button",
1294
+ attachments.length > 0 && /* @__PURE__ */ jsx9("div", { className: "flex gap-2 px-4 pt-3 pb-1 overflow-x-auto", children: attachments.map((att, i) => /* @__PURE__ */ jsx9(
1295
+ AttachmentPreview,
874
1296
  {
875
- type: "button",
876
- onClick: handleSend,
877
- disabled: !value.trim() || isLoading,
878
- "aria-label": "Send message",
879
- className: twMerge6(
880
- "inline-flex items-center justify-center rounded-xl px-5 py-2.5",
881
- "text-sm font-semibold text-white shrink-0",
882
- "transition-all duration-200",
883
- "focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent",
884
- value.trim() && !isLoading ? "bg-accent hover:bg-accent-hover hover:scale-[1.02] hover:shadow-glow-cyan active:scale-[0.98]" : "bg-accent/30 text-text-muted cursor-not-allowed"
885
- ),
886
- children: "Send"
887
- }
888
- )
1297
+ attachment: att,
1298
+ onRemove: () => removeAttachment(i)
1299
+ },
1300
+ `${att.filename}-${i}`
1301
+ )) }),
1302
+ dragOver && /* @__PURE__ */ jsx9("div", { className: "absolute inset-0 rounded-3xl flex items-center justify-center bg-accent/10 border-2 border-dashed border-accent/40 z-10 pointer-events-none", children: /* @__PURE__ */ jsx9("span", { className: "text-sm font-display font-semibold text-accent", children: "Drop files here" }) }),
1303
+ /* @__PURE__ */ jsxs7("div", { className: "flex items-end", children: [
1304
+ /* @__PURE__ */ jsx9(
1305
+ "button",
1306
+ {
1307
+ type: "button",
1308
+ onClick: handleFileSelect,
1309
+ disabled: isLoading || attachments.length >= MAX_ATTACHMENTS,
1310
+ "aria-label": "Attach file",
1311
+ className: twMerge6(
1312
+ "flex-shrink-0 ml-2 mb-3",
1313
+ "inline-flex items-center justify-center",
1314
+ "w-9 h-9 rounded-full",
1315
+ "transition-all duration-200",
1316
+ "text-text-muted/60 hover:text-text-secondary hover:bg-text-muted/10",
1317
+ "focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent",
1318
+ "disabled:opacity-30 disabled:cursor-not-allowed disabled:hover:bg-transparent"
1319
+ ),
1320
+ children: /* @__PURE__ */ jsx9(PaperclipIcon, {})
1321
+ }
1322
+ ),
1323
+ /* @__PURE__ */ jsx9(
1324
+ "textarea",
1325
+ {
1326
+ ref: textareaRef,
1327
+ value,
1328
+ onChange: handleChange,
1329
+ onKeyDown: handleKeyDown,
1330
+ onPaste: handlePaste,
1331
+ placeholder,
1332
+ rows: 1,
1333
+ disabled: isLoading,
1334
+ className: twMerge6(
1335
+ "flex-1 resize-none bg-transparent",
1336
+ "pl-2 pr-14 pt-4 pb-4 text-[15px] leading-relaxed",
1337
+ "text-text-primary placeholder:text-text-muted/70",
1338
+ "focus:outline-none",
1339
+ "disabled:opacity-50 disabled:cursor-not-allowed",
1340
+ "overflow-hidden"
1341
+ ),
1342
+ style: { colorScheme: "dark" },
1343
+ "aria-label": "Message input"
1344
+ }
1345
+ ),
1346
+ /* @__PURE__ */ jsx9(
1347
+ "button",
1348
+ {
1349
+ type: "button",
1350
+ onClick: isLoading && onStop ? onStop : handleSend,
1351
+ disabled: !canSend && !isLoading,
1352
+ "aria-label": isLoading ? "Stop generating" : "Send message",
1353
+ className: twMerge6(
1354
+ "absolute bottom-3 right-3",
1355
+ "inline-flex items-center justify-center",
1356
+ "w-9 h-9 rounded-full",
1357
+ "transition-all duration-200",
1358
+ "focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent",
1359
+ canSend ? "bg-accent text-white hover:bg-accent-hover active:scale-90 shadow-md shadow-accent/25" : isLoading ? "bg-text-muted/20 text-text-secondary hover:bg-text-muted/30 cursor-pointer" : "bg-transparent text-text-muted/40 cursor-default"
1360
+ ),
1361
+ children: isLoading ? /* @__PURE__ */ jsx9(StopIcon, {}) : /* @__PURE__ */ jsx9(ArrowUpIcon, {})
1362
+ }
1363
+ )
1364
+ ] })
889
1365
  ]
890
1366
  }
891
1367
  );
@@ -898,6 +1374,7 @@ function WelcomeScreen({
898
1374
  title = "Welcome",
899
1375
  message = "How can I help you today?",
900
1376
  icon,
1377
+ iconClassName,
901
1378
  suggestedQuestions = [],
902
1379
  onQuestionSelect,
903
1380
  className
@@ -910,12 +1387,15 @@ function WelcomeScreen({
910
1387
  className
911
1388
  ),
912
1389
  children: [
913
- /* @__PURE__ */ jsx10(
1390
+ icon ? iconClassName ? /* @__PURE__ */ jsx10("div", { className: iconClassName, "aria-hidden": "true", children: icon }) : icon : /* @__PURE__ */ jsx10(
914
1391
  "div",
915
1392
  {
916
- className: "w-14 h-14 rounded-2xl bg-accent/10 border border-border flex items-center justify-center pulse-glow",
1393
+ className: twMerge7(
1394
+ "w-14 h-14 rounded-2xl bg-accent/10 border border-border flex items-center justify-center pulse-glow",
1395
+ iconClassName
1396
+ ),
917
1397
  "aria-hidden": "true",
918
- children: icon ?? /* @__PURE__ */ jsx10("span", { className: "text-2xl", children: "\u2726" })
1398
+ children: /* @__PURE__ */ jsx10("span", { className: "text-2xl", children: "\u2726" })
919
1399
  }
920
1400
  ),
921
1401
  /* @__PURE__ */ jsxs8("div", { className: "flex flex-col gap-2", children: [
@@ -925,7 +1405,7 @@ function WelcomeScreen({
925
1405
  suggestedQuestions.length > 0 && /* @__PURE__ */ jsx10(
926
1406
  "div",
927
1407
  {
928
- className: "flex flex-wrap justify-center gap-2 max-w-md",
1408
+ className: "flex flex-wrap justify-center gap-2 max-w-xl",
929
1409
  role: "group",
930
1410
  "aria-label": "Suggested questions",
931
1411
  children: suggestedQuestions.map((question) => /* @__PURE__ */ jsx10(
@@ -934,7 +1414,7 @@ function WelcomeScreen({
934
1414
  type: "button",
935
1415
  onClick: () => onQuestionSelect?.(question),
936
1416
  className: twMerge7(
937
- "px-4 py-2 rounded-full text-sm",
1417
+ "px-3.5 py-1.5 rounded-full text-[12px]",
938
1418
  "border border-border bg-transparent text-text-secondary",
939
1419
  "hover:bg-accent/10 hover:border-interactive hover:text-text-primary",
940
1420
  "focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent",
@@ -953,7 +1433,8 @@ function WelcomeScreen({
953
1433
 
954
1434
  // src/streaming/StreamingMessage/StreamingMessage.tsx
955
1435
  import { useEffect as useEffect3, useRef as useRef5 } from "react";
956
- import { Spinner } from "@surf-kit/core";
1436
+ import { twMerge as twMerge8 } from "tailwind-merge";
1437
+ import { WaveLoader } from "@surf-kit/core";
957
1438
 
958
1439
  // src/hooks/useCharacterDrain.ts
959
1440
  import { useState as useState3, useRef as useRef4, useEffect as useEffect2 } from "react";
@@ -982,7 +1463,10 @@ function useCharacterDrain(target, msPerChar = 15) {
982
1463
  const elapsed = now - lastTimeRef.current;
983
1464
  const charsToAdvance = Math.floor(elapsed / msPerCharRef.current);
984
1465
  if (charsToAdvance > 0 && indexRef.current < currentTarget.length) {
985
- const nextIndex = Math.min(indexRef.current + charsToAdvance, currentTarget.length);
1466
+ let nextIndex = Math.min(indexRef.current + charsToAdvance, currentTarget.length);
1467
+ while (nextIndex < currentTarget.length && currentTarget[nextIndex - 1].trim() === "") {
1468
+ nextIndex++;
1469
+ }
986
1470
  indexRef.current = nextIndex;
987
1471
  lastTimeRef.current = now;
988
1472
  setDisplayed(currentTarget.slice(0, nextIndex));
@@ -1027,14 +1511,36 @@ var phaseLabels = {
1027
1511
  generating: "Writing...",
1028
1512
  verifying: "Verifying..."
1029
1513
  };
1514
+ var CURSOR_STYLES = `
1515
+ .sk-streaming-cursor > :not(ul,ol,blockquote,div:has(table)):last-child::after,
1516
+ .sk-streaming-cursor > :is(ul,ol):last-child > li:last-child::after,
1517
+ .sk-streaming-cursor > blockquote:last-child > p:last-child::after,
1518
+ .sk-streaming-cursor > div:has(table):last-child table tbody tr:last-child td:last-child::after {
1519
+ content: "";
1520
+ display: inline-block;
1521
+ width: 2px;
1522
+ height: 1em;
1523
+ background: var(--color-accent, #38bdf8);
1524
+ animation: sk-cursor-blink 0.8s steps(1) infinite;
1525
+ margin-left: 2px;
1526
+ vertical-align: text-bottom;
1527
+ }
1528
+ @keyframes sk-cursor-blink {
1529
+ 0%, 60% { opacity: 1; }
1530
+ 61%, 100% { opacity: 0; }
1531
+ }
1532
+ `;
1030
1533
  function StreamingMessage({
1031
1534
  stream,
1032
1535
  onComplete,
1536
+ onDraining,
1033
1537
  showPhases = true,
1034
1538
  className
1035
1539
  }) {
1036
1540
  const onCompleteRef = useRef5(onComplete);
1037
1541
  onCompleteRef.current = onComplete;
1542
+ const onDrainingRef = useRef5(onDraining);
1543
+ onDrainingRef.current = onDraining;
1038
1544
  const wasActiveRef = useRef5(stream.active);
1039
1545
  useEffect3(() => {
1040
1546
  if (wasActiveRef.current && !stream.active) {
@@ -1043,35 +1549,40 @@ function StreamingMessage({
1043
1549
  wasActiveRef.current = stream.active;
1044
1550
  }, [stream.active]);
1045
1551
  const phaseLabel = phaseLabels[stream.phase];
1046
- const { displayed: displayedContent } = useCharacterDrain(stream.content);
1047
- return /* @__PURE__ */ jsxs9("div", { className, "data-testid": "streaming-message", children: [
1552
+ const { displayed: rawDisplayed, isDraining } = useCharacterDrain(stream.content);
1553
+ const displayedContent = stream.active || isDraining ? rawDisplayed.trimEnd() : rawDisplayed;
1554
+ useEffect3(() => {
1555
+ onDrainingRef.current?.(isDraining);
1556
+ }, [isDraining]);
1557
+ const agentLabel = stream.agent ? stream.agent.replace("_agent", "").replace("_", " ") : null;
1558
+ const showPhaseIndicator = showPhases && stream.active && stream.phase !== "idle" && !displayedContent;
1559
+ const showCursor = (stream.active || isDraining) && !!displayedContent;
1560
+ return /* @__PURE__ */ jsxs9("div", { className: twMerge8("flex w-full flex-col items-start", className), "data-testid": "streaming-message", children: [
1048
1561
  /* @__PURE__ */ jsxs9("div", { "aria-live": "assertive", className: "sr-only", children: [
1049
1562
  stream.active && stream.phase !== "idle" && "Response started",
1050
1563
  !stream.active && stream.content && "Response complete"
1051
1564
  ] }),
1565
+ showCursor && /* @__PURE__ */ jsx11("style", { children: CURSOR_STYLES }),
1566
+ agentLabel && /* @__PURE__ */ jsx11("div", { className: "text-[11px] font-display font-semibold uppercase tracking-[0.08em] text-text-muted px-1 mb-1.5", children: agentLabel }),
1052
1567
  /* @__PURE__ */ jsxs9("div", { className: "max-w-[88%] px-4 py-3 rounded-[18px] rounded-tl-[4px] bg-surface border border-border motion-safe:animate-springFromLeft", children: [
1053
- showPhases && stream.active && stream.phase !== "idle" && /* @__PURE__ */ jsxs9(
1568
+ showPhaseIndicator && /* @__PURE__ */ jsxs9(
1054
1569
  "div",
1055
1570
  {
1056
- className: "flex items-center gap-2 mb-2 text-sm text-text-secondary",
1571
+ className: "flex items-center gap-2 text-sm text-text-secondary",
1057
1572
  "data-testid": "phase-indicator",
1058
1573
  children: [
1059
- /* @__PURE__ */ jsx11("span", { "aria-hidden": "true", children: /* @__PURE__ */ jsx11(Spinner, { size: "sm" }) }),
1574
+ /* @__PURE__ */ jsx11("span", { "aria-hidden": "true", children: /* @__PURE__ */ jsx11(WaveLoader, { size: "sm", color: "#38bdf8" }) }),
1060
1575
  /* @__PURE__ */ jsx11("span", { children: phaseLabel })
1061
1576
  ]
1062
1577
  }
1063
1578
  ),
1064
- /* @__PURE__ */ jsxs9("div", { className: "text-sm leading-relaxed text-text-primary whitespace-pre-wrap", children: [
1065
- displayedContent,
1066
- stream.active && /* @__PURE__ */ jsx11(
1067
- "span",
1068
- {
1069
- className: "inline-block w-0.5 h-4 bg-accent align-text-bottom animate-pulse ml-0.5",
1070
- "aria-hidden": "true",
1071
- "data-testid": "streaming-cursor"
1072
- }
1073
- )
1074
- ] })
1579
+ displayedContent && /* @__PURE__ */ jsx11(
1580
+ ResponseMessage,
1581
+ {
1582
+ content: displayedContent,
1583
+ className: showCursor ? "sk-streaming-cursor" : void 0
1584
+ }
1585
+ )
1075
1586
  ] })
1076
1587
  ] });
1077
1588
  }
@@ -1102,7 +1613,7 @@ function AgentChat({
1102
1613
  return /* @__PURE__ */ jsxs10(
1103
1614
  "div",
1104
1615
  {
1105
- className: twMerge8(
1616
+ className: twMerge9(
1106
1617
  "flex flex-col h-full bg-canvas border border-border rounded-xl overflow-hidden",
1107
1618
  className
1108
1619
  ),
@@ -1138,14 +1649,14 @@ function AgentChat({
1138
1649
  onQuestionSelect: handleQuestionSelect
1139
1650
  }
1140
1651
  ),
1141
- /* @__PURE__ */ jsx12(MessageComposer, { onSend: handleSend, isLoading: state.isLoading })
1652
+ /* @__PURE__ */ jsx12(MessageComposer, { onSend: handleSend, onStop: actions.stop, isLoading: state.isLoading })
1142
1653
  ]
1143
1654
  }
1144
1655
  );
1145
1656
  }
1146
1657
 
1147
1658
  // src/chat/ConversationList/ConversationList.tsx
1148
- import { twMerge as twMerge9 } from "tailwind-merge";
1659
+ import { twMerge as twMerge10 } from "tailwind-merge";
1149
1660
  import { jsx as jsx13, jsxs as jsxs11 } from "react/jsx-runtime";
1150
1661
  function ConversationList({
1151
1662
  conversations,
@@ -1159,14 +1670,14 @@ function ConversationList({
1159
1670
  "nav",
1160
1671
  {
1161
1672
  "aria-label": "Conversation list",
1162
- className: twMerge9("flex flex-col h-full bg-canvas", className),
1673
+ className: twMerge10("flex flex-col flex-1 min-h-0", className),
1163
1674
  children: [
1164
- onNew && /* @__PURE__ */ jsx13("div", { className: "p-3 border-b border-border", children: /* @__PURE__ */ jsx13(
1675
+ onNew && /* @__PURE__ */ jsx13("div", { className: "px-5 pt-1 pb-3 border-b border-border", children: /* @__PURE__ */ jsx13(
1165
1676
  "button",
1166
1677
  {
1167
1678
  type: "button",
1168
1679
  onClick: onNew,
1169
- className: "w-full px-4 py-2.5 rounded-xl text-sm font-semibold bg-accent text-white hover:bg-accent-hover transition-all duration-200",
1680
+ className: "w-full px-4 py-2 rounded-lg text-sm font-medium border border-border text-text-secondary hover:text-text-primary hover:bg-surface hover:border-border-strong transition-colors duration-150",
1170
1681
  children: "New conversation"
1171
1682
  }
1172
1683
  ) }),
@@ -1176,10 +1687,10 @@ function ConversationList({
1176
1687
  return /* @__PURE__ */ jsxs11(
1177
1688
  "li",
1178
1689
  {
1179
- className: twMerge9(
1180
- "flex items-start border-b border-border transition-colors duration-200",
1181
- "hover:bg-surface",
1182
- isActive && "bg-surface-raised border-l-2 border-l-accent"
1690
+ className: twMerge10(
1691
+ "flex items-start transition-colors duration-150",
1692
+ "hover:bg-surface-raised",
1693
+ isActive ? "bg-accent-subtlest border-l-[3px] border-l-accent" : "border-l-[3px] border-l-transparent"
1183
1694
  ),
1184
1695
  children: [
1185
1696
  /* @__PURE__ */ jsxs11(
@@ -1188,10 +1699,10 @@ function ConversationList({
1188
1699
  type: "button",
1189
1700
  onClick: () => onSelect(conversation.id),
1190
1701
  "aria-current": isActive ? "true" : void 0,
1191
- className: "flex-1 min-w-0 text-left px-4 py-3",
1702
+ className: "flex-1 min-w-0 text-left px-5 py-2.5",
1192
1703
  children: [
1193
- /* @__PURE__ */ jsx13("div", { className: "text-sm font-medium text-brand-cream truncate", children: conversation.title }),
1194
- /* @__PURE__ */ jsx13("div", { className: "text-xs text-brand-cream/40 truncate mt-0.5 leading-relaxed", children: conversation.lastMessage })
1704
+ /* @__PURE__ */ jsx13("div", { className: "text-sm font-medium text-text-primary truncate", children: conversation.title }),
1705
+ /* @__PURE__ */ jsx13("div", { className: "text-xs text-text-muted truncate mt-0.5 leading-relaxed", children: conversation.lastMessage })
1195
1706
  ]
1196
1707
  }
1197
1708
  ),
@@ -1201,7 +1712,7 @@ function ConversationList({
1201
1712
  type: "button",
1202
1713
  onClick: () => onDelete(conversation.id),
1203
1714
  "aria-label": `Delete ${conversation.title}`,
1204
- className: "shrink-0 p-1.5 m-2 rounded-lg text-brand-cream/25 hover:text-brand-watermelon hover:bg-brand-watermelon/10 transition-colors duration-200",
1715
+ className: "shrink-0 p-1.5 m-2 rounded-lg text-text-muted hover:text-status-error hover:bg-status-error/10 transition-colors duration-150",
1205
1716
  children: /* @__PURE__ */ jsxs11(
1206
1717
  "svg",
1207
1718
  {
@@ -1228,7 +1739,7 @@ function ConversationList({
1228
1739
  conversation.id
1229
1740
  );
1230
1741
  }),
1231
- conversations.length === 0 && /* @__PURE__ */ jsx13("li", { className: "px-4 py-8 text-center", children: /* @__PURE__ */ jsx13("span", { className: "text-sm text-brand-cream/30 font-body", children: "No conversations yet" }) })
1742
+ conversations.length === 0 && /* @__PURE__ */ jsx13("li", { className: "px-5 py-12 text-center", children: /* @__PURE__ */ jsx13("span", { className: "text-sm text-text-muted font-body", children: "No conversations yet" }) })
1232
1743
  ] })
1233
1744
  ]
1234
1745
  }
@@ -1236,7 +1747,7 @@ function ConversationList({
1236
1747
  }
1237
1748
 
1238
1749
  // src/layouts/AgentFullPage/AgentFullPage.tsx
1239
- import { Fragment, jsx as jsx14, jsxs as jsxs12 } from "react/jsx-runtime";
1750
+ import { Fragment as Fragment2, jsx as jsx14, jsxs as jsxs12 } from "react/jsx-runtime";
1240
1751
  function AgentFullPage({
1241
1752
  endpoint,
1242
1753
  title = "Chat",
@@ -1249,7 +1760,7 @@ function AgentFullPage({
1249
1760
  className
1250
1761
  }) {
1251
1762
  const [sidebarOpen, setSidebarOpen] = useState4(false);
1252
- const handleSelect = useCallback3(
1763
+ const handleSelect = useCallback4(
1253
1764
  (id) => {
1254
1765
  onConversationSelect?.(id);
1255
1766
  setSidebarOpen(false);
@@ -1259,10 +1770,10 @@ function AgentFullPage({
1259
1770
  return /* @__PURE__ */ jsxs12(
1260
1771
  "div",
1261
1772
  {
1262
- className: twMerge10("flex h-screen w-full overflow-hidden bg-brand-dark", className),
1773
+ className: twMerge11("flex h-screen w-full overflow-hidden bg-brand-dark", className),
1263
1774
  "data-testid": "agent-full-page",
1264
1775
  children: [
1265
- showConversationList && /* @__PURE__ */ jsxs12(Fragment, { children: [
1776
+ showConversationList && /* @__PURE__ */ jsxs12(Fragment2, { children: [
1266
1777
  sidebarOpen && /* @__PURE__ */ jsx14(
1267
1778
  "div",
1268
1779
  {
@@ -1274,7 +1785,7 @@ function AgentFullPage({
1274
1785
  /* @__PURE__ */ jsx14(
1275
1786
  "aside",
1276
1787
  {
1277
- className: twMerge10(
1788
+ className: twMerge11(
1278
1789
  "bg-brand-dark border-r border-brand-gold/15 w-72 shrink-0 flex-col z-40",
1279
1790
  // Desktop: always visible
1280
1791
  "hidden md:flex",
@@ -1340,7 +1851,7 @@ function AgentFullPage({
1340
1851
  }
1341
1852
 
1342
1853
  // src/layouts/AgentPanel/AgentPanel.tsx
1343
- import { twMerge as twMerge11 } from "tailwind-merge";
1854
+ import { twMerge as twMerge12 } from "tailwind-merge";
1344
1855
  import { useRef as useRef6, useEffect as useEffect4 } from "react";
1345
1856
  import { jsx as jsx15, jsxs as jsxs13 } from "react/jsx-runtime";
1346
1857
  function AgentPanel({
@@ -1365,13 +1876,13 @@ function AgentPanel({
1365
1876
  return /* @__PURE__ */ jsxs13(
1366
1877
  "div",
1367
1878
  {
1368
- className: twMerge11("fixed inset-0 z-50", !isOpen && "pointer-events-none"),
1879
+ className: twMerge12("fixed inset-0 z-50", !isOpen && "pointer-events-none"),
1369
1880
  "aria-hidden": !isOpen,
1370
1881
  children: [
1371
1882
  /* @__PURE__ */ jsx15(
1372
1883
  "div",
1373
1884
  {
1374
- className: twMerge11(
1885
+ className: twMerge12(
1375
1886
  "fixed inset-0 transition-opacity duration-300",
1376
1887
  isOpen ? "opacity-100 bg-brand-dark/70 backdrop-blur-sm pointer-events-auto" : "opacity-0 pointer-events-none"
1377
1888
  ),
@@ -1387,7 +1898,7 @@ function AgentPanel({
1387
1898
  "aria-label": title,
1388
1899
  "aria-modal": isOpen ? "true" : void 0,
1389
1900
  style: { width: widthStyle, maxWidth: "100vw" },
1390
- className: twMerge11(
1901
+ className: twMerge12(
1391
1902
  "fixed top-0 h-full flex flex-col z-50 bg-brand-dark shadow-card",
1392
1903
  "transition-transform duration-300 ease-in-out",
1393
1904
  side === "left" ? `left-0 border-r border-brand-gold/15 ${isOpen ? "translate-x-0" : "-translate-x-full"}` : `right-0 border-l border-brand-gold/15 ${isOpen ? "translate-x-0" : "translate-x-full"}`,
@@ -1429,8 +1940,8 @@ function AgentPanel({
1429
1940
  }
1430
1941
 
1431
1942
  // src/layouts/AgentWidget/AgentWidget.tsx
1432
- import { twMerge as twMerge12 } from "tailwind-merge";
1433
- import { useState as useState5, useCallback as useCallback4 } from "react";
1943
+ import { twMerge as twMerge13 } from "tailwind-merge";
1944
+ import { useState as useState5, useCallback as useCallback5 } from "react";
1434
1945
  import { jsx as jsx16, jsxs as jsxs14 } from "react/jsx-runtime";
1435
1946
  function AgentWidget({
1436
1947
  endpoint,
@@ -1440,7 +1951,7 @@ function AgentWidget({
1440
1951
  className
1441
1952
  }) {
1442
1953
  const [isOpen, setIsOpen] = useState5(false);
1443
- const toggle = useCallback4(() => {
1954
+ const toggle = useCallback5(() => {
1444
1955
  setIsOpen((prev) => !prev);
1445
1956
  }, []);
1446
1957
  const positionClasses = position === "bottom-left" ? "left-4 bottom-4" : "right-4 bottom-4";
@@ -1453,7 +1964,7 @@ function AgentWidget({
1453
1964
  role: "dialog",
1454
1965
  "aria-label": title,
1455
1966
  "aria-hidden": !isOpen,
1456
- className: twMerge12(
1967
+ className: twMerge13(
1457
1968
  "fixed z-50 flex flex-col",
1458
1969
  "w-[min(400px,calc(100vw-2rem))] h-[min(600px,calc(100vh-6rem))]",
1459
1970
  "rounded-2xl overflow-hidden border border-brand-gold/15",
@@ -1500,7 +2011,7 @@ function AgentWidget({
1500
2011
  onClick: toggle,
1501
2012
  "aria-label": isOpen ? "Close chat" : triggerLabel,
1502
2013
  "aria-expanded": isOpen,
1503
- className: twMerge12(
2014
+ className: twMerge13(
1504
2015
  "fixed z-50 flex items-center justify-center w-14 h-14 rounded-full",
1505
2016
  "bg-brand-blue text-brand-cream shadow-glow-cyan",
1506
2017
  "hover:bg-brand-cyan hover:shadow-glow-cyan hover:scale-105",
@@ -1518,7 +2029,7 @@ function AgentWidget({
1518
2029
  }
1519
2030
 
1520
2031
  // src/layouts/AgentEmbed/AgentEmbed.tsx
1521
- import { twMerge as twMerge13 } from "tailwind-merge";
2032
+ import { twMerge as twMerge14 } from "tailwind-merge";
1522
2033
  import { jsx as jsx17 } from "react/jsx-runtime";
1523
2034
  function AgentEmbed({
1524
2035
  endpoint,
@@ -1528,7 +2039,7 @@ function AgentEmbed({
1528
2039
  return /* @__PURE__ */ jsx17(
1529
2040
  "div",
1530
2041
  {
1531
- className: twMerge13("w-full h-full min-h-0", className),
2042
+ className: twMerge14("w-full h-full min-h-0", className),
1532
2043
  "data-testid": "agent-embed",
1533
2044
  children: /* @__PURE__ */ jsx17(
1534
2045
  AgentChat,