@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
@@ -39,11 +39,11 @@ __export(layouts_exports, {
39
39
  module.exports = __toCommonJS(layouts_exports);
40
40
 
41
41
  // src/layouts/AgentFullPage/AgentFullPage.tsx
42
- var import_tailwind_merge10 = require("tailwind-merge");
43
- var import_react7 = require("react");
42
+ var import_tailwind_merge11 = require("tailwind-merge");
43
+ var import_react8 = require("react");
44
44
 
45
45
  // src/chat/AgentChat/AgentChat.tsx
46
- var import_tailwind_merge8 = require("tailwind-merge");
46
+ var import_tailwind_merge9 = require("tailwind-merge");
47
47
 
48
48
  // src/hooks/useAgentChat.ts
49
49
  var import_react = require("react");
@@ -54,7 +54,8 @@ var initialState = {
54
54
  error: null,
55
55
  inputValue: "",
56
56
  streamPhase: "idle",
57
- streamingContent: ""
57
+ streamingContent: "",
58
+ streamingAgent: null
58
59
  };
59
60
  function reducer(state, action) {
60
61
  switch (action.type) {
@@ -68,12 +69,17 @@ function reducer(state, action) {
68
69
  error: null,
69
70
  inputValue: "",
70
71
  streamPhase: "thinking",
71
- streamingContent: ""
72
+ streamingContent: "",
73
+ streamingAgent: null
72
74
  };
73
75
  case "STREAM_PHASE":
74
76
  return { ...state, streamPhase: action.phase };
75
77
  case "STREAM_CONTENT":
76
78
  return { ...state, streamingContent: state.streamingContent + action.content };
79
+ case "STREAM_CONTENT_RESET":
80
+ return { ...state, streamingContent: "" };
81
+ case "STREAM_AGENT":
82
+ return { ...state, streamingAgent: action.agent };
77
83
  case "SEND_SUCCESS":
78
84
  return {
79
85
  ...state,
@@ -89,7 +95,8 @@ function reducer(state, action) {
89
95
  isLoading: false,
90
96
  error: action.error,
91
97
  streamPhase: "idle",
92
- streamingContent: ""
98
+ streamingContent: "",
99
+ streamingAgent: null
93
100
  };
94
101
  case "LOAD_CONVERSATION":
95
102
  return {
@@ -115,115 +122,172 @@ function useAgentChat(config) {
115
122
  const configRef = (0, import_react.useRef)(config);
116
123
  configRef.current = config;
117
124
  const lastUserMessageRef = (0, import_react.useRef)(null);
125
+ const lastUserAttachmentsRef = (0, import_react.useRef)(void 0);
126
+ const abortControllerRef = (0, import_react.useRef)(null);
118
127
  const sendMessage = (0, import_react.useCallback)(
119
- async (content) => {
120
- const { apiUrl, streamPath = "/chat/stream", headers = {}, timeout = 3e4 } = configRef.current;
128
+ async (content, attachments) => {
129
+ const { apiUrl, streamPath = "/chat/stream", headers: headersOrFn, timeout = 3e4, bodyExtra } = configRef.current;
130
+ const headers = typeof headersOrFn === "function" ? await headersOrFn() : headersOrFn ?? {};
121
131
  lastUserMessageRef.current = content;
132
+ lastUserAttachmentsRef.current = attachments;
122
133
  const userMessage = {
123
134
  id: generateMessageId(),
124
135
  role: "user",
125
136
  content,
137
+ attachments,
126
138
  timestamp: /* @__PURE__ */ new Date()
127
139
  };
128
140
  dispatch({ type: "SEND_START", message: userMessage });
129
141
  const controller = new AbortController();
142
+ abortControllerRef.current = controller;
130
143
  const timeoutId = setTimeout(() => controller.abort(), timeout);
144
+ const ctx = {
145
+ accumulatedContent: "",
146
+ agentResponse: null,
147
+ capturedAgent: null,
148
+ capturedConversationId: null,
149
+ hadStreamError: false
150
+ };
131
151
  try {
132
- const response = await fetch(`${apiUrl}${streamPath}`, {
133
- method: "POST",
134
- headers: {
135
- "Content-Type": "application/json",
136
- Accept: "text/event-stream",
137
- ...headers
138
- },
139
- body: JSON.stringify({
140
- message: content,
141
- conversation_id: state.conversationId
142
- }),
143
- signal: controller.signal
144
- });
145
- clearTimeout(timeoutId);
146
- if (!response.ok) {
147
- dispatch({
148
- type: "SEND_ERROR",
149
- error: {
150
- code: "API_ERROR",
151
- message: `HTTP ${response.status}: ${response.statusText}`,
152
- retryable: response.status >= 500
153
- }
154
- });
155
- return;
152
+ const url = `${apiUrl}${streamPath}`;
153
+ const mergedHeaders = {
154
+ "Content-Type": "application/json",
155
+ Accept: "text/event-stream",
156
+ ...headers
157
+ };
158
+ const requestBody = {
159
+ message: content,
160
+ conversation_id: state.conversationId,
161
+ ...bodyExtra
162
+ };
163
+ if (attachments && attachments.length > 0) {
164
+ requestBody.attachments = attachments.map((a) => ({
165
+ filename: a.filename,
166
+ content_type: a.content_type,
167
+ data: a.data
168
+ }));
156
169
  }
157
- const reader = response.body?.getReader();
158
- if (!reader) {
159
- dispatch({
160
- type: "SEND_ERROR",
161
- error: { code: "STREAM_ERROR", message: "No response body", retryable: true }
170
+ const body = JSON.stringify(requestBody);
171
+ const handleEvent = (event) => {
172
+ switch (event.type) {
173
+ case "agent":
174
+ ctx.capturedAgent = event.agent;
175
+ dispatch({ type: "STREAM_AGENT", agent: ctx.capturedAgent });
176
+ break;
177
+ case "phase":
178
+ dispatch({ type: "STREAM_PHASE", phase: event.phase });
179
+ break;
180
+ case "delta":
181
+ ctx.accumulatedContent += event.content;
182
+ dispatch({ type: "STREAM_CONTENT", content: event.content });
183
+ break;
184
+ case "delta_reset":
185
+ ctx.accumulatedContent = "";
186
+ dispatch({ type: "STREAM_CONTENT_RESET" });
187
+ break;
188
+ case "done":
189
+ ctx.agentResponse = event.response;
190
+ ctx.capturedConversationId = event.conversation_id ?? null;
191
+ break;
192
+ case "error":
193
+ ctx.hadStreamError = true;
194
+ dispatch({ type: "SEND_ERROR", error: event.error });
195
+ break;
196
+ }
197
+ };
198
+ const { streamAdapter } = configRef.current;
199
+ if (streamAdapter) {
200
+ await streamAdapter(
201
+ url,
202
+ { method: "POST", headers: mergedHeaders, body, signal: controller.signal },
203
+ handleEvent
204
+ );
205
+ clearTimeout(timeoutId);
206
+ } else {
207
+ const response = await fetch(url, {
208
+ method: "POST",
209
+ headers: mergedHeaders,
210
+ body,
211
+ signal: controller.signal
162
212
  });
163
- return;
164
- }
165
- const decoder = new TextDecoder();
166
- let buffer = "";
167
- let accumulatedContent = "";
168
- let agentResponse = null;
169
- let capturedAgent = null;
170
- let capturedConversationId = null;
171
- while (true) {
172
- const { done, value } = await reader.read();
173
- if (done) break;
174
- buffer += decoder.decode(value, { stream: true });
175
- const lines = buffer.split("\n");
176
- buffer = lines.pop() ?? "";
177
- for (const line of lines) {
178
- if (!line.startsWith("data: ")) continue;
179
- const data = line.slice(6).trim();
180
- if (data === "[DONE]") continue;
181
- try {
182
- const event = JSON.parse(data);
183
- switch (event.type) {
184
- case "agent":
185
- capturedAgent = event.agent;
186
- break;
187
- case "phase":
188
- dispatch({ type: "STREAM_PHASE", phase: event.phase });
189
- break;
190
- case "delta":
191
- accumulatedContent += event.content;
192
- dispatch({ type: "STREAM_CONTENT", content: event.content });
193
- break;
194
- case "done":
195
- agentResponse = event.response;
196
- capturedConversationId = event.conversation_id ?? null;
197
- break;
198
- case "error":
199
- dispatch({ type: "SEND_ERROR", error: event.error });
200
- return;
213
+ clearTimeout(timeoutId);
214
+ if (!response.ok) {
215
+ dispatch({
216
+ type: "SEND_ERROR",
217
+ error: {
218
+ code: "API_ERROR",
219
+ message: `HTTP ${response.status}: ${response.statusText}`,
220
+ retryable: response.status >= 500
221
+ }
222
+ });
223
+ return;
224
+ }
225
+ const reader = response.body?.getReader();
226
+ if (!reader) {
227
+ dispatch({
228
+ type: "SEND_ERROR",
229
+ error: { code: "STREAM_ERROR", message: "No response body", retryable: true }
230
+ });
231
+ return;
232
+ }
233
+ const decoder = new TextDecoder();
234
+ let buffer = "";
235
+ while (true) {
236
+ const { done, value } = await reader.read();
237
+ if (done) break;
238
+ buffer += decoder.decode(value, { stream: true });
239
+ const lines = buffer.split("\n");
240
+ buffer = lines.pop() ?? "";
241
+ for (const line of lines) {
242
+ if (!line.startsWith("data: ")) continue;
243
+ const data = line.slice(6).trim();
244
+ if (data === "[DONE]") continue;
245
+ try {
246
+ const event = JSON.parse(data);
247
+ handleEvent(event);
248
+ } catch {
201
249
  }
202
- } catch {
203
250
  }
204
251
  }
205
252
  }
253
+ if (ctx.hadStreamError) return;
206
254
  const assistantMessage = {
207
255
  id: generateMessageId(),
208
256
  role: "assistant",
209
- content: agentResponse?.message ?? accumulatedContent,
210
- response: agentResponse ?? void 0,
211
- agent: capturedAgent ?? void 0,
257
+ content: ctx.agentResponse?.message ?? ctx.accumulatedContent,
258
+ response: ctx.agentResponse ?? void 0,
259
+ agent: ctx.capturedAgent ?? void 0,
212
260
  timestamp: /* @__PURE__ */ new Date()
213
261
  };
214
262
  dispatch({
215
263
  type: "SEND_SUCCESS",
216
264
  message: assistantMessage,
217
- streamingContent: accumulatedContent,
218
- conversationId: capturedConversationId
265
+ streamingContent: ctx.accumulatedContent,
266
+ conversationId: ctx.capturedConversationId
219
267
  });
220
268
  } catch (err) {
221
269
  clearTimeout(timeoutId);
222
270
  if (err.name === "AbortError") {
223
- dispatch({
224
- type: "SEND_ERROR",
225
- error: { code: "TIMEOUT", message: "Request timed out", retryable: true }
226
- });
271
+ if (ctx.accumulatedContent) {
272
+ const partialMessage = {
273
+ id: generateMessageId(),
274
+ role: "assistant",
275
+ content: ctx.accumulatedContent,
276
+ agent: ctx.capturedAgent ?? void 0,
277
+ timestamp: /* @__PURE__ */ new Date()
278
+ };
279
+ dispatch({
280
+ type: "SEND_SUCCESS",
281
+ message: partialMessage,
282
+ streamingContent: ctx.accumulatedContent,
283
+ conversationId: ctx.capturedConversationId
284
+ });
285
+ } else {
286
+ dispatch({
287
+ type: "SEND_ERROR",
288
+ error: { code: "ABORTED", message: "Request stopped", retryable: true }
289
+ });
290
+ }
227
291
  } else {
228
292
  dispatch({
229
293
  type: "SEND_ERROR",
@@ -234,6 +298,8 @@ function useAgentChat(config) {
234
298
  }
235
299
  });
236
300
  }
301
+ } finally {
302
+ abortControllerRef.current = null;
237
303
  }
238
304
  },
239
305
  [state.conversationId]
@@ -246,7 +312,8 @@ function useAgentChat(config) {
246
312
  }, []);
247
313
  const submitFeedback = (0, import_react.useCallback)(
248
314
  async (messageId, rating, comment) => {
249
- const { apiUrl, feedbackPath = "/feedback", headers = {} } = configRef.current;
315
+ const { apiUrl, feedbackPath = "/feedback", headers: headersOrFn } = configRef.current;
316
+ const headers = typeof headersOrFn === "function" ? await headersOrFn() : headersOrFn ?? {};
250
317
  await fetch(`${apiUrl}${feedbackPath}`, {
251
318
  method: "POST",
252
319
  headers: { "Content-Type": "application/json", ...headers },
@@ -257,12 +324,16 @@ function useAgentChat(config) {
257
324
  );
258
325
  const retry = (0, import_react.useCallback)(async () => {
259
326
  if (lastUserMessageRef.current) {
260
- await sendMessage(lastUserMessageRef.current);
327
+ await sendMessage(lastUserMessageRef.current, lastUserAttachmentsRef.current);
261
328
  }
262
329
  }, [sendMessage]);
330
+ const stop = (0, import_react.useCallback)(() => {
331
+ abortControllerRef.current?.abort();
332
+ }, []);
263
333
  const reset = (0, import_react.useCallback)(() => {
264
334
  dispatch({ type: "RESET" });
265
335
  lastUserMessageRef.current = null;
336
+ lastUserAttachmentsRef.current = void 0;
266
337
  }, []);
267
338
  const actions = {
268
339
  sendMessage,
@@ -270,6 +341,7 @@ function useAgentChat(config) {
270
341
  loadConversation,
271
342
  submitFeedback,
272
343
  retry,
344
+ stop,
273
345
  reset
274
346
  };
275
347
  return { state, actions };
@@ -277,7 +349,7 @@ function useAgentChat(config) {
277
349
 
278
350
  // src/chat/MessageThread/MessageThread.tsx
279
351
  var import_tailwind_merge5 = require("tailwind-merge");
280
- var import_react3 = require("react");
352
+ var import_react4 = require("react");
281
353
 
282
354
  // src/chat/MessageBubble/MessageBubble.tsx
283
355
  var import_tailwind_merge4 = require("tailwind-merge");
@@ -286,8 +358,10 @@ var import_tailwind_merge4 = require("tailwind-merge");
286
358
  var import_core2 = require("@surf-kit/core");
287
359
 
288
360
  // src/response/ResponseMessage/ResponseMessage.tsx
361
+ var import_react2 = __toESM(require("react"), 1);
289
362
  var import_react_markdown = __toESM(require("react-markdown"), 1);
290
363
  var import_rehype_sanitize = __toESM(require("rehype-sanitize"), 1);
364
+ var import_remark_gfm = __toESM(require("remark-gfm"), 1);
291
365
  var import_tailwind_merge = require("tailwind-merge");
292
366
  var import_jsx_runtime = require("react/jsx-runtime");
293
367
  function normalizeMarkdownLists(content) {
@@ -310,7 +384,12 @@ function ResponseMessage({ content, className }) {
310
384
  "[&_h3]:text-sm [&_h3]:font-semibold [&_h3]:text-accent [&_h3]:mt-2 [&_h3]:mb-1",
311
385
  "[&_code]:bg-surface-raised [&_code]:text-accent [&_code]:px-1.5 [&_code]:py-0.5 [&_code]:rounded [&_code]:text-xs [&_code]:font-mono",
312
386
  "[&_pre]:bg-surface-raised [&_pre]:border [&_pre]:border-border [&_pre]:rounded-xl [&_pre]:p-4 [&_pre]:overflow-x-auto",
387
+ "[&_hr]:my-3 [&_hr]:border-border",
313
388
  "[&_blockquote]:border-l-2 [&_blockquote]:border-border-strong [&_blockquote]:pl-4 [&_blockquote]:text-text-secondary",
389
+ "[&_table]:w-full [&_table]:text-sm [&_table]:border-collapse [&_table]:my-2",
390
+ "[&_thead]:border-b [&_thead]:border-border",
391
+ "[&_th]:text-left [&_th]:px-2 [&_th]:py-1.5 [&_th]:font-semibold",
392
+ "[&_td]:px-2 [&_td]:py-1.5 [&_td]:border-t [&_td]:border-border/50",
314
393
  "[&_a]:text-accent [&_a]:underline-offset-2 [&_a]:hover:text-accent/80",
315
394
  className
316
395
  ),
@@ -318,6 +397,7 @@ function ResponseMessage({ content, className }) {
318
397
  children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
319
398
  import_react_markdown.default,
320
399
  {
400
+ remarkPlugins: [import_remark_gfm.default],
321
401
  rehypePlugins: [import_rehype_sanitize.default],
322
402
  components: {
323
403
  script: () => null,
@@ -325,12 +405,29 @@ function ResponseMessage({ content, className }) {
325
405
  p: ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "my-2", children }),
326
406
  ul: ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ul", { className: "my-2 list-disc pl-6", children }),
327
407
  ol: ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ol", { className: "my-2 list-decimal pl-6", children }),
328
- li: ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("li", { className: "my-1", children }),
408
+ li: ({ children, ...props }) => {
409
+ let content2 = children;
410
+ if (props.ordered) {
411
+ content2 = import_react2.default.Children.map(children, (child, i) => {
412
+ if (i === 0 && typeof child === "string") {
413
+ return child.replace(/^\d+[.)]\s*/, "");
414
+ }
415
+ return child;
416
+ });
417
+ }
418
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("li", { className: "my-1", children: content2 });
419
+ },
329
420
  strong: ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { className: "font-semibold", children }),
421
+ em: ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("em", { className: "italic text-text-secondary", children }),
330
422
  h1: ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h1", { className: "text-base font-bold mt-4 mb-2", children }),
331
423
  h2: ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", { className: "text-sm font-bold mt-3 mb-1", children }),
332
424
  h3: ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h3", { className: "text-sm font-semibold mt-2 mb-1", children }),
333
- code: ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("code", { className: "bg-surface-sunken rounded px-1 py-0.5 text-xs font-mono", children })
425
+ hr: () => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("hr", { className: "my-3 border-border" }),
426
+ code: ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("code", { className: "bg-surface-sunken rounded px-1 py-0.5 text-xs font-mono", children }),
427
+ table: ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "overflow-x-auto my-2", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("table", { className: "w-full text-sm border-collapse", children }) }),
428
+ thead: ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("thead", { className: "border-b border-border", children }),
429
+ th: ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("th", { className: "text-left px-2 py-1.5 font-semibold", children }),
430
+ td: ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", { className: "px-2 py-1.5 border-t border-border/50", children })
334
431
  },
335
432
  children: normalizeMarkdownLists(content)
336
433
  }
@@ -340,6 +437,8 @@ function ResponseMessage({ content, className }) {
340
437
  }
341
438
 
342
439
  // src/response/StructuredResponse/StructuredResponse.tsx
440
+ var import_react_markdown2 = __toESM(require("react-markdown"), 1);
441
+ var import_rehype_sanitize2 = __toESM(require("rehype-sanitize"), 1);
343
442
  var import_jsx_runtime2 = require("react/jsx-runtime");
344
443
  function tryParse(value) {
345
444
  if (value === void 0 || value === null) return null;
@@ -352,6 +451,25 @@ function tryParse(value) {
352
451
  }
353
452
  return value;
354
453
  }
454
+ function InlineMarkdown({ text }) {
455
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
456
+ import_react_markdown2.default,
457
+ {
458
+ rehypePlugins: [import_rehype_sanitize2.default],
459
+ components: {
460
+ // Unwrap block-level <p> so content stays inline within its parent
461
+ p: ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_jsx_runtime2.Fragment, { children }),
462
+ strong: ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("strong", { className: "font-semibold", children }),
463
+ em: ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("em", { className: "italic", children }),
464
+ code: ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("code", { className: "bg-surface-sunken rounded px-1 py-0.5 text-xs font-mono", children }),
465
+ // Prevent block elements that would break layout
466
+ script: () => null,
467
+ iframe: () => null
468
+ },
469
+ children: text
470
+ }
471
+ );
472
+ }
355
473
  function renderSteps(data) {
356
474
  const steps = tryParse(data.steps);
357
475
  if (!steps || !Array.isArray(steps)) return null;
@@ -364,7 +482,7 @@ function renderSteps(data) {
364
482
  children: i + 1
365
483
  }
366
484
  ),
367
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "text-sm text-text-primary leading-relaxed", children: step })
485
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "text-sm text-text-primary leading-relaxed", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(InlineMarkdown, { text: step }) })
368
486
  ] }, i)) });
369
487
  }
370
488
  function renderTable(data) {
@@ -430,7 +548,7 @@ function renderList(data) {
430
548
  title && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "text-xs font-semibold uppercase tracking-wider text-text-secondary mb-1", children: title }),
431
549
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("ul", { className: "flex flex-col gap-1.5", children: items.map((item, i) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("li", { className: "flex items-start gap-2.5", children: [
432
550
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "mt-1.5 h-1.5 w-1.5 shrink-0 rounded-full bg-accent", "aria-hidden": "true" }),
433
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "text-sm text-text-primary leading-relaxed", children: item })
551
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "text-sm text-text-primary leading-relaxed", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(InlineMarkdown, { text: item }) })
434
552
  ] }, i)) })
435
553
  ] });
436
554
  }
@@ -465,7 +583,14 @@ function renderWarning(data) {
465
583
  }
466
584
  );
467
585
  }
468
- function StructuredResponse({ uiHint, data, className }) {
586
+ function StructuredResponse({ uiHint, data: rawData, className }) {
587
+ const data = typeof rawData === "string" ? (() => {
588
+ try {
589
+ return JSON.parse(rawData);
590
+ } catch {
591
+ return null;
592
+ }
593
+ })() : rawData;
469
594
  if (!data) return null;
470
595
  let content;
471
596
  switch (uiHint) {
@@ -495,7 +620,7 @@ function StructuredResponse({ uiHint, data, className }) {
495
620
  }
496
621
 
497
622
  // src/sources/SourceList/SourceList.tsx
498
- var import_react2 = require("react");
623
+ var import_react3 = require("react");
499
624
 
500
625
  // src/sources/SourceCard/SourceCard.tsx
501
626
  var import_core = require("@surf-kit/core");
@@ -545,7 +670,36 @@ function SourceCard({ source, variant = "compact", onNavigate, className }) {
545
670
  children: [
546
671
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "flex items-start justify-between gap-2", children: [
547
672
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "flex-1 min-w-0", children: [
548
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "text-sm font-medium text-text-primary truncate", children: source.title }),
673
+ source.url ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
674
+ "a",
675
+ {
676
+ href: source.url,
677
+ target: "_blank",
678
+ rel: "noopener noreferrer",
679
+ className: "text-sm font-medium text-accent hover:underline truncate block",
680
+ onClick: (e) => e.stopPropagation(),
681
+ children: [
682
+ source.title,
683
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
684
+ "svg",
685
+ {
686
+ className: "inline-block ml-1 w-3 h-3 opacity-60",
687
+ viewBox: "0 0 24 24",
688
+ fill: "none",
689
+ stroke: "currentColor",
690
+ strokeWidth: "2",
691
+ strokeLinecap: "round",
692
+ strokeLinejoin: "round",
693
+ children: [
694
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("path", { d: "M18 13v6a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h6" }),
695
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("polyline", { points: "15 3 21 3 21 9" }),
696
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("line", { x1: "10", y1: "14", x2: "21", y2: "3" })
697
+ ]
698
+ }
699
+ )
700
+ ]
701
+ }
702
+ ) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "text-sm font-medium text-text-primary truncate", children: source.title }),
549
703
  source.section && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "text-[11px] font-semibold uppercase tracking-wider text-text-secondary truncate mt-0.5", children: source.section })
550
704
  ] }),
551
705
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
@@ -575,7 +729,7 @@ function SourceList({
575
729
  onNavigate,
576
730
  className
577
731
  }) {
578
- const [isExpanded, setIsExpanded] = (0, import_react2.useState)(defaultExpanded);
732
+ const [isExpanded, setIsExpanded] = (0, import_react3.useState)(defaultExpanded);
579
733
  if (sources.length === 0) return null;
580
734
  const content = /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "flex flex-col gap-1.5", "data-testid": "source-list-items", children: sources.map((source) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
581
735
  SourceCard,
@@ -679,13 +833,16 @@ function AgentResponse({
679
833
  }) {
680
834
  return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: `flex flex-col gap-4 ${className ?? ""}`, "data-testid": "agent-response", children: [
681
835
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(ResponseMessage, { content: response.message }),
682
- response.ui_hint !== "text" && response.structured_data && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
683
- StructuredResponse,
684
- {
685
- uiHint: response.ui_hint,
686
- data: response.structured_data
687
- }
688
- ),
836
+ response.ui_hint !== "text" && response.structured_data && (() => {
837
+ const parsed = typeof response.structured_data === "string" ? (() => {
838
+ try {
839
+ return JSON.parse(response.structured_data);
840
+ } catch {
841
+ return null;
842
+ }
843
+ })() : response.structured_data;
844
+ return parsed ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(StructuredResponse, { uiHint: response.ui_hint, data: parsed }) : null;
845
+ })(),
689
846
  (showConfidence || showVerification) && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "flex flex-wrap items-center gap-2 mt-1", "data-testid": "response-meta", children: [
690
847
  showConfidence && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
691
848
  import_core2.Badge,
@@ -736,6 +893,31 @@ function AgentResponse({
736
893
 
737
894
  // src/chat/MessageBubble/MessageBubble.tsx
738
895
  var import_jsx_runtime7 = require("react/jsx-runtime");
896
+ function DocumentIcon() {
897
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round", children: [
898
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("path", { d: "M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z" }),
899
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("polyline", { points: "14 2 14 8 20 8" }),
900
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("line", { x1: "16", y1: "13", x2: "8", y2: "13" }),
901
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("line", { x1: "16", y1: "17", x2: "8", y2: "17" })
902
+ ] });
903
+ }
904
+ function AttachmentThumbnail({ attachment }) {
905
+ const isImage = attachment.content_type.startsWith("image/");
906
+ if (isImage) {
907
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "rounded-lg overflow-hidden border border-black/10 max-w-[240px]", children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
908
+ "img",
909
+ {
910
+ src: attachment.preview_url ?? `data:${attachment.content_type};base64,${attachment.data}`,
911
+ alt: attachment.filename,
912
+ className: "max-w-full max-h-[200px] object-contain"
913
+ }
914
+ ) });
915
+ }
916
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "flex items-center gap-2 px-3 py-2 rounded-lg border border-black/10 bg-black/5", children: [
917
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(DocumentIcon, {}),
918
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "text-xs truncate max-w-[160px]", children: attachment.filename })
919
+ ] });
920
+ }
739
921
  function MessageBubble({
740
922
  message,
741
923
  showAgent,
@@ -743,23 +925,29 @@ function MessageBubble({
743
925
  showConfidence = true,
744
926
  showVerification = true,
745
927
  animated = true,
928
+ userBubbleClassName,
746
929
  className
747
930
  }) {
748
931
  const isUser = message.role === "user";
932
+ const hasAttachments = message.attachments && message.attachments.length > 0;
749
933
  if (isUser) {
750
934
  return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
751
935
  "div",
752
936
  {
753
937
  "data-message-id": message.id,
754
938
  className: (0, import_tailwind_merge4.twMerge)("flex w-full justify-end", className),
755
- children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
939
+ children: /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
756
940
  "div",
757
941
  {
758
942
  className: (0, import_tailwind_merge4.twMerge)(
759
- "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",
760
- animated && "motion-safe:animate-slideFromRight"
943
+ "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",
944
+ animated && "motion-safe:animate-slideFromRight",
945
+ userBubbleClassName
761
946
  ),
762
- children: message.content
947
+ children: [
948
+ hasAttachments && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "flex flex-wrap gap-2 mb-2", children: message.attachments.map((att, i) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(AttachmentThumbnail, { attachment: att }, `${att.filename}-${i}`)) }),
949
+ message.content
950
+ ]
763
951
  }
764
952
  )
765
953
  }
@@ -771,7 +959,7 @@ function MessageBubble({
771
959
  "data-message-id": message.id,
772
960
  className: (0, import_tailwind_merge4.twMerge)("flex w-full flex-col items-start gap-1.5", className),
773
961
  children: [
774
- showAgent && message.agent && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "text-[11px] font-semibold uppercase tracking-[0.08em] text-text-muted px-1", children: message.agent.replace("_agent", "").replace("_", " ") }),
962
+ showAgent && message.agent && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "text-[11px] font-display font-semibold uppercase tracking-[0.08em] text-text-muted px-1", children: message.agent.replace("_agent", "").replace("_", " ") }),
775
963
  /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
776
964
  "div",
777
965
  {
@@ -797,34 +985,97 @@ function MessageBubble({
797
985
 
798
986
  // src/chat/MessageThread/MessageThread.tsx
799
987
  var import_jsx_runtime8 = require("react/jsx-runtime");
800
- function MessageThread({ messages, streamingSlot, showSources, showConfidence, showVerification, className }) {
801
- const bottomRef = (0, import_react3.useRef)(null);
802
- (0, import_react3.useEffect)(() => {
803
- bottomRef.current?.scrollIntoView?.({ behavior: "smooth" });
804
- }, [messages.length, streamingSlot]);
988
+ function MessageThread({ messages, streamingSlot, showAgent, showSources, showConfidence, showVerification, hideLastAssistant, userBubbleClassName, className }) {
989
+ const scrollRef = (0, import_react4.useRef)(null);
990
+ const shouldAutoScroll = (0, import_react4.useRef)(true);
991
+ const hasStreaming = !!streamingSlot;
992
+ const scrollToBottom = (0, import_react4.useCallback)(() => {
993
+ const el = scrollRef.current;
994
+ if (el && shouldAutoScroll.current) {
995
+ el.scrollTop = el.scrollHeight;
996
+ }
997
+ }, []);
998
+ (0, import_react4.useEffect)(() => {
999
+ const el = scrollRef.current;
1000
+ if (!el) return;
1001
+ const onWheel = (e) => {
1002
+ if (e.deltaY < 0) {
1003
+ shouldAutoScroll.current = false;
1004
+ }
1005
+ };
1006
+ const onPointerDown = () => {
1007
+ el.dataset.userPointer = "1";
1008
+ };
1009
+ const onPointerUp = () => {
1010
+ delete el.dataset.userPointer;
1011
+ };
1012
+ el.addEventListener("wheel", onWheel, { passive: true });
1013
+ el.addEventListener("pointerdown", onPointerDown);
1014
+ window.addEventListener("pointerup", onPointerUp);
1015
+ return () => {
1016
+ el.removeEventListener("wheel", onWheel);
1017
+ el.removeEventListener("pointerdown", onPointerDown);
1018
+ window.removeEventListener("pointerup", onPointerUp);
1019
+ };
1020
+ }, []);
1021
+ const handleScroll = (0, import_react4.useCallback)(() => {
1022
+ const el = scrollRef.current;
1023
+ if (!el) return;
1024
+ const nearBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 80;
1025
+ if (nearBottom) {
1026
+ shouldAutoScroll.current = true;
1027
+ } else if (el.dataset.userPointer) {
1028
+ shouldAutoScroll.current = false;
1029
+ }
1030
+ }, []);
1031
+ (0, import_react4.useEffect)(scrollToBottom, [messages.length, scrollToBottom]);
1032
+ (0, import_react4.useEffect)(() => {
1033
+ if (!hasStreaming) return;
1034
+ let raf;
1035
+ const tick = () => {
1036
+ scrollToBottom();
1037
+ raf = requestAnimationFrame(tick);
1038
+ };
1039
+ raf = requestAnimationFrame(tick);
1040
+ return () => cancelAnimationFrame(raf);
1041
+ }, [hasStreaming, scrollToBottom]);
1042
+ (0, import_react4.useEffect)(() => {
1043
+ if (!hasStreaming) {
1044
+ shouldAutoScroll.current = true;
1045
+ }
1046
+ }, [hasStreaming]);
805
1047
  return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
806
1048
  "div",
807
1049
  {
1050
+ ref: scrollRef,
808
1051
  role: "log",
809
1052
  "aria-live": "polite",
810
1053
  "aria-label": "Message thread",
1054
+ onScroll: handleScroll,
811
1055
  className: (0, import_tailwind_merge5.twMerge)(
812
1056
  "flex flex-col gap-4 overflow-y-auto flex-1 px-4 py-6",
813
1057
  className
814
1058
  ),
815
1059
  children: [
816
- messages.map((message) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
817
- MessageBubble,
818
- {
819
- message,
820
- showSources,
821
- showConfidence,
822
- showVerification
823
- },
824
- message.id
825
- )),
826
- streamingSlot,
827
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { ref: bottomRef })
1060
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "flex-1 shrink-0" }),
1061
+ messages.map((message, i) => {
1062
+ if (hideLastAssistant && i === messages.length - 1 && message.role === "assistant") {
1063
+ return null;
1064
+ }
1065
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1066
+ MessageBubble,
1067
+ {
1068
+ message,
1069
+ showAgent,
1070
+ showSources,
1071
+ showConfidence,
1072
+ showVerification,
1073
+ userBubbleClassName
1074
+ },
1075
+ message.id
1076
+ );
1077
+ }),
1078
+ streamingSlot
828
1079
  ]
829
1080
  }
830
1081
  );
@@ -832,32 +1083,127 @@ function MessageThread({ messages, streamingSlot, showSources, showConfidence, s
832
1083
 
833
1084
  // src/chat/MessageComposer/MessageComposer.tsx
834
1085
  var import_tailwind_merge6 = require("tailwind-merge");
835
- var import_react4 = require("react");
1086
+ var import_react5 = require("react");
836
1087
  var import_jsx_runtime9 = require("react/jsx-runtime");
1088
+ var ALLOWED_TYPES = /* @__PURE__ */ new Set([
1089
+ "image/png",
1090
+ "image/jpeg",
1091
+ "image/gif",
1092
+ "image/webp",
1093
+ "application/pdf"
1094
+ ]);
1095
+ var MAX_FILE_SIZE = 10 * 1024 * 1024;
1096
+ var MAX_ATTACHMENTS = 5;
1097
+ function ArrowUpIcon() {
1098
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("svg", { width: "20", height: "20", viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round", children: [
1099
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("path", { d: "M10 16V4" }),
1100
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("path", { d: "M4 10l6-6 6 6" })
1101
+ ] });
1102
+ }
1103
+ function StopIcon() {
1104
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "currentColor", children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("rect", { x: "3", y: "3", width: "10", height: "10", rx: "2" }) });
1105
+ }
1106
+ function PaperclipIcon() {
1107
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("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" }) });
1108
+ }
1109
+ function XIcon({ size = 14 }) {
1110
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round", children: [
1111
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("path", { d: "M18 6L6 18" }),
1112
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("path", { d: "M6 6l12 12" })
1113
+ ] });
1114
+ }
1115
+ function DocumentIcon2() {
1116
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round", children: [
1117
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("path", { d: "M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z" }),
1118
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("polyline", { points: "14 2 14 8 20 8" }),
1119
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("line", { x1: "16", y1: "13", x2: "8", y2: "13" }),
1120
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("line", { x1: "16", y1: "17", x2: "8", y2: "17" }),
1121
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("polyline", { points: "10 9 9 9 8 9" })
1122
+ ] });
1123
+ }
1124
+ function fileToBase64(file) {
1125
+ return new Promise((resolve, reject) => {
1126
+ const reader = new FileReader();
1127
+ reader.onload = () => {
1128
+ const result = reader.result;
1129
+ const base64 = result.split(",")[1];
1130
+ resolve(base64);
1131
+ };
1132
+ reader.onerror = reject;
1133
+ reader.readAsDataURL(file);
1134
+ });
1135
+ }
1136
+ function AttachmentPreview({
1137
+ attachment,
1138
+ onRemove
1139
+ }) {
1140
+ const isImage = attachment.content_type.startsWith("image/");
1141
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "relative group flex-shrink-0", children: [
1142
+ isImage ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { className: "w-16 h-16 rounded-lg overflow-hidden border border-border/60 bg-surface-alt", children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
1143
+ "img",
1144
+ {
1145
+ src: attachment.preview_url ?? `data:${attachment.content_type};base64,${attachment.data}`,
1146
+ alt: attachment.filename,
1147
+ className: "w-full h-full object-cover"
1148
+ }
1149
+ ) }) : /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "h-16 px-3 rounded-lg border border-border/60 bg-surface-alt flex items-center gap-2", children: [
1150
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { className: "text-text-muted", children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(DocumentIcon2, {}) }),
1151
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "flex flex-col min-w-0", children: [
1152
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: "text-xs text-text-primary truncate max-w-[120px]", children: attachment.filename }),
1153
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: "text-[10px] text-text-muted", children: "PDF" })
1154
+ ] })
1155
+ ] }),
1156
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
1157
+ "button",
1158
+ {
1159
+ type: "button",
1160
+ onClick: onRemove,
1161
+ className: (0, import_tailwind_merge6.twMerge)(
1162
+ "absolute -top-1.5 -right-1.5",
1163
+ "w-5 h-5 rounded-full",
1164
+ "bg-text-muted/80 text-white",
1165
+ "flex items-center justify-center",
1166
+ "opacity-0 group-hover:opacity-100",
1167
+ "transition-opacity duration-150",
1168
+ "hover:bg-text-primary"
1169
+ ),
1170
+ "aria-label": `Remove ${attachment.filename}`,
1171
+ children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(XIcon, { size: 10 })
1172
+ }
1173
+ )
1174
+ ] });
1175
+ }
837
1176
  function MessageComposer({
838
1177
  onSend,
1178
+ onStop,
839
1179
  isLoading = false,
840
1180
  placeholder = "Type a message...",
841
1181
  className
842
1182
  }) {
843
- const [value, setValue] = (0, import_react4.useState)("");
844
- const textareaRef = (0, import_react4.useRef)(null);
845
- const canSend = value.trim().length > 0 && !isLoading;
846
- const resetHeight = (0, import_react4.useCallback)(() => {
1183
+ const [value, setValue] = (0, import_react5.useState)("");
1184
+ const [attachments, setAttachments] = (0, import_react5.useState)([]);
1185
+ const [dragOver, setDragOver] = (0, import_react5.useState)(false);
1186
+ const textareaRef = (0, import_react5.useRef)(null);
1187
+ const fileInputRef = (0, import_react5.useRef)(null);
1188
+ const canSend = (value.trim().length > 0 || attachments.length > 0) && !isLoading;
1189
+ const resetHeight = (0, import_react5.useCallback)(() => {
847
1190
  const el = textareaRef.current;
848
1191
  if (el) {
849
1192
  el.style.height = "auto";
850
1193
  el.style.overflowY = "hidden";
851
1194
  }
852
1195
  }, []);
853
- const handleSend = (0, import_react4.useCallback)(() => {
1196
+ const handleSend = (0, import_react5.useCallback)(() => {
854
1197
  if (!canSend) return;
855
- onSend(value.trim());
1198
+ const message = value.trim() || (attachments.length > 0 ? "Please analyse the attached file(s)." : "");
1199
+ if (!message && attachments.length === 0) return;
1200
+ onSend(message, attachments.length > 0 ? attachments : void 0);
856
1201
  setValue("");
1202
+ setAttachments([]);
857
1203
  resetHeight();
858
1204
  textareaRef.current?.focus();
859
- }, [canSend, onSend, value, resetHeight]);
860
- const handleKeyDown = (0, import_react4.useCallback)(
1205
+ }, [canSend, onSend, value, attachments, resetHeight]);
1206
+ const handleKeyDown = (0, import_react5.useCallback)(
861
1207
  (e) => {
862
1208
  if (e.key === "Enter" && !e.shiftKey) {
863
1209
  e.preventDefault();
@@ -866,64 +1212,194 @@ function MessageComposer({
866
1212
  },
867
1213
  [handleSend]
868
1214
  );
869
- const handleChange = (0, import_react4.useCallback)(
1215
+ const handleChange = (0, import_react5.useCallback)(
870
1216
  (e) => {
871
1217
  setValue(e.target.value);
872
1218
  const el = e.target;
873
1219
  el.style.height = "auto";
874
- const capped = Math.min(el.scrollHeight, 128);
1220
+ const capped = Math.min(el.scrollHeight, 200);
875
1221
  el.style.height = `${capped}px`;
876
- el.style.overflowY = el.scrollHeight > 128 ? "auto" : "hidden";
1222
+ el.style.overflowY = el.scrollHeight > 200 ? "auto" : "hidden";
877
1223
  },
878
1224
  []
879
1225
  );
1226
+ const addFiles = (0, import_react5.useCallback)(async (files) => {
1227
+ const fileArray = Array.from(files);
1228
+ for (const file of fileArray) {
1229
+ if (attachments.length >= MAX_ATTACHMENTS) break;
1230
+ if (!ALLOWED_TYPES.has(file.type)) continue;
1231
+ if (file.size > MAX_FILE_SIZE) continue;
1232
+ try {
1233
+ const data = await fileToBase64(file);
1234
+ const previewUrl = file.type.startsWith("image/") ? URL.createObjectURL(file) : void 0;
1235
+ const attachment = {
1236
+ filename: file.name,
1237
+ content_type: file.type,
1238
+ data,
1239
+ preview_url: previewUrl
1240
+ };
1241
+ setAttachments((prev) => {
1242
+ if (prev.length >= MAX_ATTACHMENTS) return prev;
1243
+ return [...prev, attachment];
1244
+ });
1245
+ } catch {
1246
+ }
1247
+ }
1248
+ }, [attachments.length]);
1249
+ const handleFileSelect = (0, import_react5.useCallback)(() => {
1250
+ fileInputRef.current?.click();
1251
+ }, []);
1252
+ const handleFileInputChange = (0, import_react5.useCallback)(
1253
+ (e) => {
1254
+ if (e.target.files) {
1255
+ void addFiles(e.target.files);
1256
+ e.target.value = "";
1257
+ }
1258
+ },
1259
+ [addFiles]
1260
+ );
1261
+ const removeAttachment = (0, import_react5.useCallback)((index) => {
1262
+ setAttachments((prev) => {
1263
+ const removed = prev[index];
1264
+ if (removed?.preview_url) URL.revokeObjectURL(removed.preview_url);
1265
+ return prev.filter((_, i) => i !== index);
1266
+ });
1267
+ }, []);
1268
+ const handlePaste = (0, import_react5.useCallback)(
1269
+ (e) => {
1270
+ const items = e.clipboardData.items;
1271
+ const files = [];
1272
+ for (const item of items) {
1273
+ if (item.kind === "file" && ALLOWED_TYPES.has(item.type)) {
1274
+ const file = item.getAsFile();
1275
+ if (file) files.push(file);
1276
+ }
1277
+ }
1278
+ if (files.length > 0) {
1279
+ void addFiles(files);
1280
+ }
1281
+ },
1282
+ [addFiles]
1283
+ );
1284
+ const handleDragOver = (0, import_react5.useCallback)((e) => {
1285
+ e.preventDefault();
1286
+ e.stopPropagation();
1287
+ setDragOver(true);
1288
+ }, []);
1289
+ const handleDragLeave = (0, import_react5.useCallback)((e) => {
1290
+ e.preventDefault();
1291
+ e.stopPropagation();
1292
+ setDragOver(false);
1293
+ }, []);
1294
+ const handleDrop = (0, import_react5.useCallback)(
1295
+ (e) => {
1296
+ e.preventDefault();
1297
+ e.stopPropagation();
1298
+ setDragOver(false);
1299
+ if (e.dataTransfer.files.length > 0) {
1300
+ void addFiles(e.dataTransfer.files);
1301
+ }
1302
+ },
1303
+ [addFiles]
1304
+ );
880
1305
  return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(
881
1306
  "div",
882
1307
  {
883
1308
  className: (0, import_tailwind_merge6.twMerge)(
884
- "flex items-end gap-3 shrink-0 border-t border-border px-4 py-3",
1309
+ "relative shrink-0 rounded-3xl border bg-surface",
1310
+ "shadow-lg shadow-black/10",
1311
+ "transition-all duration-200",
1312
+ "focus-within:border-accent/40 focus-within:shadow-accent/5",
1313
+ dragOver ? "border-accent/60 bg-accent/5" : "border-border/60",
885
1314
  className
886
1315
  ),
1316
+ onDragOver: handleDragOver,
1317
+ onDragLeave: handleDragLeave,
1318
+ onDrop: handleDrop,
887
1319
  children: [
888
1320
  /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
889
- "textarea",
1321
+ "input",
890
1322
  {
891
- ref: textareaRef,
892
- value,
893
- onChange: handleChange,
894
- onKeyDown: handleKeyDown,
895
- placeholder,
896
- rows: 1,
897
- disabled: isLoading,
898
- className: (0, import_tailwind_merge6.twMerge)(
899
- "flex-1 resize-none rounded-xl border border-border bg-surface/80",
900
- "px-4 py-2.5 text-sm text-text-primary placeholder:text-text-muted",
901
- "focus:border-transparent focus:ring-2 focus:ring-accent/40 focus:outline-none",
902
- "disabled:opacity-50 disabled:cursor-not-allowed",
903
- "overflow-hidden",
904
- "transition-all duration-200"
905
- ),
906
- style: { colorScheme: "dark" },
907
- "aria-label": "Message input"
1323
+ ref: fileInputRef,
1324
+ type: "file",
1325
+ multiple: true,
1326
+ accept: "image/png,image/jpeg,image/gif,image/webp,application/pdf",
1327
+ onChange: handleFileInputChange,
1328
+ className: "hidden",
1329
+ "aria-hidden": "true"
908
1330
  }
909
1331
  ),
910
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
911
- "button",
1332
+ attachments.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { className: "flex gap-2 px-4 pt-3 pb-1 overflow-x-auto", children: attachments.map((att, i) => /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
1333
+ AttachmentPreview,
912
1334
  {
913
- type: "button",
914
- onClick: handleSend,
915
- disabled: !value.trim() || isLoading,
916
- "aria-label": "Send message",
917
- className: (0, import_tailwind_merge6.twMerge)(
918
- "inline-flex items-center justify-center rounded-xl px-5 py-2.5",
919
- "text-sm font-semibold text-white shrink-0",
920
- "transition-all duration-200",
921
- "focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent",
922
- 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"
923
- ),
924
- children: "Send"
925
- }
926
- )
1335
+ attachment: att,
1336
+ onRemove: () => removeAttachment(i)
1337
+ },
1338
+ `${att.filename}-${i}`
1339
+ )) }),
1340
+ dragOver && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("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__ */ (0, import_jsx_runtime9.jsx)("span", { className: "text-sm font-display font-semibold text-accent", children: "Drop files here" }) }),
1341
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "flex items-end", children: [
1342
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
1343
+ "button",
1344
+ {
1345
+ type: "button",
1346
+ onClick: handleFileSelect,
1347
+ disabled: isLoading || attachments.length >= MAX_ATTACHMENTS,
1348
+ "aria-label": "Attach file",
1349
+ className: (0, import_tailwind_merge6.twMerge)(
1350
+ "flex-shrink-0 ml-2 mb-3",
1351
+ "inline-flex items-center justify-center",
1352
+ "w-9 h-9 rounded-full",
1353
+ "transition-all duration-200",
1354
+ "text-text-muted/60 hover:text-text-secondary hover:bg-text-muted/10",
1355
+ "focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent",
1356
+ "disabled:opacity-30 disabled:cursor-not-allowed disabled:hover:bg-transparent"
1357
+ ),
1358
+ children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(PaperclipIcon, {})
1359
+ }
1360
+ ),
1361
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
1362
+ "textarea",
1363
+ {
1364
+ ref: textareaRef,
1365
+ value,
1366
+ onChange: handleChange,
1367
+ onKeyDown: handleKeyDown,
1368
+ onPaste: handlePaste,
1369
+ placeholder,
1370
+ rows: 1,
1371
+ disabled: isLoading,
1372
+ className: (0, import_tailwind_merge6.twMerge)(
1373
+ "flex-1 resize-none bg-transparent",
1374
+ "pl-2 pr-14 pt-4 pb-4 text-[15px] leading-relaxed",
1375
+ "text-text-primary placeholder:text-text-muted/70",
1376
+ "focus:outline-none",
1377
+ "disabled:opacity-50 disabled:cursor-not-allowed",
1378
+ "overflow-hidden"
1379
+ ),
1380
+ style: { colorScheme: "dark" },
1381
+ "aria-label": "Message input"
1382
+ }
1383
+ ),
1384
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
1385
+ "button",
1386
+ {
1387
+ type: "button",
1388
+ onClick: isLoading && onStop ? onStop : handleSend,
1389
+ disabled: !canSend && !isLoading,
1390
+ "aria-label": isLoading ? "Stop generating" : "Send message",
1391
+ className: (0, import_tailwind_merge6.twMerge)(
1392
+ "absolute bottom-3 right-3",
1393
+ "inline-flex items-center justify-center",
1394
+ "w-9 h-9 rounded-full",
1395
+ "transition-all duration-200",
1396
+ "focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent",
1397
+ 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"
1398
+ ),
1399
+ children: isLoading ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(StopIcon, {}) : /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(ArrowUpIcon, {})
1400
+ }
1401
+ )
1402
+ ] })
927
1403
  ]
928
1404
  }
929
1405
  );
@@ -936,6 +1412,7 @@ function WelcomeScreen({
936
1412
  title = "Welcome",
937
1413
  message = "How can I help you today?",
938
1414
  icon,
1415
+ iconClassName,
939
1416
  suggestedQuestions = [],
940
1417
  onQuestionSelect,
941
1418
  className
@@ -948,12 +1425,15 @@ function WelcomeScreen({
948
1425
  className
949
1426
  ),
950
1427
  children: [
951
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1428
+ icon ? iconClassName ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { className: iconClassName, "aria-hidden": "true", children: icon }) : icon : /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
952
1429
  "div",
953
1430
  {
954
- className: "w-14 h-14 rounded-2xl bg-accent/10 border border-border flex items-center justify-center pulse-glow",
1431
+ className: (0, import_tailwind_merge7.twMerge)(
1432
+ "w-14 h-14 rounded-2xl bg-accent/10 border border-border flex items-center justify-center pulse-glow",
1433
+ iconClassName
1434
+ ),
955
1435
  "aria-hidden": "true",
956
- children: icon ?? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { className: "text-2xl", children: "\u2726" })
1436
+ children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { className: "text-2xl", children: "\u2726" })
957
1437
  }
958
1438
  ),
959
1439
  /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "flex flex-col gap-2", children: [
@@ -963,7 +1443,7 @@ function WelcomeScreen({
963
1443
  suggestedQuestions.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
964
1444
  "div",
965
1445
  {
966
- className: "flex flex-wrap justify-center gap-2 max-w-md",
1446
+ className: "flex flex-wrap justify-center gap-2 max-w-xl",
967
1447
  role: "group",
968
1448
  "aria-label": "Suggested questions",
969
1449
  children: suggestedQuestions.map((question) => /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
@@ -972,7 +1452,7 @@ function WelcomeScreen({
972
1452
  type: "button",
973
1453
  onClick: () => onQuestionSelect?.(question),
974
1454
  className: (0, import_tailwind_merge7.twMerge)(
975
- "px-4 py-2 rounded-full text-sm",
1455
+ "px-3.5 py-1.5 rounded-full text-[12px]",
976
1456
  "border border-border bg-transparent text-text-secondary",
977
1457
  "hover:bg-accent/10 hover:border-interactive hover:text-text-primary",
978
1458
  "focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent",
@@ -990,25 +1470,26 @@ function WelcomeScreen({
990
1470
  }
991
1471
 
992
1472
  // src/streaming/StreamingMessage/StreamingMessage.tsx
993
- var import_react6 = require("react");
1473
+ var import_react7 = require("react");
1474
+ var import_tailwind_merge8 = require("tailwind-merge");
994
1475
  var import_core3 = require("@surf-kit/core");
995
1476
 
996
1477
  // src/hooks/useCharacterDrain.ts
997
- var import_react5 = require("react");
1478
+ var import_react6 = require("react");
998
1479
  function useCharacterDrain(target, msPerChar = 15) {
999
- const [displayed, setDisplayed] = (0, import_react5.useState)("");
1000
- const indexRef = (0, import_react5.useRef)(0);
1001
- const lastTimeRef = (0, import_react5.useRef)(0);
1002
- const rafRef = (0, import_react5.useRef)(null);
1003
- const drainTargetRef = (0, import_react5.useRef)("");
1004
- const msPerCharRef = (0, import_react5.useRef)(msPerChar);
1480
+ const [displayed, setDisplayed] = (0, import_react6.useState)("");
1481
+ const indexRef = (0, import_react6.useRef)(0);
1482
+ const lastTimeRef = (0, import_react6.useRef)(0);
1483
+ const rafRef = (0, import_react6.useRef)(null);
1484
+ const drainTargetRef = (0, import_react6.useRef)("");
1485
+ const msPerCharRef = (0, import_react6.useRef)(msPerChar);
1005
1486
  msPerCharRef.current = msPerChar;
1006
1487
  if (target !== "") {
1007
1488
  drainTargetRef.current = target;
1008
1489
  }
1009
1490
  const drainTarget = drainTargetRef.current;
1010
1491
  const isDraining = displayed.length < drainTarget.length;
1011
- const tickRef = (0, import_react5.useRef)(() => {
1492
+ const tickRef = (0, import_react6.useRef)(() => {
1012
1493
  });
1013
1494
  tickRef.current = (now) => {
1014
1495
  const currentTarget = drainTargetRef.current;
@@ -1020,7 +1501,10 @@ function useCharacterDrain(target, msPerChar = 15) {
1020
1501
  const elapsed = now - lastTimeRef.current;
1021
1502
  const charsToAdvance = Math.floor(elapsed / msPerCharRef.current);
1022
1503
  if (charsToAdvance > 0 && indexRef.current < currentTarget.length) {
1023
- const nextIndex = Math.min(indexRef.current + charsToAdvance, currentTarget.length);
1504
+ let nextIndex = Math.min(indexRef.current + charsToAdvance, currentTarget.length);
1505
+ while (nextIndex < currentTarget.length && currentTarget[nextIndex - 1].trim() === "") {
1506
+ nextIndex++;
1507
+ }
1024
1508
  indexRef.current = nextIndex;
1025
1509
  lastTimeRef.current = now;
1026
1510
  setDisplayed(currentTarget.slice(0, nextIndex));
@@ -1031,12 +1515,12 @@ function useCharacterDrain(target, msPerChar = 15) {
1031
1515
  rafRef.current = null;
1032
1516
  }
1033
1517
  };
1034
- (0, import_react5.useEffect)(() => {
1518
+ (0, import_react6.useEffect)(() => {
1035
1519
  if (drainTargetRef.current !== "" && indexRef.current < drainTargetRef.current.length && rafRef.current === null) {
1036
1520
  rafRef.current = requestAnimationFrame((t) => tickRef.current(t));
1037
1521
  }
1038
1522
  }, [drainTarget]);
1039
- (0, import_react5.useEffect)(() => {
1523
+ (0, import_react6.useEffect)(() => {
1040
1524
  if (target === "" && !isDraining && displayed !== "") {
1041
1525
  indexRef.current = 0;
1042
1526
  lastTimeRef.current = 0;
@@ -1044,7 +1528,7 @@ function useCharacterDrain(target, msPerChar = 15) {
1044
1528
  setDisplayed("");
1045
1529
  }
1046
1530
  }, [target, isDraining, displayed]);
1047
- (0, import_react5.useEffect)(() => {
1531
+ (0, import_react6.useEffect)(() => {
1048
1532
  return () => {
1049
1533
  if (rafRef.current !== null) {
1050
1534
  cancelAnimationFrame(rafRef.current);
@@ -1065,51 +1549,78 @@ var phaseLabels = {
1065
1549
  generating: "Writing...",
1066
1550
  verifying: "Verifying..."
1067
1551
  };
1552
+ var CURSOR_STYLES = `
1553
+ .sk-streaming-cursor > :not(ul,ol,blockquote,div:has(table)):last-child::after,
1554
+ .sk-streaming-cursor > :is(ul,ol):last-child > li:last-child::after,
1555
+ .sk-streaming-cursor > blockquote:last-child > p:last-child::after,
1556
+ .sk-streaming-cursor > div:has(table):last-child table tbody tr:last-child td:last-child::after {
1557
+ content: "";
1558
+ display: inline-block;
1559
+ width: 2px;
1560
+ height: 1em;
1561
+ background: var(--color-accent, #38bdf8);
1562
+ animation: sk-cursor-blink 0.8s steps(1) infinite;
1563
+ margin-left: 2px;
1564
+ vertical-align: text-bottom;
1565
+ }
1566
+ @keyframes sk-cursor-blink {
1567
+ 0%, 60% { opacity: 1; }
1568
+ 61%, 100% { opacity: 0; }
1569
+ }
1570
+ `;
1068
1571
  function StreamingMessage({
1069
1572
  stream,
1070
1573
  onComplete,
1574
+ onDraining,
1071
1575
  showPhases = true,
1072
1576
  className
1073
1577
  }) {
1074
- const onCompleteRef = (0, import_react6.useRef)(onComplete);
1578
+ const onCompleteRef = (0, import_react7.useRef)(onComplete);
1075
1579
  onCompleteRef.current = onComplete;
1076
- const wasActiveRef = (0, import_react6.useRef)(stream.active);
1077
- (0, import_react6.useEffect)(() => {
1580
+ const onDrainingRef = (0, import_react7.useRef)(onDraining);
1581
+ onDrainingRef.current = onDraining;
1582
+ const wasActiveRef = (0, import_react7.useRef)(stream.active);
1583
+ (0, import_react7.useEffect)(() => {
1078
1584
  if (wasActiveRef.current && !stream.active) {
1079
1585
  onCompleteRef.current?.();
1080
1586
  }
1081
1587
  wasActiveRef.current = stream.active;
1082
1588
  }, [stream.active]);
1083
1589
  const phaseLabel = phaseLabels[stream.phase];
1084
- const { displayed: displayedContent } = useCharacterDrain(stream.content);
1085
- return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className, "data-testid": "streaming-message", children: [
1590
+ const { displayed: rawDisplayed, isDraining } = useCharacterDrain(stream.content);
1591
+ const displayedContent = stream.active || isDraining ? rawDisplayed.trimEnd() : rawDisplayed;
1592
+ (0, import_react7.useEffect)(() => {
1593
+ onDrainingRef.current?.(isDraining);
1594
+ }, [isDraining]);
1595
+ const agentLabel = stream.agent ? stream.agent.replace("_agent", "").replace("_", " ") : null;
1596
+ const showPhaseIndicator = showPhases && stream.active && stream.phase !== "idle" && !displayedContent;
1597
+ const showCursor = (stream.active || isDraining) && !!displayedContent;
1598
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: (0, import_tailwind_merge8.twMerge)("flex w-full flex-col items-start", className), "data-testid": "streaming-message", children: [
1086
1599
  /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { "aria-live": "assertive", className: "sr-only", children: [
1087
1600
  stream.active && stream.phase !== "idle" && "Response started",
1088
1601
  !stream.active && stream.content && "Response complete"
1089
1602
  ] }),
1603
+ showCursor && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("style", { children: CURSOR_STYLES }),
1604
+ agentLabel && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { className: "text-[11px] font-display font-semibold uppercase tracking-[0.08em] text-text-muted px-1 mb-1.5", children: agentLabel }),
1090
1605
  /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "max-w-[88%] px-4 py-3 rounded-[18px] rounded-tl-[4px] bg-surface border border-border motion-safe:animate-springFromLeft", children: [
1091
- showPhases && stream.active && stream.phase !== "idle" && /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(
1606
+ showPhaseIndicator && /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(
1092
1607
  "div",
1093
1608
  {
1094
- className: "flex items-center gap-2 mb-2 text-sm text-text-secondary",
1609
+ className: "flex items-center gap-2 text-sm text-text-secondary",
1095
1610
  "data-testid": "phase-indicator",
1096
1611
  children: [
1097
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { "aria-hidden": "true", children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_core3.Spinner, { size: "sm" }) }),
1612
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { "aria-hidden": "true", children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_core3.WaveLoader, { size: "sm", color: "#38bdf8" }) }),
1098
1613
  /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { children: phaseLabel })
1099
1614
  ]
1100
1615
  }
1101
1616
  ),
1102
- /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "text-sm leading-relaxed text-text-primary whitespace-pre-wrap", children: [
1103
- displayedContent,
1104
- stream.active && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
1105
- "span",
1106
- {
1107
- className: "inline-block w-0.5 h-4 bg-accent align-text-bottom animate-pulse ml-0.5",
1108
- "aria-hidden": "true",
1109
- "data-testid": "streaming-cursor"
1110
- }
1111
- )
1112
- ] })
1617
+ displayedContent && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
1618
+ ResponseMessage,
1619
+ {
1620
+ content: displayedContent,
1621
+ className: showCursor ? "sk-streaming-cursor" : void 0
1622
+ }
1623
+ )
1113
1624
  ] })
1114
1625
  ] });
1115
1626
  }
@@ -1140,7 +1651,7 @@ function AgentChat({
1140
1651
  return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(
1141
1652
  "div",
1142
1653
  {
1143
- className: (0, import_tailwind_merge8.twMerge)(
1654
+ className: (0, import_tailwind_merge9.twMerge)(
1144
1655
  "flex flex-col h-full bg-canvas border border-border rounded-xl overflow-hidden",
1145
1656
  className
1146
1657
  ),
@@ -1176,14 +1687,14 @@ function AgentChat({
1176
1687
  onQuestionSelect: handleQuestionSelect
1177
1688
  }
1178
1689
  ),
1179
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(MessageComposer, { onSend: handleSend, isLoading: state.isLoading })
1690
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(MessageComposer, { onSend: handleSend, onStop: actions.stop, isLoading: state.isLoading })
1180
1691
  ]
1181
1692
  }
1182
1693
  );
1183
1694
  }
1184
1695
 
1185
1696
  // src/chat/ConversationList/ConversationList.tsx
1186
- var import_tailwind_merge9 = require("tailwind-merge");
1697
+ var import_tailwind_merge10 = require("tailwind-merge");
1187
1698
  var import_jsx_runtime13 = require("react/jsx-runtime");
1188
1699
  function ConversationList({
1189
1700
  conversations,
@@ -1197,14 +1708,14 @@ function ConversationList({
1197
1708
  "nav",
1198
1709
  {
1199
1710
  "aria-label": "Conversation list",
1200
- className: (0, import_tailwind_merge9.twMerge)("flex flex-col h-full bg-canvas", className),
1711
+ className: (0, import_tailwind_merge10.twMerge)("flex flex-col flex-1 min-h-0", className),
1201
1712
  children: [
1202
- onNew && /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { className: "p-3 border-b border-border", children: /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
1713
+ onNew && /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { className: "px-5 pt-1 pb-3 border-b border-border", children: /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
1203
1714
  "button",
1204
1715
  {
1205
1716
  type: "button",
1206
1717
  onClick: onNew,
1207
- 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",
1718
+ 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",
1208
1719
  children: "New conversation"
1209
1720
  }
1210
1721
  ) }),
@@ -1214,10 +1725,10 @@ function ConversationList({
1214
1725
  return /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(
1215
1726
  "li",
1216
1727
  {
1217
- className: (0, import_tailwind_merge9.twMerge)(
1218
- "flex items-start border-b border-border transition-colors duration-200",
1219
- "hover:bg-surface",
1220
- isActive && "bg-surface-raised border-l-2 border-l-accent"
1728
+ className: (0, import_tailwind_merge10.twMerge)(
1729
+ "flex items-start transition-colors duration-150",
1730
+ "hover:bg-surface-raised",
1731
+ isActive ? "bg-accent-subtlest border-l-[3px] border-l-accent" : "border-l-[3px] border-l-transparent"
1221
1732
  ),
1222
1733
  children: [
1223
1734
  /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(
@@ -1226,10 +1737,10 @@ function ConversationList({
1226
1737
  type: "button",
1227
1738
  onClick: () => onSelect(conversation.id),
1228
1739
  "aria-current": isActive ? "true" : void 0,
1229
- className: "flex-1 min-w-0 text-left px-4 py-3",
1740
+ className: "flex-1 min-w-0 text-left px-5 py-2.5",
1230
1741
  children: [
1231
- /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { className: "text-sm font-medium text-brand-cream truncate", children: conversation.title }),
1232
- /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { className: "text-xs text-brand-cream/40 truncate mt-0.5 leading-relaxed", children: conversation.lastMessage })
1742
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { className: "text-sm font-medium text-text-primary truncate", children: conversation.title }),
1743
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { className: "text-xs text-text-muted truncate mt-0.5 leading-relaxed", children: conversation.lastMessage })
1233
1744
  ]
1234
1745
  }
1235
1746
  ),
@@ -1239,7 +1750,7 @@ function ConversationList({
1239
1750
  type: "button",
1240
1751
  onClick: () => onDelete(conversation.id),
1241
1752
  "aria-label": `Delete ${conversation.title}`,
1242
- 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",
1753
+ 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",
1243
1754
  children: /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(
1244
1755
  "svg",
1245
1756
  {
@@ -1266,7 +1777,7 @@ function ConversationList({
1266
1777
  conversation.id
1267
1778
  );
1268
1779
  }),
1269
- conversations.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("li", { className: "px-4 py-8 text-center", children: /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("span", { className: "text-sm text-brand-cream/30 font-body", children: "No conversations yet" }) })
1780
+ conversations.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("li", { className: "px-5 py-12 text-center", children: /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("span", { className: "text-sm text-text-muted font-body", children: "No conversations yet" }) })
1270
1781
  ] })
1271
1782
  ]
1272
1783
  }
@@ -1286,8 +1797,8 @@ function AgentFullPage({
1286
1797
  onNewConversation,
1287
1798
  className
1288
1799
  }) {
1289
- const [sidebarOpen, setSidebarOpen] = (0, import_react7.useState)(false);
1290
- const handleSelect = (0, import_react7.useCallback)(
1800
+ const [sidebarOpen, setSidebarOpen] = (0, import_react8.useState)(false);
1801
+ const handleSelect = (0, import_react8.useCallback)(
1291
1802
  (id) => {
1292
1803
  onConversationSelect?.(id);
1293
1804
  setSidebarOpen(false);
@@ -1297,7 +1808,7 @@ function AgentFullPage({
1297
1808
  return /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(
1298
1809
  "div",
1299
1810
  {
1300
- className: (0, import_tailwind_merge10.twMerge)("flex h-screen w-full overflow-hidden bg-brand-dark", className),
1811
+ className: (0, import_tailwind_merge11.twMerge)("flex h-screen w-full overflow-hidden bg-brand-dark", className),
1301
1812
  "data-testid": "agent-full-page",
1302
1813
  children: [
1303
1814
  showConversationList && /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(import_jsx_runtime14.Fragment, { children: [
@@ -1312,7 +1823,7 @@ function AgentFullPage({
1312
1823
  /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
1313
1824
  "aside",
1314
1825
  {
1315
- className: (0, import_tailwind_merge10.twMerge)(
1826
+ className: (0, import_tailwind_merge11.twMerge)(
1316
1827
  "bg-brand-dark border-r border-brand-gold/15 w-72 shrink-0 flex-col z-40",
1317
1828
  // Desktop: always visible
1318
1829
  "hidden md:flex",
@@ -1378,8 +1889,8 @@ function AgentFullPage({
1378
1889
  }
1379
1890
 
1380
1891
  // src/layouts/AgentPanel/AgentPanel.tsx
1381
- var import_tailwind_merge11 = require("tailwind-merge");
1382
- var import_react8 = require("react");
1892
+ var import_tailwind_merge12 = require("tailwind-merge");
1893
+ var import_react9 = require("react");
1383
1894
  var import_jsx_runtime15 = require("react/jsx-runtime");
1384
1895
  function AgentPanel({
1385
1896
  endpoint,
@@ -1390,8 +1901,8 @@ function AgentPanel({
1390
1901
  title = "Chat",
1391
1902
  className
1392
1903
  }) {
1393
- const panelRef = (0, import_react8.useRef)(null);
1394
- (0, import_react8.useEffect)(() => {
1904
+ const panelRef = (0, import_react9.useRef)(null);
1905
+ (0, import_react9.useEffect)(() => {
1395
1906
  if (!isOpen) return;
1396
1907
  const handleKeyDown = (e) => {
1397
1908
  if (e.key === "Escape") onClose();
@@ -1403,13 +1914,13 @@ function AgentPanel({
1403
1914
  return /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
1404
1915
  "div",
1405
1916
  {
1406
- className: (0, import_tailwind_merge11.twMerge)("fixed inset-0 z-50", !isOpen && "pointer-events-none"),
1917
+ className: (0, import_tailwind_merge12.twMerge)("fixed inset-0 z-50", !isOpen && "pointer-events-none"),
1407
1918
  "aria-hidden": !isOpen,
1408
1919
  children: [
1409
1920
  /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
1410
1921
  "div",
1411
1922
  {
1412
- className: (0, import_tailwind_merge11.twMerge)(
1923
+ className: (0, import_tailwind_merge12.twMerge)(
1413
1924
  "fixed inset-0 transition-opacity duration-300",
1414
1925
  isOpen ? "opacity-100 bg-brand-dark/70 backdrop-blur-sm pointer-events-auto" : "opacity-0 pointer-events-none"
1415
1926
  ),
@@ -1425,7 +1936,7 @@ function AgentPanel({
1425
1936
  "aria-label": title,
1426
1937
  "aria-modal": isOpen ? "true" : void 0,
1427
1938
  style: { width: widthStyle, maxWidth: "100vw" },
1428
- className: (0, import_tailwind_merge11.twMerge)(
1939
+ className: (0, import_tailwind_merge12.twMerge)(
1429
1940
  "fixed top-0 h-full flex flex-col z-50 bg-brand-dark shadow-card",
1430
1941
  "transition-transform duration-300 ease-in-out",
1431
1942
  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"}`,
@@ -1467,8 +1978,8 @@ function AgentPanel({
1467
1978
  }
1468
1979
 
1469
1980
  // src/layouts/AgentWidget/AgentWidget.tsx
1470
- var import_tailwind_merge12 = require("tailwind-merge");
1471
- var import_react9 = require("react");
1981
+ var import_tailwind_merge13 = require("tailwind-merge");
1982
+ var import_react10 = require("react");
1472
1983
  var import_jsx_runtime16 = require("react/jsx-runtime");
1473
1984
  function AgentWidget({
1474
1985
  endpoint,
@@ -1477,8 +1988,8 @@ function AgentWidget({
1477
1988
  title = "Chat",
1478
1989
  className
1479
1990
  }) {
1480
- const [isOpen, setIsOpen] = (0, import_react9.useState)(false);
1481
- const toggle = (0, import_react9.useCallback)(() => {
1991
+ const [isOpen, setIsOpen] = (0, import_react10.useState)(false);
1992
+ const toggle = (0, import_react10.useCallback)(() => {
1482
1993
  setIsOpen((prev) => !prev);
1483
1994
  }, []);
1484
1995
  const positionClasses = position === "bottom-left" ? "left-4 bottom-4" : "right-4 bottom-4";
@@ -1491,7 +2002,7 @@ function AgentWidget({
1491
2002
  role: "dialog",
1492
2003
  "aria-label": title,
1493
2004
  "aria-hidden": !isOpen,
1494
- className: (0, import_tailwind_merge12.twMerge)(
2005
+ className: (0, import_tailwind_merge13.twMerge)(
1495
2006
  "fixed z-50 flex flex-col",
1496
2007
  "w-[min(400px,calc(100vw-2rem))] h-[min(600px,calc(100vh-6rem))]",
1497
2008
  "rounded-2xl overflow-hidden border border-brand-gold/15",
@@ -1538,7 +2049,7 @@ function AgentWidget({
1538
2049
  onClick: toggle,
1539
2050
  "aria-label": isOpen ? "Close chat" : triggerLabel,
1540
2051
  "aria-expanded": isOpen,
1541
- className: (0, import_tailwind_merge12.twMerge)(
2052
+ className: (0, import_tailwind_merge13.twMerge)(
1542
2053
  "fixed z-50 flex items-center justify-center w-14 h-14 rounded-full",
1543
2054
  "bg-brand-blue text-brand-cream shadow-glow-cyan",
1544
2055
  "hover:bg-brand-cyan hover:shadow-glow-cyan hover:scale-105",
@@ -1556,7 +2067,7 @@ function AgentWidget({
1556
2067
  }
1557
2068
 
1558
2069
  // src/layouts/AgentEmbed/AgentEmbed.tsx
1559
- var import_tailwind_merge13 = require("tailwind-merge");
2070
+ var import_tailwind_merge14 = require("tailwind-merge");
1560
2071
  var import_jsx_runtime17 = require("react/jsx-runtime");
1561
2072
  function AgentEmbed({
1562
2073
  endpoint,
@@ -1566,7 +2077,7 @@ function AgentEmbed({
1566
2077
  return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
1567
2078
  "div",
1568
2079
  {
1569
- className: (0, import_tailwind_merge13.twMerge)("w-full h-full min-h-0", className),
2080
+ className: (0, import_tailwind_merge14.twMerge)("w-full h-full min-h-0", className),
1570
2081
  "data-testid": "agent-embed",
1571
2082
  children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
1572
2083
  AgentChat,