@surf-kit/agent 0.2.2 → 0.3.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 +625 -204
  2. package/dist/chat/index.cjs.map +1 -1
  3. package/dist/chat/index.d.cts +11 -6
  4. package/dist/chat/index.d.ts +11 -6
  5. package/dist/chat/index.js +606 -185
  6. package/dist/chat/index.js.map +1 -1
  7. package/dist/{chat--OifhIRe.d.ts → chat-BIIDOGrD.d.ts} +10 -1
  8. package/dist/{chat-ChYl2XjV.d.cts → chat-CGamM7Mz.d.cts} +10 -1
  9. package/dist/{hooks-DLfF18IU.d.cts → hooks-B1NYoLLs.d.cts} +21 -5
  10. package/dist/{hooks-BGs8-4GK.d.ts → hooks-CTeEqnBQ.d.ts} +21 -5
  11. package/dist/hooks.cjs +126 -81
  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 +126 -81
  16. package/dist/hooks.js.map +1 -1
  17. package/dist/index.cjs +686 -265
  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 +645 -224
  22. package/dist/index.js.map +1 -1
  23. package/dist/layouts/index.cjs +646 -225
  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 +622 -201
  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 +66 -12
  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 +64 -10
  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 +202 -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 +172 -73
  48. package/dist/streaming/index.js.map +1 -1
  49. package/dist/{streaming-DbQxScpi.d.ts → streaming-Bx-ff2tt.d.ts} +1 -1
  50. package/dist/{streaming-DfT22A0z.d.cts → streaming-x7umFHoP.d.cts} +1 -1
  51. package/package.json +15 -4
@@ -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,15 @@ 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_AGENT":
78
+ return { ...state, streamingAgent: action.agent };
75
79
  case "SEND_SUCCESS":
76
80
  return {
77
81
  ...state,
@@ -87,7 +91,8 @@ function reducer(state, action) {
87
91
  isLoading: false,
88
92
  error: action.error,
89
93
  streamPhase: "idle",
90
- streamingContent: ""
94
+ streamingContent: "",
95
+ streamingAgent: null
91
96
  };
92
97
  case "LOAD_CONVERSATION":
93
98
  return {
@@ -113,107 +118,142 @@ function useAgentChat(config) {
113
118
  const configRef = (0, import_react.useRef)(config);
114
119
  configRef.current = config;
115
120
  const lastUserMessageRef = (0, import_react.useRef)(null);
121
+ const lastUserAttachmentsRef = (0, import_react.useRef)(void 0);
116
122
  const sendMessage = (0, import_react.useCallback)(
117
- async (content) => {
118
- const { apiUrl, streamPath = "/chat/stream", headers = {}, timeout = 3e4 } = configRef.current;
123
+ async (content, attachments) => {
124
+ const { apiUrl, streamPath = "/chat/stream", headers: headersOrFn, timeout = 3e4, bodyExtra } = configRef.current;
125
+ const headers = typeof headersOrFn === "function" ? await headersOrFn() : headersOrFn ?? {};
119
126
  lastUserMessageRef.current = content;
127
+ lastUserAttachmentsRef.current = attachments;
120
128
  const userMessage = {
121
129
  id: generateMessageId(),
122
130
  role: "user",
123
131
  content,
132
+ attachments,
124
133
  timestamp: /* @__PURE__ */ new Date()
125
134
  };
126
135
  dispatch({ type: "SEND_START", message: userMessage });
127
136
  const controller = new AbortController();
128
137
  const timeoutId = setTimeout(() => controller.abort(), timeout);
129
138
  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;
139
+ const url = `${apiUrl}${streamPath}`;
140
+ const mergedHeaders = {
141
+ "Content-Type": "application/json",
142
+ Accept: "text/event-stream",
143
+ ...headers
144
+ };
145
+ const requestBody = {
146
+ message: content,
147
+ conversation_id: state.conversationId,
148
+ ...bodyExtra
149
+ };
150
+ if (attachments && attachments.length > 0) {
151
+ requestBody.attachments = attachments.map((a) => ({
152
+ filename: a.filename,
153
+ content_type: a.content_type,
154
+ data: a.data
155
+ }));
154
156
  }
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 }
157
+ const body = JSON.stringify(requestBody);
158
+ const ctx = {
159
+ accumulatedContent: "",
160
+ agentResponse: null,
161
+ capturedAgent: null,
162
+ capturedConversationId: null,
163
+ hadStreamError: false
164
+ };
165
+ const handleEvent = (event) => {
166
+ switch (event.type) {
167
+ case "agent":
168
+ ctx.capturedAgent = event.agent;
169
+ dispatch({ type: "STREAM_AGENT", agent: ctx.capturedAgent });
170
+ break;
171
+ case "phase":
172
+ dispatch({ type: "STREAM_PHASE", phase: event.phase });
173
+ break;
174
+ case "delta":
175
+ ctx.accumulatedContent += event.content;
176
+ dispatch({ type: "STREAM_CONTENT", content: event.content });
177
+ break;
178
+ case "done":
179
+ ctx.agentResponse = event.response;
180
+ ctx.capturedConversationId = event.conversation_id ?? null;
181
+ break;
182
+ case "error":
183
+ ctx.hadStreamError = true;
184
+ dispatch({ type: "SEND_ERROR", error: event.error });
185
+ break;
186
+ }
187
+ };
188
+ const { streamAdapter } = configRef.current;
189
+ if (streamAdapter) {
190
+ await streamAdapter(
191
+ url,
192
+ { method: "POST", headers: mergedHeaders, body, signal: controller.signal },
193
+ handleEvent
194
+ );
195
+ clearTimeout(timeoutId);
196
+ } else {
197
+ const response = await fetch(url, {
198
+ method: "POST",
199
+ headers: mergedHeaders,
200
+ body,
201
+ signal: controller.signal
160
202
  });
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;
203
+ clearTimeout(timeoutId);
204
+ if (!response.ok) {
205
+ dispatch({
206
+ type: "SEND_ERROR",
207
+ error: {
208
+ code: "API_ERROR",
209
+ message: `HTTP ${response.status}: ${response.statusText}`,
210
+ retryable: response.status >= 500
211
+ }
212
+ });
213
+ return;
214
+ }
215
+ const reader = response.body?.getReader();
216
+ if (!reader) {
217
+ dispatch({
218
+ type: "SEND_ERROR",
219
+ error: { code: "STREAM_ERROR", message: "No response body", retryable: true }
220
+ });
221
+ return;
222
+ }
223
+ const decoder = new TextDecoder();
224
+ let buffer = "";
225
+ while (true) {
226
+ const { done, value } = await reader.read();
227
+ if (done) break;
228
+ buffer += decoder.decode(value, { stream: true });
229
+ const lines = buffer.split("\n");
230
+ buffer = lines.pop() ?? "";
231
+ for (const line of lines) {
232
+ if (!line.startsWith("data: ")) continue;
233
+ const data = line.slice(6).trim();
234
+ if (data === "[DONE]") continue;
235
+ try {
236
+ const event = JSON.parse(data);
237
+ handleEvent(event);
238
+ } catch {
199
239
  }
200
- } catch {
201
240
  }
202
241
  }
203
242
  }
243
+ if (ctx.hadStreamError) return;
204
244
  const assistantMessage = {
205
245
  id: generateMessageId(),
206
246
  role: "assistant",
207
- content: agentResponse?.message ?? accumulatedContent,
208
- response: agentResponse ?? void 0,
209
- agent: capturedAgent ?? void 0,
247
+ content: ctx.agentResponse?.message ?? ctx.accumulatedContent,
248
+ response: ctx.agentResponse ?? void 0,
249
+ agent: ctx.capturedAgent ?? void 0,
210
250
  timestamp: /* @__PURE__ */ new Date()
211
251
  };
212
252
  dispatch({
213
253
  type: "SEND_SUCCESS",
214
254
  message: assistantMessage,
215
- streamingContent: accumulatedContent,
216
- conversationId: capturedConversationId
255
+ streamingContent: ctx.accumulatedContent,
256
+ conversationId: ctx.capturedConversationId
217
257
  });
218
258
  } catch (err) {
219
259
  clearTimeout(timeoutId);
@@ -244,7 +284,8 @@ function useAgentChat(config) {
244
284
  }, []);
245
285
  const submitFeedback = (0, import_react.useCallback)(
246
286
  async (messageId, rating, comment) => {
247
- const { apiUrl, feedbackPath = "/feedback", headers = {} } = configRef.current;
287
+ const { apiUrl, feedbackPath = "/feedback", headers: headersOrFn } = configRef.current;
288
+ const headers = typeof headersOrFn === "function" ? await headersOrFn() : headersOrFn ?? {};
248
289
  await fetch(`${apiUrl}${feedbackPath}`, {
249
290
  method: "POST",
250
291
  headers: { "Content-Type": "application/json", ...headers },
@@ -255,12 +296,13 @@ function useAgentChat(config) {
255
296
  );
256
297
  const retry = (0, import_react.useCallback)(async () => {
257
298
  if (lastUserMessageRef.current) {
258
- await sendMessage(lastUserMessageRef.current);
299
+ await sendMessage(lastUserMessageRef.current, lastUserAttachmentsRef.current);
259
300
  }
260
301
  }, [sendMessage]);
261
302
  const reset = (0, import_react.useCallback)(() => {
262
303
  dispatch({ type: "RESET" });
263
304
  lastUserMessageRef.current = null;
305
+ lastUserAttachmentsRef.current = void 0;
264
306
  }, []);
265
307
  const actions = {
266
308
  sendMessage,
@@ -275,7 +317,7 @@ function useAgentChat(config) {
275
317
 
276
318
  // src/chat/MessageThread/MessageThread.tsx
277
319
  var import_tailwind_merge5 = require("tailwind-merge");
278
- var import_react3 = require("react");
320
+ var import_react4 = require("react");
279
321
 
280
322
  // src/chat/MessageBubble/MessageBubble.tsx
281
323
  var import_tailwind_merge4 = require("tailwind-merge");
@@ -284,6 +326,7 @@ var import_tailwind_merge4 = require("tailwind-merge");
284
326
  var import_core2 = require("@surf-kit/core");
285
327
 
286
328
  // src/response/ResponseMessage/ResponseMessage.tsx
329
+ var import_react2 = __toESM(require("react"), 1);
287
330
  var import_react_markdown = __toESM(require("react-markdown"), 1);
288
331
  var import_rehype_sanitize = __toESM(require("rehype-sanitize"), 1);
289
332
  var import_tailwind_merge = require("tailwind-merge");
@@ -308,6 +351,7 @@ function ResponseMessage({ content, className }) {
308
351
  "[&_h3]:text-sm [&_h3]:font-semibold [&_h3]:text-accent [&_h3]:mt-2 [&_h3]:mb-1",
309
352
  "[&_code]:bg-surface-raised [&_code]:text-accent [&_code]:px-1.5 [&_code]:py-0.5 [&_code]:rounded [&_code]:text-xs [&_code]:font-mono",
310
353
  "[&_pre]:bg-surface-raised [&_pre]:border [&_pre]:border-border [&_pre]:rounded-xl [&_pre]:p-4 [&_pre]:overflow-x-auto",
354
+ "[&_hr]:my-3 [&_hr]:border-border",
311
355
  "[&_blockquote]:border-l-2 [&_blockquote]:border-border-strong [&_blockquote]:pl-4 [&_blockquote]:text-text-secondary",
312
356
  "[&_a]:text-accent [&_a]:underline-offset-2 [&_a]:hover:text-accent/80",
313
357
  className
@@ -323,11 +367,24 @@ function ResponseMessage({ content, className }) {
323
367
  p: ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "my-2", children }),
324
368
  ul: ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ul", { className: "my-2 list-disc pl-6", children }),
325
369
  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 }),
370
+ li: ({ children, ...props }) => {
371
+ let content2 = children;
372
+ if (props.ordered) {
373
+ content2 = import_react2.default.Children.map(children, (child, i) => {
374
+ if (i === 0 && typeof child === "string") {
375
+ return child.replace(/^\d+[.)]\s*/, "");
376
+ }
377
+ return child;
378
+ });
379
+ }
380
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("li", { className: "my-1", children: content2 });
381
+ },
327
382
  strong: ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { className: "font-semibold", children }),
383
+ em: ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("em", { className: "italic text-text-secondary", children }),
328
384
  h1: ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h1", { className: "text-base font-bold mt-4 mb-2", children }),
329
385
  h2: ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", { className: "text-sm font-bold mt-3 mb-1", children }),
330
386
  h3: ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h3", { className: "text-sm font-semibold mt-2 mb-1", children }),
387
+ hr: () => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("hr", { className: "my-3 border-border" }),
331
388
  code: ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("code", { className: "bg-surface-sunken rounded px-1 py-0.5 text-xs font-mono", children })
332
389
  },
333
390
  children: normalizeMarkdownLists(content)
@@ -463,7 +520,14 @@ function renderWarning(data) {
463
520
  }
464
521
  );
465
522
  }
466
- function StructuredResponse({ uiHint, data, className }) {
523
+ function StructuredResponse({ uiHint, data: rawData, className }) {
524
+ const data = typeof rawData === "string" ? (() => {
525
+ try {
526
+ return JSON.parse(rawData);
527
+ } catch {
528
+ return null;
529
+ }
530
+ })() : rawData;
467
531
  if (!data) return null;
468
532
  let content;
469
533
  switch (uiHint) {
@@ -493,7 +557,7 @@ function StructuredResponse({ uiHint, data, className }) {
493
557
  }
494
558
 
495
559
  // src/sources/SourceList/SourceList.tsx
496
- var import_react2 = require("react");
560
+ var import_react3 = require("react");
497
561
 
498
562
  // src/sources/SourceCard/SourceCard.tsx
499
563
  var import_core = require("@surf-kit/core");
@@ -543,7 +607,36 @@ function SourceCard({ source, variant = "compact", onNavigate, className }) {
543
607
  children: [
544
608
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "flex items-start justify-between gap-2", children: [
545
609
  /* @__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 }),
610
+ source.url ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
611
+ "a",
612
+ {
613
+ href: source.url,
614
+ target: "_blank",
615
+ rel: "noopener noreferrer",
616
+ className: "text-sm font-medium text-accent hover:underline truncate block",
617
+ onClick: (e) => e.stopPropagation(),
618
+ children: [
619
+ source.title,
620
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
621
+ "svg",
622
+ {
623
+ className: "inline-block ml-1 w-3 h-3 opacity-60",
624
+ viewBox: "0 0 24 24",
625
+ fill: "none",
626
+ stroke: "currentColor",
627
+ strokeWidth: "2",
628
+ strokeLinecap: "round",
629
+ strokeLinejoin: "round",
630
+ children: [
631
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("path", { d: "M18 13v6a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h6" }),
632
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("polyline", { points: "15 3 21 3 21 9" }),
633
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("line", { x1: "10", y1: "14", x2: "21", y2: "3" })
634
+ ]
635
+ }
636
+ )
637
+ ]
638
+ }
639
+ ) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "text-sm font-medium text-text-primary truncate", children: source.title }),
547
640
  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
641
  ] }),
549
642
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
@@ -573,7 +666,7 @@ function SourceList({
573
666
  onNavigate,
574
667
  className
575
668
  }) {
576
- const [isExpanded, setIsExpanded] = (0, import_react2.useState)(defaultExpanded);
669
+ const [isExpanded, setIsExpanded] = (0, import_react3.useState)(defaultExpanded);
577
670
  if (sources.length === 0) return null;
578
671
  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
672
  SourceCard,
@@ -677,13 +770,16 @@ function AgentResponse({
677
770
  }) {
678
771
  return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: `flex flex-col gap-4 ${className ?? ""}`, "data-testid": "agent-response", children: [
679
772
  /* @__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
- ),
773
+ response.ui_hint !== "text" && response.structured_data && (() => {
774
+ const parsed = typeof response.structured_data === "string" ? (() => {
775
+ try {
776
+ return JSON.parse(response.structured_data);
777
+ } catch {
778
+ return null;
779
+ }
780
+ })() : response.structured_data;
781
+ return parsed ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(StructuredResponse, { uiHint: response.ui_hint, data: parsed }) : null;
782
+ })(),
687
783
  (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
784
  showConfidence && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
689
785
  import_core2.Badge,
@@ -734,6 +830,31 @@ function AgentResponse({
734
830
 
735
831
  // src/chat/MessageBubble/MessageBubble.tsx
736
832
  var import_jsx_runtime7 = require("react/jsx-runtime");
833
+ function DocumentIcon() {
834
+ 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: [
835
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("path", { d: "M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z" }),
836
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("polyline", { points: "14 2 14 8 20 8" }),
837
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("line", { x1: "16", y1: "13", x2: "8", y2: "13" }),
838
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("line", { x1: "16", y1: "17", x2: "8", y2: "17" })
839
+ ] });
840
+ }
841
+ function AttachmentThumbnail({ attachment }) {
842
+ const isImage = attachment.content_type.startsWith("image/");
843
+ if (isImage) {
844
+ 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)(
845
+ "img",
846
+ {
847
+ src: attachment.preview_url ?? `data:${attachment.content_type};base64,${attachment.data}`,
848
+ alt: attachment.filename,
849
+ className: "max-w-full max-h-[200px] object-contain"
850
+ }
851
+ ) });
852
+ }
853
+ 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: [
854
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(DocumentIcon, {}),
855
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "text-xs truncate max-w-[160px]", children: attachment.filename })
856
+ ] });
857
+ }
737
858
  function MessageBubble({
738
859
  message,
739
860
  showAgent,
@@ -741,23 +862,29 @@ function MessageBubble({
741
862
  showConfidence = true,
742
863
  showVerification = true,
743
864
  animated = true,
865
+ userBubbleClassName,
744
866
  className
745
867
  }) {
746
868
  const isUser = message.role === "user";
869
+ const hasAttachments = message.attachments && message.attachments.length > 0;
747
870
  if (isUser) {
748
871
  return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
749
872
  "div",
750
873
  {
751
874
  "data-message-id": message.id,
752
875
  className: (0, import_tailwind_merge4.twMerge)("flex w-full justify-end", className),
753
- children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
876
+ children: /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
754
877
  "div",
755
878
  {
756
879
  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"
880
+ "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",
881
+ animated && "motion-safe:animate-slideFromRight",
882
+ userBubbleClassName
759
883
  ),
760
- children: message.content
884
+ children: [
885
+ 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}`)) }),
886
+ message.content
887
+ ]
761
888
  }
762
889
  )
763
890
  }
@@ -769,7 +896,7 @@ function MessageBubble({
769
896
  "data-message-id": message.id,
770
897
  className: (0, import_tailwind_merge4.twMerge)("flex w-full flex-col items-start gap-1.5", className),
771
898
  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("_", " ") }),
899
+ 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
900
  /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
774
901
  "div",
775
902
  {
@@ -795,34 +922,70 @@ function MessageBubble({
795
922
 
796
923
  // src/chat/MessageThread/MessageThread.tsx
797
924
  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]);
925
+ function MessageThread({ messages, streamingSlot, showAgent, showSources, showConfidence, showVerification, hideLastAssistant, userBubbleClassName, className }) {
926
+ const scrollRef = (0, import_react4.useRef)(null);
927
+ const isNearBottom = (0, import_react4.useRef)(true);
928
+ const isProgrammaticScroll = (0, import_react4.useRef)(false);
929
+ const hasStreaming = !!streamingSlot;
930
+ const scrollToBottom = (0, import_react4.useCallback)(() => {
931
+ const el = scrollRef.current;
932
+ if (el && isNearBottom.current) {
933
+ isProgrammaticScroll.current = true;
934
+ el.scrollTop = el.scrollHeight;
935
+ }
936
+ }, []);
937
+ const handleScroll = (0, import_react4.useCallback)(() => {
938
+ if (isProgrammaticScroll.current) {
939
+ isProgrammaticScroll.current = false;
940
+ return;
941
+ }
942
+ const el = scrollRef.current;
943
+ if (!el) return;
944
+ isNearBottom.current = el.scrollHeight - el.scrollTop - el.clientHeight < 80;
945
+ }, []);
946
+ (0, import_react4.useEffect)(scrollToBottom, [messages.length, scrollToBottom]);
947
+ (0, import_react4.useEffect)(() => {
948
+ if (!hasStreaming) return;
949
+ let raf;
950
+ const tick = () => {
951
+ scrollToBottom();
952
+ raf = requestAnimationFrame(tick);
953
+ };
954
+ raf = requestAnimationFrame(tick);
955
+ return () => cancelAnimationFrame(raf);
956
+ }, [hasStreaming, scrollToBottom]);
803
957
  return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
804
958
  "div",
805
959
  {
960
+ ref: scrollRef,
806
961
  role: "log",
807
962
  "aria-live": "polite",
808
963
  "aria-label": "Message thread",
964
+ onScroll: handleScroll,
809
965
  className: (0, import_tailwind_merge5.twMerge)(
810
966
  "flex flex-col gap-4 overflow-y-auto flex-1 px-4 py-6",
811
967
  className
812
968
  ),
813
969
  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 })
970
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "flex-1 shrink-0" }),
971
+ messages.map((message, i) => {
972
+ if (hideLastAssistant && i === messages.length - 1 && message.role === "assistant") {
973
+ return null;
974
+ }
975
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
976
+ MessageBubble,
977
+ {
978
+ message,
979
+ showAgent,
980
+ showSources,
981
+ showConfidence,
982
+ showVerification,
983
+ userBubbleClassName
984
+ },
985
+ message.id
986
+ );
987
+ }),
988
+ streamingSlot
826
989
  ]
827
990
  }
828
991
  );
@@ -830,32 +993,126 @@ function MessageThread({ messages, streamingSlot, showSources, showConfidence, s
830
993
 
831
994
  // src/chat/MessageComposer/MessageComposer.tsx
832
995
  var import_tailwind_merge6 = require("tailwind-merge");
833
- var import_react4 = require("react");
996
+ var import_react5 = require("react");
834
997
  var import_jsx_runtime9 = require("react/jsx-runtime");
998
+ var ALLOWED_TYPES = /* @__PURE__ */ new Set([
999
+ "image/png",
1000
+ "image/jpeg",
1001
+ "image/gif",
1002
+ "image/webp",
1003
+ "application/pdf"
1004
+ ]);
1005
+ var MAX_FILE_SIZE = 10 * 1024 * 1024;
1006
+ var MAX_ATTACHMENTS = 5;
1007
+ function ArrowUpIcon() {
1008
+ 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: [
1009
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("path", { d: "M10 16V4" }),
1010
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("path", { d: "M4 10l6-6 6 6" })
1011
+ ] });
1012
+ }
1013
+ function StopIcon() {
1014
+ 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" }) });
1015
+ }
1016
+ function PaperclipIcon() {
1017
+ 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" }) });
1018
+ }
1019
+ function XIcon({ size = 14 }) {
1020
+ 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: [
1021
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("path", { d: "M18 6L6 18" }),
1022
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("path", { d: "M6 6l12 12" })
1023
+ ] });
1024
+ }
1025
+ function DocumentIcon2() {
1026
+ 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: [
1027
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("path", { d: "M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z" }),
1028
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("polyline", { points: "14 2 14 8 20 8" }),
1029
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("line", { x1: "16", y1: "13", x2: "8", y2: "13" }),
1030
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("line", { x1: "16", y1: "17", x2: "8", y2: "17" }),
1031
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("polyline", { points: "10 9 9 9 8 9" })
1032
+ ] });
1033
+ }
1034
+ function fileToBase64(file) {
1035
+ return new Promise((resolve, reject) => {
1036
+ const reader = new FileReader();
1037
+ reader.onload = () => {
1038
+ const result = reader.result;
1039
+ const base64 = result.split(",")[1];
1040
+ resolve(base64);
1041
+ };
1042
+ reader.onerror = reject;
1043
+ reader.readAsDataURL(file);
1044
+ });
1045
+ }
1046
+ function AttachmentPreview({
1047
+ attachment,
1048
+ onRemove
1049
+ }) {
1050
+ const isImage = attachment.content_type.startsWith("image/");
1051
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "relative group flex-shrink-0", children: [
1052
+ 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)(
1053
+ "img",
1054
+ {
1055
+ src: attachment.preview_url ?? `data:${attachment.content_type};base64,${attachment.data}`,
1056
+ alt: attachment.filename,
1057
+ className: "w-full h-full object-cover"
1058
+ }
1059
+ ) }) : /* @__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: [
1060
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { className: "text-text-muted", children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(DocumentIcon2, {}) }),
1061
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "flex flex-col min-w-0", children: [
1062
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: "text-xs text-text-primary truncate max-w-[120px]", children: attachment.filename }),
1063
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: "text-[10px] text-text-muted", children: "PDF" })
1064
+ ] })
1065
+ ] }),
1066
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
1067
+ "button",
1068
+ {
1069
+ type: "button",
1070
+ onClick: onRemove,
1071
+ className: (0, import_tailwind_merge6.twMerge)(
1072
+ "absolute -top-1.5 -right-1.5",
1073
+ "w-5 h-5 rounded-full",
1074
+ "bg-text-muted/80 text-white",
1075
+ "flex items-center justify-center",
1076
+ "opacity-0 group-hover:opacity-100",
1077
+ "transition-opacity duration-150",
1078
+ "hover:bg-text-primary"
1079
+ ),
1080
+ "aria-label": `Remove ${attachment.filename}`,
1081
+ children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(XIcon, { size: 10 })
1082
+ }
1083
+ )
1084
+ ] });
1085
+ }
835
1086
  function MessageComposer({
836
1087
  onSend,
837
1088
  isLoading = false,
838
1089
  placeholder = "Type a message...",
839
1090
  className
840
1091
  }) {
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)(() => {
1092
+ const [value, setValue] = (0, import_react5.useState)("");
1093
+ const [attachments, setAttachments] = (0, import_react5.useState)([]);
1094
+ const [dragOver, setDragOver] = (0, import_react5.useState)(false);
1095
+ const textareaRef = (0, import_react5.useRef)(null);
1096
+ const fileInputRef = (0, import_react5.useRef)(null);
1097
+ const canSend = (value.trim().length > 0 || attachments.length > 0) && !isLoading;
1098
+ const resetHeight = (0, import_react5.useCallback)(() => {
845
1099
  const el = textareaRef.current;
846
1100
  if (el) {
847
1101
  el.style.height = "auto";
848
1102
  el.style.overflowY = "hidden";
849
1103
  }
850
1104
  }, []);
851
- const handleSend = (0, import_react4.useCallback)(() => {
1105
+ const handleSend = (0, import_react5.useCallback)(() => {
852
1106
  if (!canSend) return;
853
- onSend(value.trim());
1107
+ const message = value.trim() || (attachments.length > 0 ? "Please analyse the attached file(s)." : "");
1108
+ if (!message && attachments.length === 0) return;
1109
+ onSend(message, attachments.length > 0 ? attachments : void 0);
854
1110
  setValue("");
1111
+ setAttachments([]);
855
1112
  resetHeight();
856
1113
  textareaRef.current?.focus();
857
- }, [canSend, onSend, value, resetHeight]);
858
- const handleKeyDown = (0, import_react4.useCallback)(
1114
+ }, [canSend, onSend, value, attachments, resetHeight]);
1115
+ const handleKeyDown = (0, import_react5.useCallback)(
859
1116
  (e) => {
860
1117
  if (e.key === "Enter" && !e.shiftKey) {
861
1118
  e.preventDefault();
@@ -864,64 +1121,194 @@ function MessageComposer({
864
1121
  },
865
1122
  [handleSend]
866
1123
  );
867
- const handleChange = (0, import_react4.useCallback)(
1124
+ const handleChange = (0, import_react5.useCallback)(
868
1125
  (e) => {
869
1126
  setValue(e.target.value);
870
1127
  const el = e.target;
871
1128
  el.style.height = "auto";
872
- const capped = Math.min(el.scrollHeight, 128);
1129
+ const capped = Math.min(el.scrollHeight, 200);
873
1130
  el.style.height = `${capped}px`;
874
- el.style.overflowY = el.scrollHeight > 128 ? "auto" : "hidden";
1131
+ el.style.overflowY = el.scrollHeight > 200 ? "auto" : "hidden";
875
1132
  },
876
1133
  []
877
1134
  );
1135
+ const addFiles = (0, import_react5.useCallback)(async (files) => {
1136
+ const fileArray = Array.from(files);
1137
+ for (const file of fileArray) {
1138
+ if (attachments.length >= MAX_ATTACHMENTS) break;
1139
+ if (!ALLOWED_TYPES.has(file.type)) continue;
1140
+ if (file.size > MAX_FILE_SIZE) continue;
1141
+ try {
1142
+ const data = await fileToBase64(file);
1143
+ const previewUrl = file.type.startsWith("image/") ? URL.createObjectURL(file) : void 0;
1144
+ const attachment = {
1145
+ filename: file.name,
1146
+ content_type: file.type,
1147
+ data,
1148
+ preview_url: previewUrl
1149
+ };
1150
+ setAttachments((prev) => {
1151
+ if (prev.length >= MAX_ATTACHMENTS) return prev;
1152
+ return [...prev, attachment];
1153
+ });
1154
+ } catch {
1155
+ }
1156
+ }
1157
+ }, [attachments.length]);
1158
+ const handleFileSelect = (0, import_react5.useCallback)(() => {
1159
+ fileInputRef.current?.click();
1160
+ }, []);
1161
+ const handleFileInputChange = (0, import_react5.useCallback)(
1162
+ (e) => {
1163
+ if (e.target.files) {
1164
+ void addFiles(e.target.files);
1165
+ e.target.value = "";
1166
+ }
1167
+ },
1168
+ [addFiles]
1169
+ );
1170
+ const removeAttachment = (0, import_react5.useCallback)((index) => {
1171
+ setAttachments((prev) => {
1172
+ const removed = prev[index];
1173
+ if (removed?.preview_url) URL.revokeObjectURL(removed.preview_url);
1174
+ return prev.filter((_, i) => i !== index);
1175
+ });
1176
+ }, []);
1177
+ const handlePaste = (0, import_react5.useCallback)(
1178
+ (e) => {
1179
+ const items = e.clipboardData.items;
1180
+ const files = [];
1181
+ for (const item of items) {
1182
+ if (item.kind === "file" && ALLOWED_TYPES.has(item.type)) {
1183
+ const file = item.getAsFile();
1184
+ if (file) files.push(file);
1185
+ }
1186
+ }
1187
+ if (files.length > 0) {
1188
+ void addFiles(files);
1189
+ }
1190
+ },
1191
+ [addFiles]
1192
+ );
1193
+ const handleDragOver = (0, import_react5.useCallback)((e) => {
1194
+ e.preventDefault();
1195
+ e.stopPropagation();
1196
+ setDragOver(true);
1197
+ }, []);
1198
+ const handleDragLeave = (0, import_react5.useCallback)((e) => {
1199
+ e.preventDefault();
1200
+ e.stopPropagation();
1201
+ setDragOver(false);
1202
+ }, []);
1203
+ const handleDrop = (0, import_react5.useCallback)(
1204
+ (e) => {
1205
+ e.preventDefault();
1206
+ e.stopPropagation();
1207
+ setDragOver(false);
1208
+ if (e.dataTransfer.files.length > 0) {
1209
+ void addFiles(e.dataTransfer.files);
1210
+ }
1211
+ },
1212
+ [addFiles]
1213
+ );
878
1214
  return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(
879
1215
  "div",
880
1216
  {
881
1217
  className: (0, import_tailwind_merge6.twMerge)(
882
- "flex items-end gap-3 shrink-0 border-t border-border px-4 py-3",
1218
+ "relative shrink-0 rounded-3xl border bg-surface",
1219
+ "shadow-lg shadow-black/10",
1220
+ "transition-all duration-200",
1221
+ "focus-within:border-accent/40 focus-within:shadow-accent/5",
1222
+ dragOver ? "border-accent/60 bg-accent/5" : "border-border/60",
883
1223
  className
884
1224
  ),
1225
+ onDragOver: handleDragOver,
1226
+ onDragLeave: handleDragLeave,
1227
+ onDrop: handleDrop,
885
1228
  children: [
886
1229
  /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
887
- "textarea",
1230
+ "input",
888
1231
  {
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"
1232
+ ref: fileInputRef,
1233
+ type: "file",
1234
+ multiple: true,
1235
+ accept: "image/png,image/jpeg,image/gif,image/webp,application/pdf",
1236
+ onChange: handleFileInputChange,
1237
+ className: "hidden",
1238
+ "aria-hidden": "true"
906
1239
  }
907
1240
  ),
908
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
909
- "button",
1241
+ 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)(
1242
+ AttachmentPreview,
910
1243
  {
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
- )
1244
+ attachment: att,
1245
+ onRemove: () => removeAttachment(i)
1246
+ },
1247
+ `${att.filename}-${i}`
1248
+ )) }),
1249
+ 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" }) }),
1250
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "flex items-end", children: [
1251
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
1252
+ "button",
1253
+ {
1254
+ type: "button",
1255
+ onClick: handleFileSelect,
1256
+ disabled: isLoading || attachments.length >= MAX_ATTACHMENTS,
1257
+ "aria-label": "Attach file",
1258
+ className: (0, import_tailwind_merge6.twMerge)(
1259
+ "flex-shrink-0 ml-2 mb-3",
1260
+ "inline-flex items-center justify-center",
1261
+ "w-9 h-9 rounded-full",
1262
+ "transition-all duration-200",
1263
+ "text-text-muted/60 hover:text-text-secondary hover:bg-text-muted/10",
1264
+ "focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent",
1265
+ "disabled:opacity-30 disabled:cursor-not-allowed disabled:hover:bg-transparent"
1266
+ ),
1267
+ children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(PaperclipIcon, {})
1268
+ }
1269
+ ),
1270
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
1271
+ "textarea",
1272
+ {
1273
+ ref: textareaRef,
1274
+ value,
1275
+ onChange: handleChange,
1276
+ onKeyDown: handleKeyDown,
1277
+ onPaste: handlePaste,
1278
+ placeholder,
1279
+ rows: 1,
1280
+ disabled: isLoading,
1281
+ className: (0, import_tailwind_merge6.twMerge)(
1282
+ "flex-1 resize-none bg-transparent",
1283
+ "pl-2 pr-14 pt-4 pb-4 text-[15px] leading-relaxed",
1284
+ "text-text-primary placeholder:text-text-muted/70",
1285
+ "focus:outline-none",
1286
+ "disabled:opacity-50 disabled:cursor-not-allowed",
1287
+ "overflow-hidden"
1288
+ ),
1289
+ style: { colorScheme: "dark" },
1290
+ "aria-label": "Message input"
1291
+ }
1292
+ ),
1293
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
1294
+ "button",
1295
+ {
1296
+ type: "button",
1297
+ onClick: handleSend,
1298
+ disabled: !canSend,
1299
+ "aria-label": "Send message",
1300
+ className: (0, import_tailwind_merge6.twMerge)(
1301
+ "absolute bottom-3 right-3",
1302
+ "inline-flex items-center justify-center",
1303
+ "w-9 h-9 rounded-full",
1304
+ "transition-all duration-200",
1305
+ "focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent",
1306
+ 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" : "bg-transparent text-text-muted/40 cursor-default"
1307
+ ),
1308
+ children: isLoading ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(StopIcon, {}) : /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(ArrowUpIcon, {})
1309
+ }
1310
+ )
1311
+ ] })
925
1312
  ]
926
1313
  }
927
1314
  );
@@ -934,6 +1321,7 @@ function WelcomeScreen({
934
1321
  title = "Welcome",
935
1322
  message = "How can I help you today?",
936
1323
  icon,
1324
+ iconClassName,
937
1325
  suggestedQuestions = [],
938
1326
  onQuestionSelect,
939
1327
  className
@@ -946,12 +1334,15 @@ function WelcomeScreen({
946
1334
  className
947
1335
  ),
948
1336
  children: [
949
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1337
+ icon ? iconClassName ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { className: iconClassName, "aria-hidden": "true", children: icon }) : icon : /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
950
1338
  "div",
951
1339
  {
952
- className: "w-14 h-14 rounded-2xl bg-accent/10 border border-border flex items-center justify-center pulse-glow",
1340
+ className: (0, import_tailwind_merge7.twMerge)(
1341
+ "w-14 h-14 rounded-2xl bg-accent/10 border border-border flex items-center justify-center pulse-glow",
1342
+ iconClassName
1343
+ ),
953
1344
  "aria-hidden": "true",
954
- children: icon ?? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { className: "text-2xl", children: "\u2726" })
1345
+ children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { className: "text-2xl", children: "\u2726" })
955
1346
  }
956
1347
  ),
957
1348
  /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "flex flex-col gap-2", children: [
@@ -961,7 +1352,7 @@ function WelcomeScreen({
961
1352
  suggestedQuestions.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
962
1353
  "div",
963
1354
  {
964
- className: "flex flex-wrap justify-center gap-2 max-w-md",
1355
+ className: "flex flex-wrap justify-center gap-2 max-w-xl",
965
1356
  role: "group",
966
1357
  "aria-label": "Suggested questions",
967
1358
  children: suggestedQuestions.map((question) => /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
@@ -970,7 +1361,7 @@ function WelcomeScreen({
970
1361
  type: "button",
971
1362
  onClick: () => onQuestionSelect?.(question),
972
1363
  className: (0, import_tailwind_merge7.twMerge)(
973
- "px-4 py-2 rounded-full text-sm",
1364
+ "px-3.5 py-1.5 rounded-full text-[12px]",
974
1365
  "border border-border bg-transparent text-text-secondary",
975
1366
  "hover:bg-accent/10 hover:border-interactive hover:text-text-primary",
976
1367
  "focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent",
@@ -988,25 +1379,26 @@ function WelcomeScreen({
988
1379
  }
989
1380
 
990
1381
  // src/streaming/StreamingMessage/StreamingMessage.tsx
991
- var import_react6 = require("react");
1382
+ var import_react7 = require("react");
1383
+ var import_tailwind_merge8 = require("tailwind-merge");
992
1384
  var import_core3 = require("@surf-kit/core");
993
1385
 
994
1386
  // src/hooks/useCharacterDrain.ts
995
- var import_react5 = require("react");
1387
+ var import_react6 = require("react");
996
1388
  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);
1389
+ const [displayed, setDisplayed] = (0, import_react6.useState)("");
1390
+ const indexRef = (0, import_react6.useRef)(0);
1391
+ const lastTimeRef = (0, import_react6.useRef)(0);
1392
+ const rafRef = (0, import_react6.useRef)(null);
1393
+ const drainTargetRef = (0, import_react6.useRef)("");
1394
+ const msPerCharRef = (0, import_react6.useRef)(msPerChar);
1003
1395
  msPerCharRef.current = msPerChar;
1004
1396
  if (target !== "") {
1005
1397
  drainTargetRef.current = target;
1006
1398
  }
1007
1399
  const drainTarget = drainTargetRef.current;
1008
1400
  const isDraining = displayed.length < drainTarget.length;
1009
- const tickRef = (0, import_react5.useRef)(() => {
1401
+ const tickRef = (0, import_react6.useRef)(() => {
1010
1402
  });
1011
1403
  tickRef.current = (now) => {
1012
1404
  const currentTarget = drainTargetRef.current;
@@ -1018,7 +1410,10 @@ function useCharacterDrain(target, msPerChar = 15) {
1018
1410
  const elapsed = now - lastTimeRef.current;
1019
1411
  const charsToAdvance = Math.floor(elapsed / msPerCharRef.current);
1020
1412
  if (charsToAdvance > 0 && indexRef.current < currentTarget.length) {
1021
- const nextIndex = Math.min(indexRef.current + charsToAdvance, currentTarget.length);
1413
+ let nextIndex = Math.min(indexRef.current + charsToAdvance, currentTarget.length);
1414
+ while (nextIndex < currentTarget.length && currentTarget[nextIndex - 1].trim() === "") {
1415
+ nextIndex++;
1416
+ }
1022
1417
  indexRef.current = nextIndex;
1023
1418
  lastTimeRef.current = now;
1024
1419
  setDisplayed(currentTarget.slice(0, nextIndex));
@@ -1029,12 +1424,12 @@ function useCharacterDrain(target, msPerChar = 15) {
1029
1424
  rafRef.current = null;
1030
1425
  }
1031
1426
  };
1032
- (0, import_react5.useEffect)(() => {
1427
+ (0, import_react6.useEffect)(() => {
1033
1428
  if (drainTargetRef.current !== "" && indexRef.current < drainTargetRef.current.length && rafRef.current === null) {
1034
1429
  rafRef.current = requestAnimationFrame((t) => tickRef.current(t));
1035
1430
  }
1036
1431
  }, [drainTarget]);
1037
- (0, import_react5.useEffect)(() => {
1432
+ (0, import_react6.useEffect)(() => {
1038
1433
  if (target === "" && !isDraining && displayed !== "") {
1039
1434
  indexRef.current = 0;
1040
1435
  lastTimeRef.current = 0;
@@ -1042,7 +1437,7 @@ function useCharacterDrain(target, msPerChar = 15) {
1042
1437
  setDisplayed("");
1043
1438
  }
1044
1439
  }, [target, isDraining, displayed]);
1045
- (0, import_react5.useEffect)(() => {
1440
+ (0, import_react6.useEffect)(() => {
1046
1441
  return () => {
1047
1442
  if (rafRef.current !== null) {
1048
1443
  cancelAnimationFrame(rafRef.current);
@@ -1063,51 +1458,77 @@ var phaseLabels = {
1063
1458
  generating: "Writing...",
1064
1459
  verifying: "Verifying..."
1065
1460
  };
1461
+ var CURSOR_STYLES = `
1462
+ .sk-streaming-cursor > :not(ul,ol,blockquote):last-child::after,
1463
+ .sk-streaming-cursor > :is(ul,ol):last-child > li:last-child::after,
1464
+ .sk-streaming-cursor > blockquote:last-child > p:last-child::after {
1465
+ content: "";
1466
+ display: inline-block;
1467
+ width: 2px;
1468
+ height: 1em;
1469
+ background: var(--color-accent, #38bdf8);
1470
+ animation: sk-cursor-blink 0.8s steps(1) infinite;
1471
+ margin-left: 2px;
1472
+ vertical-align: text-bottom;
1473
+ }
1474
+ @keyframes sk-cursor-blink {
1475
+ 0%, 60% { opacity: 1; }
1476
+ 61%, 100% { opacity: 0; }
1477
+ }
1478
+ `;
1066
1479
  function StreamingMessage({
1067
1480
  stream,
1068
1481
  onComplete,
1482
+ onDraining,
1069
1483
  showPhases = true,
1070
1484
  className
1071
1485
  }) {
1072
- const onCompleteRef = (0, import_react6.useRef)(onComplete);
1486
+ const onCompleteRef = (0, import_react7.useRef)(onComplete);
1073
1487
  onCompleteRef.current = onComplete;
1074
- const wasActiveRef = (0, import_react6.useRef)(stream.active);
1075
- (0, import_react6.useEffect)(() => {
1488
+ const onDrainingRef = (0, import_react7.useRef)(onDraining);
1489
+ onDrainingRef.current = onDraining;
1490
+ const wasActiveRef = (0, import_react7.useRef)(stream.active);
1491
+ (0, import_react7.useEffect)(() => {
1076
1492
  if (wasActiveRef.current && !stream.active) {
1077
1493
  onCompleteRef.current?.();
1078
1494
  }
1079
1495
  wasActiveRef.current = stream.active;
1080
1496
  }, [stream.active]);
1081
1497
  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: [
1498
+ const { displayed: rawDisplayed, isDraining } = useCharacterDrain(stream.content);
1499
+ const displayedContent = stream.active || isDraining ? rawDisplayed.trimEnd() : rawDisplayed;
1500
+ (0, import_react7.useEffect)(() => {
1501
+ onDrainingRef.current?.(isDraining);
1502
+ }, [isDraining]);
1503
+ const agentLabel = stream.agent ? stream.agent.replace("_agent", "").replace("_", " ") : null;
1504
+ const showPhaseIndicator = showPhases && stream.active && stream.phase !== "idle" && !displayedContent;
1505
+ const showCursor = (stream.active || isDraining) && !!displayedContent;
1506
+ 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
1507
  /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { "aria-live": "assertive", className: "sr-only", children: [
1085
1508
  stream.active && stream.phase !== "idle" && "Response started",
1086
1509
  !stream.active && stream.content && "Response complete"
1087
1510
  ] }),
1511
+ showCursor && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("style", { children: CURSOR_STYLES }),
1512
+ 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
1513
  /* @__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)(
1514
+ showPhaseIndicator && /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(
1090
1515
  "div",
1091
1516
  {
1092
- className: "flex items-center gap-2 mb-2 text-sm text-text-secondary",
1517
+ className: "flex items-center gap-2 text-sm text-text-secondary",
1093
1518
  "data-testid": "phase-indicator",
1094
1519
  children: [
1095
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { "aria-hidden": "true", children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_core3.Spinner, { size: "sm" }) }),
1520
+ /* @__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
1521
  /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { children: phaseLabel })
1097
1522
  ]
1098
1523
  }
1099
1524
  ),
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
- ] })
1525
+ displayedContent && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
1526
+ ResponseMessage,
1527
+ {
1528
+ content: displayedContent,
1529
+ className: showCursor ? "sk-streaming-cursor" : void 0
1530
+ }
1531
+ )
1111
1532
  ] })
1112
1533
  ] });
1113
1534
  }
@@ -1138,7 +1559,7 @@ function AgentChat({
1138
1559
  return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(
1139
1560
  "div",
1140
1561
  {
1141
- className: (0, import_tailwind_merge8.twMerge)(
1562
+ className: (0, import_tailwind_merge9.twMerge)(
1142
1563
  "flex flex-col h-full bg-canvas border border-border rounded-xl overflow-hidden",
1143
1564
  className
1144
1565
  ),
@@ -1181,7 +1602,7 @@ function AgentChat({
1181
1602
  }
1182
1603
 
1183
1604
  // src/chat/ConversationList/ConversationList.tsx
1184
- var import_tailwind_merge9 = require("tailwind-merge");
1605
+ var import_tailwind_merge10 = require("tailwind-merge");
1185
1606
  var import_jsx_runtime13 = require("react/jsx-runtime");
1186
1607
  function ConversationList({
1187
1608
  conversations,
@@ -1195,7 +1616,7 @@ function ConversationList({
1195
1616
  "nav",
1196
1617
  {
1197
1618
  "aria-label": "Conversation list",
1198
- className: (0, import_tailwind_merge9.twMerge)("flex flex-col h-full bg-canvas", className),
1619
+ className: (0, import_tailwind_merge10.twMerge)("flex flex-col h-full bg-canvas", className),
1199
1620
  children: [
1200
1621
  onNew && /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { className: "p-3 border-b border-border", children: /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
1201
1622
  "button",
@@ -1212,7 +1633,7 @@ function ConversationList({
1212
1633
  return /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(
1213
1634
  "li",
1214
1635
  {
1215
- className: (0, import_tailwind_merge9.twMerge)(
1636
+ className: (0, import_tailwind_merge10.twMerge)(
1216
1637
  "flex items-start border-b border-border transition-colors duration-200",
1217
1638
  "hover:bg-surface",
1218
1639
  isActive && "bg-surface-raised border-l-2 border-l-accent"