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