@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.
- package/dist/chat/index.cjs +733 -222
- package/dist/chat/index.cjs.map +1 -1
- package/dist/chat/index.d.cts +13 -7
- package/dist/chat/index.d.ts +13 -7
- package/dist/chat/index.js +715 -204
- package/dist/chat/index.js.map +1 -1
- package/dist/{chat-ChYl2XjV.d.cts → chat-BRY3xGg_.d.cts} +11 -2
- package/dist/{chat--OifhIRe.d.ts → chat-CcKc6OAR.d.ts} +11 -2
- package/dist/{hooks-BGs8-4GK.d.ts → hooks-BLeiVk-x.d.ts} +22 -5
- package/dist/{hooks-DLfF18IU.d.cts → hooks-CSGGLd7j.d.cts} +22 -5
- package/dist/hooks.cjs +160 -85
- package/dist/hooks.cjs.map +1 -1
- package/dist/hooks.d.cts +3 -3
- package/dist/hooks.d.ts +3 -3
- package/dist/hooks.js +160 -85
- package/dist/hooks.js.map +1 -1
- package/dist/index.cjs +794 -283
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +758 -247
- package/dist/index.js.map +1 -1
- package/dist/layouts/index.cjs +754 -243
- package/dist/layouts/index.cjs.map +1 -1
- package/dist/layouts/index.d.cts +1 -1
- package/dist/layouts/index.d.ts +1 -1
- package/dist/layouts/index.js +733 -222
- package/dist/layouts/index.js.map +1 -1
- package/dist/mcp/index.cjs +1 -1
- package/dist/mcp/index.cjs.map +1 -1
- package/dist/mcp/index.js +2 -2
- package/dist/mcp/index.js.map +1 -1
- package/dist/response/index.cjs +100 -15
- package/dist/response/index.cjs.map +1 -1
- package/dist/response/index.d.cts +2 -2
- package/dist/response/index.d.ts +2 -2
- package/dist/response/index.js +99 -14
- package/dist/response/index.js.map +1 -1
- package/dist/sources/index.cjs +30 -1
- package/dist/sources/index.cjs.map +1 -1
- package/dist/sources/index.js +30 -1
- package/dist/sources/index.js.map +1 -1
- package/dist/streaming/index.cjs +213 -93
- package/dist/streaming/index.cjs.map +1 -1
- package/dist/streaming/index.d.cts +4 -3
- package/dist/streaming/index.d.ts +4 -3
- package/dist/streaming/index.js +183 -73
- package/dist/streaming/index.js.map +1 -1
- package/dist/{streaming-DfT22A0z.d.cts → streaming-BHPXnwwo.d.cts} +3 -1
- package/dist/{streaming-DbQxScpi.d.ts → streaming-C6mbU7My.d.ts} +3 -1
- package/package.json +17 -5
package/dist/chat/index.cjs
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
|
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
|
|
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 }) =>
|
|
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
|
-
|
|
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
|
|
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.
|
|
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,
|
|
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 &&
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
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.
|
|
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-
|
|
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:
|
|
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
|
|
800
|
-
(0,
|
|
801
|
-
|
|
802
|
-
|
|
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
|
-
|
|
815
|
-
|
|
816
|
-
{
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
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
|
|
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,
|
|
842
|
-
const
|
|
843
|
-
const
|
|
844
|
-
const
|
|
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,
|
|
1194
|
+
const handleSend = (0, import_react5.useCallback)(() => {
|
|
852
1195
|
if (!canSend) return;
|
|
853
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
1218
|
+
const capped = Math.min(el.scrollHeight, 200);
|
|
873
1219
|
el.style.height = `${capped}px`;
|
|
874
|
-
el.style.overflowY = el.scrollHeight >
|
|
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
|
-
"
|
|
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
|
-
"
|
|
1319
|
+
"input",
|
|
888
1320
|
{
|
|
889
|
-
ref:
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
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
|
-
|
|
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
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
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:
|
|
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:
|
|
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-
|
|
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-
|
|
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
|
|
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
|
|
1476
|
+
var import_react6 = require("react");
|
|
996
1477
|
function useCharacterDrain(target, msPerChar = 15) {
|
|
997
|
-
const [displayed, setDisplayed] = (0,
|
|
998
|
-
const indexRef = (0,
|
|
999
|
-
const lastTimeRef = (0,
|
|
1000
|
-
const rafRef = (0,
|
|
1001
|
-
const drainTargetRef = (0,
|
|
1002
|
-
const msPerCharRef = (0,
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
1576
|
+
const onCompleteRef = (0, import_react7.useRef)(onComplete);
|
|
1073
1577
|
onCompleteRef.current = onComplete;
|
|
1074
|
-
const
|
|
1075
|
-
|
|
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:
|
|
1083
|
-
|
|
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
|
-
|
|
1604
|
+
showPhaseIndicator && /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(
|
|
1090
1605
|
"div",
|
|
1091
1606
|
{
|
|
1092
|
-
className: "flex items-center gap-2
|
|
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.
|
|
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.
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
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,
|
|
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
|
|
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,
|
|
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: "
|
|
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
|
|
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,
|
|
1216
|
-
"flex items-start
|
|
1217
|
-
"hover:bg-surface",
|
|
1218
|
-
isActive
|
|
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-
|
|
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-
|
|
1230
|
-
/* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { className: "text-xs text-
|
|
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-
|
|
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-
|
|
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
|
}
|