@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.
- package/dist/chat/index.cjs +625 -204
- package/dist/chat/index.cjs.map +1 -1
- package/dist/chat/index.d.cts +11 -6
- package/dist/chat/index.d.ts +11 -6
- package/dist/chat/index.js +606 -185
- package/dist/chat/index.js.map +1 -1
- package/dist/{chat--OifhIRe.d.ts → chat-BIIDOGrD.d.ts} +10 -1
- package/dist/{chat-ChYl2XjV.d.cts → chat-CGamM7Mz.d.cts} +10 -1
- package/dist/{hooks-DLfF18IU.d.cts → hooks-B1NYoLLs.d.cts} +21 -5
- package/dist/{hooks-BGs8-4GK.d.ts → hooks-CTeEqnBQ.d.ts} +21 -5
- package/dist/hooks.cjs +126 -81
- 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 +126 -81
- package/dist/hooks.js.map +1 -1
- package/dist/index.cjs +686 -265
- 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 +645 -224
- package/dist/index.js.map +1 -1
- package/dist/layouts/index.cjs +646 -225
- 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 +622 -201
- 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 +66 -12
- 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 +64 -10
- 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 +202 -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 +172 -73
- package/dist/streaming/index.js.map +1 -1
- package/dist/{streaming-DbQxScpi.d.ts → streaming-Bx-ff2tt.d.ts} +1 -1
- package/dist/{streaming-DfT22A0z.d.cts → streaming-x7umFHoP.d.cts} +1 -1
- package/package.json +15 -4
package/dist/chat/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
// src/chat/AgentChat/AgentChat.tsx
|
|
4
|
-
import { twMerge as
|
|
4
|
+
import { twMerge as twMerge9 } from "tailwind-merge";
|
|
5
5
|
|
|
6
6
|
// src/hooks/useAgentChat.ts
|
|
7
7
|
import { useReducer, useCallback, useRef } from "react";
|
|
@@ -12,7 +12,8 @@ var initialState = {
|
|
|
12
12
|
error: null,
|
|
13
13
|
inputValue: "",
|
|
14
14
|
streamPhase: "idle",
|
|
15
|
-
streamingContent: ""
|
|
15
|
+
streamingContent: "",
|
|
16
|
+
streamingAgent: null
|
|
16
17
|
};
|
|
17
18
|
function reducer(state, action) {
|
|
18
19
|
switch (action.type) {
|
|
@@ -26,12 +27,15 @@ function reducer(state, action) {
|
|
|
26
27
|
error: null,
|
|
27
28
|
inputValue: "",
|
|
28
29
|
streamPhase: "thinking",
|
|
29
|
-
streamingContent: ""
|
|
30
|
+
streamingContent: "",
|
|
31
|
+
streamingAgent: null
|
|
30
32
|
};
|
|
31
33
|
case "STREAM_PHASE":
|
|
32
34
|
return { ...state, streamPhase: action.phase };
|
|
33
35
|
case "STREAM_CONTENT":
|
|
34
36
|
return { ...state, streamingContent: state.streamingContent + action.content };
|
|
37
|
+
case "STREAM_AGENT":
|
|
38
|
+
return { ...state, streamingAgent: action.agent };
|
|
35
39
|
case "SEND_SUCCESS":
|
|
36
40
|
return {
|
|
37
41
|
...state,
|
|
@@ -47,7 +51,8 @@ function reducer(state, action) {
|
|
|
47
51
|
isLoading: false,
|
|
48
52
|
error: action.error,
|
|
49
53
|
streamPhase: "idle",
|
|
50
|
-
streamingContent: ""
|
|
54
|
+
streamingContent: "",
|
|
55
|
+
streamingAgent: null
|
|
51
56
|
};
|
|
52
57
|
case "LOAD_CONVERSATION":
|
|
53
58
|
return {
|
|
@@ -73,107 +78,142 @@ function useAgentChat(config) {
|
|
|
73
78
|
const configRef = useRef(config);
|
|
74
79
|
configRef.current = config;
|
|
75
80
|
const lastUserMessageRef = useRef(null);
|
|
81
|
+
const lastUserAttachmentsRef = useRef(void 0);
|
|
76
82
|
const sendMessage = useCallback(
|
|
77
|
-
async (content) => {
|
|
78
|
-
const { apiUrl, streamPath = "/chat/stream", headers
|
|
83
|
+
async (content, attachments) => {
|
|
84
|
+
const { apiUrl, streamPath = "/chat/stream", headers: headersOrFn, timeout = 3e4, bodyExtra } = configRef.current;
|
|
85
|
+
const headers = typeof headersOrFn === "function" ? await headersOrFn() : headersOrFn ?? {};
|
|
79
86
|
lastUserMessageRef.current = content;
|
|
87
|
+
lastUserAttachmentsRef.current = attachments;
|
|
80
88
|
const userMessage = {
|
|
81
89
|
id: generateMessageId(),
|
|
82
90
|
role: "user",
|
|
83
91
|
content,
|
|
92
|
+
attachments,
|
|
84
93
|
timestamp: /* @__PURE__ */ new Date()
|
|
85
94
|
};
|
|
86
95
|
dispatch({ type: "SEND_START", message: userMessage });
|
|
87
96
|
const controller = new AbortController();
|
|
88
97
|
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
89
98
|
try {
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
error: {
|
|
108
|
-
code: "API_ERROR",
|
|
109
|
-
message: `HTTP ${response.status}: ${response.statusText}`,
|
|
110
|
-
retryable: response.status >= 500
|
|
111
|
-
}
|
|
112
|
-
});
|
|
113
|
-
return;
|
|
99
|
+
const url = `${apiUrl}${streamPath}`;
|
|
100
|
+
const mergedHeaders = {
|
|
101
|
+
"Content-Type": "application/json",
|
|
102
|
+
Accept: "text/event-stream",
|
|
103
|
+
...headers
|
|
104
|
+
};
|
|
105
|
+
const requestBody = {
|
|
106
|
+
message: content,
|
|
107
|
+
conversation_id: state.conversationId,
|
|
108
|
+
...bodyExtra
|
|
109
|
+
};
|
|
110
|
+
if (attachments && attachments.length > 0) {
|
|
111
|
+
requestBody.attachments = attachments.map((a) => ({
|
|
112
|
+
filename: a.filename,
|
|
113
|
+
content_type: a.content_type,
|
|
114
|
+
data: a.data
|
|
115
|
+
}));
|
|
114
116
|
}
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
117
|
+
const body = JSON.stringify(requestBody);
|
|
118
|
+
const ctx = {
|
|
119
|
+
accumulatedContent: "",
|
|
120
|
+
agentResponse: null,
|
|
121
|
+
capturedAgent: null,
|
|
122
|
+
capturedConversationId: null,
|
|
123
|
+
hadStreamError: false
|
|
124
|
+
};
|
|
125
|
+
const handleEvent = (event) => {
|
|
126
|
+
switch (event.type) {
|
|
127
|
+
case "agent":
|
|
128
|
+
ctx.capturedAgent = event.agent;
|
|
129
|
+
dispatch({ type: "STREAM_AGENT", agent: ctx.capturedAgent });
|
|
130
|
+
break;
|
|
131
|
+
case "phase":
|
|
132
|
+
dispatch({ type: "STREAM_PHASE", phase: event.phase });
|
|
133
|
+
break;
|
|
134
|
+
case "delta":
|
|
135
|
+
ctx.accumulatedContent += event.content;
|
|
136
|
+
dispatch({ type: "STREAM_CONTENT", content: event.content });
|
|
137
|
+
break;
|
|
138
|
+
case "done":
|
|
139
|
+
ctx.agentResponse = event.response;
|
|
140
|
+
ctx.capturedConversationId = event.conversation_id ?? null;
|
|
141
|
+
break;
|
|
142
|
+
case "error":
|
|
143
|
+
ctx.hadStreamError = true;
|
|
144
|
+
dispatch({ type: "SEND_ERROR", error: event.error });
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
const { streamAdapter } = configRef.current;
|
|
149
|
+
if (streamAdapter) {
|
|
150
|
+
await streamAdapter(
|
|
151
|
+
url,
|
|
152
|
+
{ method: "POST", headers: mergedHeaders, body, signal: controller.signal },
|
|
153
|
+
handleEvent
|
|
154
|
+
);
|
|
155
|
+
clearTimeout(timeoutId);
|
|
156
|
+
} else {
|
|
157
|
+
const response = await fetch(url, {
|
|
158
|
+
method: "POST",
|
|
159
|
+
headers: mergedHeaders,
|
|
160
|
+
body,
|
|
161
|
+
signal: controller.signal
|
|
120
162
|
});
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
dispatch({ type: "SEND_ERROR", error: event.error });
|
|
158
|
-
return;
|
|
163
|
+
clearTimeout(timeoutId);
|
|
164
|
+
if (!response.ok) {
|
|
165
|
+
dispatch({
|
|
166
|
+
type: "SEND_ERROR",
|
|
167
|
+
error: {
|
|
168
|
+
code: "API_ERROR",
|
|
169
|
+
message: `HTTP ${response.status}: ${response.statusText}`,
|
|
170
|
+
retryable: response.status >= 500
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
const reader = response.body?.getReader();
|
|
176
|
+
if (!reader) {
|
|
177
|
+
dispatch({
|
|
178
|
+
type: "SEND_ERROR",
|
|
179
|
+
error: { code: "STREAM_ERROR", message: "No response body", retryable: true }
|
|
180
|
+
});
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
const decoder = new TextDecoder();
|
|
184
|
+
let buffer = "";
|
|
185
|
+
while (true) {
|
|
186
|
+
const { done, value } = await reader.read();
|
|
187
|
+
if (done) break;
|
|
188
|
+
buffer += decoder.decode(value, { stream: true });
|
|
189
|
+
const lines = buffer.split("\n");
|
|
190
|
+
buffer = lines.pop() ?? "";
|
|
191
|
+
for (const line of lines) {
|
|
192
|
+
if (!line.startsWith("data: ")) continue;
|
|
193
|
+
const data = line.slice(6).trim();
|
|
194
|
+
if (data === "[DONE]") continue;
|
|
195
|
+
try {
|
|
196
|
+
const event = JSON.parse(data);
|
|
197
|
+
handleEvent(event);
|
|
198
|
+
} catch {
|
|
159
199
|
}
|
|
160
|
-
} catch {
|
|
161
200
|
}
|
|
162
201
|
}
|
|
163
202
|
}
|
|
203
|
+
if (ctx.hadStreamError) return;
|
|
164
204
|
const assistantMessage = {
|
|
165
205
|
id: generateMessageId(),
|
|
166
206
|
role: "assistant",
|
|
167
|
-
content: agentResponse?.message ?? accumulatedContent,
|
|
168
|
-
response: agentResponse ?? void 0,
|
|
169
|
-
agent: capturedAgent ?? void 0,
|
|
207
|
+
content: ctx.agentResponse?.message ?? ctx.accumulatedContent,
|
|
208
|
+
response: ctx.agentResponse ?? void 0,
|
|
209
|
+
agent: ctx.capturedAgent ?? void 0,
|
|
170
210
|
timestamp: /* @__PURE__ */ new Date()
|
|
171
211
|
};
|
|
172
212
|
dispatch({
|
|
173
213
|
type: "SEND_SUCCESS",
|
|
174
214
|
message: assistantMessage,
|
|
175
|
-
streamingContent: accumulatedContent,
|
|
176
|
-
conversationId: capturedConversationId
|
|
215
|
+
streamingContent: ctx.accumulatedContent,
|
|
216
|
+
conversationId: ctx.capturedConversationId
|
|
177
217
|
});
|
|
178
218
|
} catch (err) {
|
|
179
219
|
clearTimeout(timeoutId);
|
|
@@ -204,7 +244,8 @@ function useAgentChat(config) {
|
|
|
204
244
|
}, []);
|
|
205
245
|
const submitFeedback = useCallback(
|
|
206
246
|
async (messageId, rating, comment) => {
|
|
207
|
-
const { apiUrl, feedbackPath = "/feedback", headers
|
|
247
|
+
const { apiUrl, feedbackPath = "/feedback", headers: headersOrFn } = configRef.current;
|
|
248
|
+
const headers = typeof headersOrFn === "function" ? await headersOrFn() : headersOrFn ?? {};
|
|
208
249
|
await fetch(`${apiUrl}${feedbackPath}`, {
|
|
209
250
|
method: "POST",
|
|
210
251
|
headers: { "Content-Type": "application/json", ...headers },
|
|
@@ -215,12 +256,13 @@ function useAgentChat(config) {
|
|
|
215
256
|
);
|
|
216
257
|
const retry = useCallback(async () => {
|
|
217
258
|
if (lastUserMessageRef.current) {
|
|
218
|
-
await sendMessage(lastUserMessageRef.current);
|
|
259
|
+
await sendMessage(lastUserMessageRef.current, lastUserAttachmentsRef.current);
|
|
219
260
|
}
|
|
220
261
|
}, [sendMessage]);
|
|
221
262
|
const reset = useCallback(() => {
|
|
222
263
|
dispatch({ type: "RESET" });
|
|
223
264
|
lastUserMessageRef.current = null;
|
|
265
|
+
lastUserAttachmentsRef.current = void 0;
|
|
224
266
|
}, []);
|
|
225
267
|
const actions = {
|
|
226
268
|
sendMessage,
|
|
@@ -235,7 +277,7 @@ function useAgentChat(config) {
|
|
|
235
277
|
|
|
236
278
|
// src/chat/MessageThread/MessageThread.tsx
|
|
237
279
|
import { twMerge as twMerge5 } from "tailwind-merge";
|
|
238
|
-
import { useEffect, useRef as useRef2 } from "react";
|
|
280
|
+
import { useCallback as useCallback2, useEffect, useRef as useRef2 } from "react";
|
|
239
281
|
|
|
240
282
|
// src/chat/MessageBubble/MessageBubble.tsx
|
|
241
283
|
import { twMerge as twMerge4 } from "tailwind-merge";
|
|
@@ -244,6 +286,7 @@ import { twMerge as twMerge4 } from "tailwind-merge";
|
|
|
244
286
|
import { Badge as Badge2 } from "@surf-kit/core";
|
|
245
287
|
|
|
246
288
|
// src/response/ResponseMessage/ResponseMessage.tsx
|
|
289
|
+
import React from "react";
|
|
247
290
|
import ReactMarkdown from "react-markdown";
|
|
248
291
|
import rehypeSanitize from "rehype-sanitize";
|
|
249
292
|
import { twMerge } from "tailwind-merge";
|
|
@@ -268,6 +311,7 @@ function ResponseMessage({ content, className }) {
|
|
|
268
311
|
"[&_h3]:text-sm [&_h3]:font-semibold [&_h3]:text-accent [&_h3]:mt-2 [&_h3]:mb-1",
|
|
269
312
|
"[&_code]:bg-surface-raised [&_code]:text-accent [&_code]:px-1.5 [&_code]:py-0.5 [&_code]:rounded [&_code]:text-xs [&_code]:font-mono",
|
|
270
313
|
"[&_pre]:bg-surface-raised [&_pre]:border [&_pre]:border-border [&_pre]:rounded-xl [&_pre]:p-4 [&_pre]:overflow-x-auto",
|
|
314
|
+
"[&_hr]:my-3 [&_hr]:border-border",
|
|
271
315
|
"[&_blockquote]:border-l-2 [&_blockquote]:border-border-strong [&_blockquote]:pl-4 [&_blockquote]:text-text-secondary",
|
|
272
316
|
"[&_a]:text-accent [&_a]:underline-offset-2 [&_a]:hover:text-accent/80",
|
|
273
317
|
className
|
|
@@ -283,11 +327,24 @@ function ResponseMessage({ content, className }) {
|
|
|
283
327
|
p: ({ children }) => /* @__PURE__ */ jsx("p", { className: "my-2", children }),
|
|
284
328
|
ul: ({ children }) => /* @__PURE__ */ jsx("ul", { className: "my-2 list-disc pl-6", children }),
|
|
285
329
|
ol: ({ children }) => /* @__PURE__ */ jsx("ol", { className: "my-2 list-decimal pl-6", children }),
|
|
286
|
-
li: ({ children }) =>
|
|
330
|
+
li: ({ children, ...props }) => {
|
|
331
|
+
let content2 = children;
|
|
332
|
+
if (props.ordered) {
|
|
333
|
+
content2 = React.Children.map(children, (child, i) => {
|
|
334
|
+
if (i === 0 && typeof child === "string") {
|
|
335
|
+
return child.replace(/^\d+[.)]\s*/, "");
|
|
336
|
+
}
|
|
337
|
+
return child;
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
return /* @__PURE__ */ jsx("li", { className: "my-1", children: content2 });
|
|
341
|
+
},
|
|
287
342
|
strong: ({ children }) => /* @__PURE__ */ jsx("strong", { className: "font-semibold", children }),
|
|
343
|
+
em: ({ children }) => /* @__PURE__ */ jsx("em", { className: "italic text-text-secondary", children }),
|
|
288
344
|
h1: ({ children }) => /* @__PURE__ */ jsx("h1", { className: "text-base font-bold mt-4 mb-2", children }),
|
|
289
345
|
h2: ({ children }) => /* @__PURE__ */ jsx("h2", { className: "text-sm font-bold mt-3 mb-1", children }),
|
|
290
346
|
h3: ({ children }) => /* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold mt-2 mb-1", children }),
|
|
347
|
+
hr: () => /* @__PURE__ */ jsx("hr", { className: "my-3 border-border" }),
|
|
291
348
|
code: ({ children }) => /* @__PURE__ */ jsx("code", { className: "bg-surface-sunken rounded px-1 py-0.5 text-xs font-mono", children })
|
|
292
349
|
},
|
|
293
350
|
children: normalizeMarkdownLists(content)
|
|
@@ -423,7 +480,14 @@ function renderWarning(data) {
|
|
|
423
480
|
}
|
|
424
481
|
);
|
|
425
482
|
}
|
|
426
|
-
function StructuredResponse({ uiHint, data, className }) {
|
|
483
|
+
function StructuredResponse({ uiHint, data: rawData, className }) {
|
|
484
|
+
const data = typeof rawData === "string" ? (() => {
|
|
485
|
+
try {
|
|
486
|
+
return JSON.parse(rawData);
|
|
487
|
+
} catch {
|
|
488
|
+
return null;
|
|
489
|
+
}
|
|
490
|
+
})() : rawData;
|
|
427
491
|
if (!data) return null;
|
|
428
492
|
let content;
|
|
429
493
|
switch (uiHint) {
|
|
@@ -503,7 +567,36 @@ function SourceCard({ source, variant = "compact", onNavigate, className }) {
|
|
|
503
567
|
children: [
|
|
504
568
|
/* @__PURE__ */ jsxs2("div", { className: "flex items-start justify-between gap-2", children: [
|
|
505
569
|
/* @__PURE__ */ jsxs2("div", { className: "flex-1 min-w-0", children: [
|
|
506
|
-
/* @__PURE__ */
|
|
570
|
+
source.url ? /* @__PURE__ */ jsxs2(
|
|
571
|
+
"a",
|
|
572
|
+
{
|
|
573
|
+
href: source.url,
|
|
574
|
+
target: "_blank",
|
|
575
|
+
rel: "noopener noreferrer",
|
|
576
|
+
className: "text-sm font-medium text-accent hover:underline truncate block",
|
|
577
|
+
onClick: (e) => e.stopPropagation(),
|
|
578
|
+
children: [
|
|
579
|
+
source.title,
|
|
580
|
+
/* @__PURE__ */ jsxs2(
|
|
581
|
+
"svg",
|
|
582
|
+
{
|
|
583
|
+
className: "inline-block ml-1 w-3 h-3 opacity-60",
|
|
584
|
+
viewBox: "0 0 24 24",
|
|
585
|
+
fill: "none",
|
|
586
|
+
stroke: "currentColor",
|
|
587
|
+
strokeWidth: "2",
|
|
588
|
+
strokeLinecap: "round",
|
|
589
|
+
strokeLinejoin: "round",
|
|
590
|
+
children: [
|
|
591
|
+
/* @__PURE__ */ jsx3("path", { d: "M18 13v6a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h6" }),
|
|
592
|
+
/* @__PURE__ */ jsx3("polyline", { points: "15 3 21 3 21 9" }),
|
|
593
|
+
/* @__PURE__ */ jsx3("line", { x1: "10", y1: "14", x2: "21", y2: "3" })
|
|
594
|
+
]
|
|
595
|
+
}
|
|
596
|
+
)
|
|
597
|
+
]
|
|
598
|
+
}
|
|
599
|
+
) : /* @__PURE__ */ jsx3("p", { className: "text-sm font-medium text-text-primary truncate", children: source.title }),
|
|
507
600
|
source.section && /* @__PURE__ */ jsx3("p", { className: "text-[11px] font-semibold uppercase tracking-wider text-text-secondary truncate mt-0.5", children: source.section })
|
|
508
601
|
] }),
|
|
509
602
|
/* @__PURE__ */ jsx3(
|
|
@@ -637,13 +730,16 @@ function AgentResponse({
|
|
|
637
730
|
}) {
|
|
638
731
|
return /* @__PURE__ */ jsxs4("div", { className: `flex flex-col gap-4 ${className ?? ""}`, "data-testid": "agent-response", children: [
|
|
639
732
|
/* @__PURE__ */ jsx6(ResponseMessage, { content: response.message }),
|
|
640
|
-
response.ui_hint !== "text" && response.structured_data &&
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
733
|
+
response.ui_hint !== "text" && response.structured_data && (() => {
|
|
734
|
+
const parsed = typeof response.structured_data === "string" ? (() => {
|
|
735
|
+
try {
|
|
736
|
+
return JSON.parse(response.structured_data);
|
|
737
|
+
} catch {
|
|
738
|
+
return null;
|
|
739
|
+
}
|
|
740
|
+
})() : response.structured_data;
|
|
741
|
+
return parsed ? /* @__PURE__ */ jsx6(StructuredResponse, { uiHint: response.ui_hint, data: parsed }) : null;
|
|
742
|
+
})(),
|
|
647
743
|
(showConfidence || showVerification) && /* @__PURE__ */ jsxs4("div", { className: "flex flex-wrap items-center gap-2 mt-1", "data-testid": "response-meta", children: [
|
|
648
744
|
showConfidence && /* @__PURE__ */ jsxs4(
|
|
649
745
|
Badge2,
|
|
@@ -694,6 +790,31 @@ function AgentResponse({
|
|
|
694
790
|
|
|
695
791
|
// src/chat/MessageBubble/MessageBubble.tsx
|
|
696
792
|
import { jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
793
|
+
function DocumentIcon() {
|
|
794
|
+
return /* @__PURE__ */ jsxs5("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
795
|
+
/* @__PURE__ */ jsx7("path", { d: "M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z" }),
|
|
796
|
+
/* @__PURE__ */ jsx7("polyline", { points: "14 2 14 8 20 8" }),
|
|
797
|
+
/* @__PURE__ */ jsx7("line", { x1: "16", y1: "13", x2: "8", y2: "13" }),
|
|
798
|
+
/* @__PURE__ */ jsx7("line", { x1: "16", y1: "17", x2: "8", y2: "17" })
|
|
799
|
+
] });
|
|
800
|
+
}
|
|
801
|
+
function AttachmentThumbnail({ attachment }) {
|
|
802
|
+
const isImage = attachment.content_type.startsWith("image/");
|
|
803
|
+
if (isImage) {
|
|
804
|
+
return /* @__PURE__ */ jsx7("div", { className: "rounded-lg overflow-hidden border border-black/10 max-w-[240px]", children: /* @__PURE__ */ jsx7(
|
|
805
|
+
"img",
|
|
806
|
+
{
|
|
807
|
+
src: attachment.preview_url ?? `data:${attachment.content_type};base64,${attachment.data}`,
|
|
808
|
+
alt: attachment.filename,
|
|
809
|
+
className: "max-w-full max-h-[200px] object-contain"
|
|
810
|
+
}
|
|
811
|
+
) });
|
|
812
|
+
}
|
|
813
|
+
return /* @__PURE__ */ jsxs5("div", { className: "flex items-center gap-2 px-3 py-2 rounded-lg border border-black/10 bg-black/5", children: [
|
|
814
|
+
/* @__PURE__ */ jsx7(DocumentIcon, {}),
|
|
815
|
+
/* @__PURE__ */ jsx7("span", { className: "text-xs truncate max-w-[160px]", children: attachment.filename })
|
|
816
|
+
] });
|
|
817
|
+
}
|
|
697
818
|
function MessageBubble({
|
|
698
819
|
message,
|
|
699
820
|
showAgent,
|
|
@@ -701,23 +822,29 @@ function MessageBubble({
|
|
|
701
822
|
showConfidence = true,
|
|
702
823
|
showVerification = true,
|
|
703
824
|
animated = true,
|
|
825
|
+
userBubbleClassName,
|
|
704
826
|
className
|
|
705
827
|
}) {
|
|
706
828
|
const isUser = message.role === "user";
|
|
829
|
+
const hasAttachments = message.attachments && message.attachments.length > 0;
|
|
707
830
|
if (isUser) {
|
|
708
831
|
return /* @__PURE__ */ jsx7(
|
|
709
832
|
"div",
|
|
710
833
|
{
|
|
711
834
|
"data-message-id": message.id,
|
|
712
835
|
className: twMerge4("flex w-full justify-end", className),
|
|
713
|
-
children: /* @__PURE__ */
|
|
836
|
+
children: /* @__PURE__ */ jsxs5(
|
|
714
837
|
"div",
|
|
715
838
|
{
|
|
716
839
|
className: twMerge4(
|
|
717
|
-
"max-w-[70%] rounded-[18px] rounded-br-[4px] px-4 py-2.5 bg-
|
|
718
|
-
animated && "motion-safe:animate-slideFromRight"
|
|
840
|
+
"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",
|
|
841
|
+
animated && "motion-safe:animate-slideFromRight",
|
|
842
|
+
userBubbleClassName
|
|
719
843
|
),
|
|
720
|
-
children:
|
|
844
|
+
children: [
|
|
845
|
+
hasAttachments && /* @__PURE__ */ jsx7("div", { className: "flex flex-wrap gap-2 mb-2", children: message.attachments.map((att, i) => /* @__PURE__ */ jsx7(AttachmentThumbnail, { attachment: att }, `${att.filename}-${i}`)) }),
|
|
846
|
+
message.content
|
|
847
|
+
]
|
|
721
848
|
}
|
|
722
849
|
)
|
|
723
850
|
}
|
|
@@ -729,7 +856,7 @@ function MessageBubble({
|
|
|
729
856
|
"data-message-id": message.id,
|
|
730
857
|
className: twMerge4("flex w-full flex-col items-start gap-1.5", className),
|
|
731
858
|
children: [
|
|
732
|
-
showAgent && message.agent && /* @__PURE__ */ jsx7("div", { className: "text-[11px] font-semibold uppercase tracking-[0.08em] text-text-muted px-1", children: message.agent.replace("_agent", "").replace("_", " ") }),
|
|
859
|
+
showAgent && message.agent && /* @__PURE__ */ jsx7("div", { className: "text-[11px] font-display font-semibold uppercase tracking-[0.08em] text-text-muted px-1", children: message.agent.replace("_agent", "").replace("_", " ") }),
|
|
733
860
|
/* @__PURE__ */ jsx7(
|
|
734
861
|
"div",
|
|
735
862
|
{
|
|
@@ -755,34 +882,70 @@ function MessageBubble({
|
|
|
755
882
|
|
|
756
883
|
// src/chat/MessageThread/MessageThread.tsx
|
|
757
884
|
import { jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
758
|
-
function MessageThread({ messages, streamingSlot, showSources, showConfidence, showVerification, className }) {
|
|
759
|
-
const
|
|
885
|
+
function MessageThread({ messages, streamingSlot, showAgent, showSources, showConfidence, showVerification, hideLastAssistant, userBubbleClassName, className }) {
|
|
886
|
+
const scrollRef = useRef2(null);
|
|
887
|
+
const isNearBottom = useRef2(true);
|
|
888
|
+
const isProgrammaticScroll = useRef2(false);
|
|
889
|
+
const hasStreaming = !!streamingSlot;
|
|
890
|
+
const scrollToBottom = useCallback2(() => {
|
|
891
|
+
const el = scrollRef.current;
|
|
892
|
+
if (el && isNearBottom.current) {
|
|
893
|
+
isProgrammaticScroll.current = true;
|
|
894
|
+
el.scrollTop = el.scrollHeight;
|
|
895
|
+
}
|
|
896
|
+
}, []);
|
|
897
|
+
const handleScroll = useCallback2(() => {
|
|
898
|
+
if (isProgrammaticScroll.current) {
|
|
899
|
+
isProgrammaticScroll.current = false;
|
|
900
|
+
return;
|
|
901
|
+
}
|
|
902
|
+
const el = scrollRef.current;
|
|
903
|
+
if (!el) return;
|
|
904
|
+
isNearBottom.current = el.scrollHeight - el.scrollTop - el.clientHeight < 80;
|
|
905
|
+
}, []);
|
|
906
|
+
useEffect(scrollToBottom, [messages.length, scrollToBottom]);
|
|
760
907
|
useEffect(() => {
|
|
761
|
-
|
|
762
|
-
|
|
908
|
+
if (!hasStreaming) return;
|
|
909
|
+
let raf;
|
|
910
|
+
const tick = () => {
|
|
911
|
+
scrollToBottom();
|
|
912
|
+
raf = requestAnimationFrame(tick);
|
|
913
|
+
};
|
|
914
|
+
raf = requestAnimationFrame(tick);
|
|
915
|
+
return () => cancelAnimationFrame(raf);
|
|
916
|
+
}, [hasStreaming, scrollToBottom]);
|
|
763
917
|
return /* @__PURE__ */ jsxs6(
|
|
764
918
|
"div",
|
|
765
919
|
{
|
|
920
|
+
ref: scrollRef,
|
|
766
921
|
role: "log",
|
|
767
922
|
"aria-live": "polite",
|
|
768
923
|
"aria-label": "Message thread",
|
|
924
|
+
onScroll: handleScroll,
|
|
769
925
|
className: twMerge5(
|
|
770
926
|
"flex flex-col gap-4 overflow-y-auto flex-1 px-4 py-6",
|
|
771
927
|
className
|
|
772
928
|
),
|
|
773
929
|
children: [
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
{
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
930
|
+
/* @__PURE__ */ jsx8("div", { className: "flex-1 shrink-0" }),
|
|
931
|
+
messages.map((message, i) => {
|
|
932
|
+
if (hideLastAssistant && i === messages.length - 1 && message.role === "assistant") {
|
|
933
|
+
return null;
|
|
934
|
+
}
|
|
935
|
+
return /* @__PURE__ */ jsx8(
|
|
936
|
+
MessageBubble,
|
|
937
|
+
{
|
|
938
|
+
message,
|
|
939
|
+
showAgent,
|
|
940
|
+
showSources,
|
|
941
|
+
showConfidence,
|
|
942
|
+
showVerification,
|
|
943
|
+
userBubbleClassName
|
|
944
|
+
},
|
|
945
|
+
message.id
|
|
946
|
+
);
|
|
947
|
+
}),
|
|
948
|
+
streamingSlot
|
|
786
949
|
]
|
|
787
950
|
}
|
|
788
951
|
);
|
|
@@ -790,8 +953,96 @@ function MessageThread({ messages, streamingSlot, showSources, showConfidence, s
|
|
|
790
953
|
|
|
791
954
|
// src/chat/MessageComposer/MessageComposer.tsx
|
|
792
955
|
import { twMerge as twMerge6 } from "tailwind-merge";
|
|
793
|
-
import { useState as useState2, useRef as useRef3, useCallback as
|
|
956
|
+
import { useState as useState2, useRef as useRef3, useCallback as useCallback3 } from "react";
|
|
794
957
|
import { jsx as jsx9, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
958
|
+
var ALLOWED_TYPES = /* @__PURE__ */ new Set([
|
|
959
|
+
"image/png",
|
|
960
|
+
"image/jpeg",
|
|
961
|
+
"image/gif",
|
|
962
|
+
"image/webp",
|
|
963
|
+
"application/pdf"
|
|
964
|
+
]);
|
|
965
|
+
var MAX_FILE_SIZE = 10 * 1024 * 1024;
|
|
966
|
+
var MAX_ATTACHMENTS = 5;
|
|
967
|
+
function ArrowUpIcon() {
|
|
968
|
+
return /* @__PURE__ */ jsxs7("svg", { width: "20", height: "20", viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
969
|
+
/* @__PURE__ */ jsx9("path", { d: "M10 16V4" }),
|
|
970
|
+
/* @__PURE__ */ jsx9("path", { d: "M4 10l6-6 6 6" })
|
|
971
|
+
] });
|
|
972
|
+
}
|
|
973
|
+
function StopIcon() {
|
|
974
|
+
return /* @__PURE__ */ jsx9("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "currentColor", children: /* @__PURE__ */ jsx9("rect", { x: "3", y: "3", width: "10", height: "10", rx: "2" }) });
|
|
975
|
+
}
|
|
976
|
+
function PaperclipIcon() {
|
|
977
|
+
return /* @__PURE__ */ jsx9("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ jsx9("path", { d: "M21.44 11.05l-9.19 9.19a6 6 0 01-8.49-8.49l9.19-9.19a4 4 0 015.66 5.66l-9.2 9.19a2 2 0 01-2.83-2.83l8.49-8.48" }) });
|
|
978
|
+
}
|
|
979
|
+
function XIcon({ size = 14 }) {
|
|
980
|
+
return /* @__PURE__ */ jsxs7("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
981
|
+
/* @__PURE__ */ jsx9("path", { d: "M18 6L6 18" }),
|
|
982
|
+
/* @__PURE__ */ jsx9("path", { d: "M6 6l12 12" })
|
|
983
|
+
] });
|
|
984
|
+
}
|
|
985
|
+
function DocumentIcon2() {
|
|
986
|
+
return /* @__PURE__ */ jsxs7("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
987
|
+
/* @__PURE__ */ jsx9("path", { d: "M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z" }),
|
|
988
|
+
/* @__PURE__ */ jsx9("polyline", { points: "14 2 14 8 20 8" }),
|
|
989
|
+
/* @__PURE__ */ jsx9("line", { x1: "16", y1: "13", x2: "8", y2: "13" }),
|
|
990
|
+
/* @__PURE__ */ jsx9("line", { x1: "16", y1: "17", x2: "8", y2: "17" }),
|
|
991
|
+
/* @__PURE__ */ jsx9("polyline", { points: "10 9 9 9 8 9" })
|
|
992
|
+
] });
|
|
993
|
+
}
|
|
994
|
+
function fileToBase64(file) {
|
|
995
|
+
return new Promise((resolve, reject) => {
|
|
996
|
+
const reader = new FileReader();
|
|
997
|
+
reader.onload = () => {
|
|
998
|
+
const result = reader.result;
|
|
999
|
+
const base64 = result.split(",")[1];
|
|
1000
|
+
resolve(base64);
|
|
1001
|
+
};
|
|
1002
|
+
reader.onerror = reject;
|
|
1003
|
+
reader.readAsDataURL(file);
|
|
1004
|
+
});
|
|
1005
|
+
}
|
|
1006
|
+
function AttachmentPreview({
|
|
1007
|
+
attachment,
|
|
1008
|
+
onRemove
|
|
1009
|
+
}) {
|
|
1010
|
+
const isImage = attachment.content_type.startsWith("image/");
|
|
1011
|
+
return /* @__PURE__ */ jsxs7("div", { className: "relative group flex-shrink-0", children: [
|
|
1012
|
+
isImage ? /* @__PURE__ */ jsx9("div", { className: "w-16 h-16 rounded-lg overflow-hidden border border-border/60 bg-surface-alt", children: /* @__PURE__ */ jsx9(
|
|
1013
|
+
"img",
|
|
1014
|
+
{
|
|
1015
|
+
src: attachment.preview_url ?? `data:${attachment.content_type};base64,${attachment.data}`,
|
|
1016
|
+
alt: attachment.filename,
|
|
1017
|
+
className: "w-full h-full object-cover"
|
|
1018
|
+
}
|
|
1019
|
+
) }) : /* @__PURE__ */ jsxs7("div", { className: "h-16 px-3 rounded-lg border border-border/60 bg-surface-alt flex items-center gap-2", children: [
|
|
1020
|
+
/* @__PURE__ */ jsx9("div", { className: "text-text-muted", children: /* @__PURE__ */ jsx9(DocumentIcon2, {}) }),
|
|
1021
|
+
/* @__PURE__ */ jsxs7("div", { className: "flex flex-col min-w-0", children: [
|
|
1022
|
+
/* @__PURE__ */ jsx9("span", { className: "text-xs text-text-primary truncate max-w-[120px]", children: attachment.filename }),
|
|
1023
|
+
/* @__PURE__ */ jsx9("span", { className: "text-[10px] text-text-muted", children: "PDF" })
|
|
1024
|
+
] })
|
|
1025
|
+
] }),
|
|
1026
|
+
/* @__PURE__ */ jsx9(
|
|
1027
|
+
"button",
|
|
1028
|
+
{
|
|
1029
|
+
type: "button",
|
|
1030
|
+
onClick: onRemove,
|
|
1031
|
+
className: twMerge6(
|
|
1032
|
+
"absolute -top-1.5 -right-1.5",
|
|
1033
|
+
"w-5 h-5 rounded-full",
|
|
1034
|
+
"bg-text-muted/80 text-white",
|
|
1035
|
+
"flex items-center justify-center",
|
|
1036
|
+
"opacity-0 group-hover:opacity-100",
|
|
1037
|
+
"transition-opacity duration-150",
|
|
1038
|
+
"hover:bg-text-primary"
|
|
1039
|
+
),
|
|
1040
|
+
"aria-label": `Remove ${attachment.filename}`,
|
|
1041
|
+
children: /* @__PURE__ */ jsx9(XIcon, { size: 10 })
|
|
1042
|
+
}
|
|
1043
|
+
)
|
|
1044
|
+
] });
|
|
1045
|
+
}
|
|
795
1046
|
function MessageComposer({
|
|
796
1047
|
onSend,
|
|
797
1048
|
isLoading = false,
|
|
@@ -799,23 +1050,29 @@ function MessageComposer({
|
|
|
799
1050
|
className
|
|
800
1051
|
}) {
|
|
801
1052
|
const [value, setValue] = useState2("");
|
|
1053
|
+
const [attachments, setAttachments] = useState2([]);
|
|
1054
|
+
const [dragOver, setDragOver] = useState2(false);
|
|
802
1055
|
const textareaRef = useRef3(null);
|
|
803
|
-
const
|
|
804
|
-
const
|
|
1056
|
+
const fileInputRef = useRef3(null);
|
|
1057
|
+
const canSend = (value.trim().length > 0 || attachments.length > 0) && !isLoading;
|
|
1058
|
+
const resetHeight = useCallback3(() => {
|
|
805
1059
|
const el = textareaRef.current;
|
|
806
1060
|
if (el) {
|
|
807
1061
|
el.style.height = "auto";
|
|
808
1062
|
el.style.overflowY = "hidden";
|
|
809
1063
|
}
|
|
810
1064
|
}, []);
|
|
811
|
-
const handleSend =
|
|
1065
|
+
const handleSend = useCallback3(() => {
|
|
812
1066
|
if (!canSend) return;
|
|
813
|
-
|
|
1067
|
+
const message = value.trim() || (attachments.length > 0 ? "Please analyse the attached file(s)." : "");
|
|
1068
|
+
if (!message && attachments.length === 0) return;
|
|
1069
|
+
onSend(message, attachments.length > 0 ? attachments : void 0);
|
|
814
1070
|
setValue("");
|
|
1071
|
+
setAttachments([]);
|
|
815
1072
|
resetHeight();
|
|
816
1073
|
textareaRef.current?.focus();
|
|
817
|
-
}, [canSend, onSend, value, resetHeight]);
|
|
818
|
-
const handleKeyDown =
|
|
1074
|
+
}, [canSend, onSend, value, attachments, resetHeight]);
|
|
1075
|
+
const handleKeyDown = useCallback3(
|
|
819
1076
|
(e) => {
|
|
820
1077
|
if (e.key === "Enter" && !e.shiftKey) {
|
|
821
1078
|
e.preventDefault();
|
|
@@ -824,64 +1081,194 @@ function MessageComposer({
|
|
|
824
1081
|
},
|
|
825
1082
|
[handleSend]
|
|
826
1083
|
);
|
|
827
|
-
const handleChange =
|
|
1084
|
+
const handleChange = useCallback3(
|
|
828
1085
|
(e) => {
|
|
829
1086
|
setValue(e.target.value);
|
|
830
1087
|
const el = e.target;
|
|
831
1088
|
el.style.height = "auto";
|
|
832
|
-
const capped = Math.min(el.scrollHeight,
|
|
1089
|
+
const capped = Math.min(el.scrollHeight, 200);
|
|
833
1090
|
el.style.height = `${capped}px`;
|
|
834
|
-
el.style.overflowY = el.scrollHeight >
|
|
1091
|
+
el.style.overflowY = el.scrollHeight > 200 ? "auto" : "hidden";
|
|
835
1092
|
},
|
|
836
1093
|
[]
|
|
837
1094
|
);
|
|
1095
|
+
const addFiles = useCallback3(async (files) => {
|
|
1096
|
+
const fileArray = Array.from(files);
|
|
1097
|
+
for (const file of fileArray) {
|
|
1098
|
+
if (attachments.length >= MAX_ATTACHMENTS) break;
|
|
1099
|
+
if (!ALLOWED_TYPES.has(file.type)) continue;
|
|
1100
|
+
if (file.size > MAX_FILE_SIZE) continue;
|
|
1101
|
+
try {
|
|
1102
|
+
const data = await fileToBase64(file);
|
|
1103
|
+
const previewUrl = file.type.startsWith("image/") ? URL.createObjectURL(file) : void 0;
|
|
1104
|
+
const attachment = {
|
|
1105
|
+
filename: file.name,
|
|
1106
|
+
content_type: file.type,
|
|
1107
|
+
data,
|
|
1108
|
+
preview_url: previewUrl
|
|
1109
|
+
};
|
|
1110
|
+
setAttachments((prev) => {
|
|
1111
|
+
if (prev.length >= MAX_ATTACHMENTS) return prev;
|
|
1112
|
+
return [...prev, attachment];
|
|
1113
|
+
});
|
|
1114
|
+
} catch {
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
}, [attachments.length]);
|
|
1118
|
+
const handleFileSelect = useCallback3(() => {
|
|
1119
|
+
fileInputRef.current?.click();
|
|
1120
|
+
}, []);
|
|
1121
|
+
const handleFileInputChange = useCallback3(
|
|
1122
|
+
(e) => {
|
|
1123
|
+
if (e.target.files) {
|
|
1124
|
+
void addFiles(e.target.files);
|
|
1125
|
+
e.target.value = "";
|
|
1126
|
+
}
|
|
1127
|
+
},
|
|
1128
|
+
[addFiles]
|
|
1129
|
+
);
|
|
1130
|
+
const removeAttachment = useCallback3((index) => {
|
|
1131
|
+
setAttachments((prev) => {
|
|
1132
|
+
const removed = prev[index];
|
|
1133
|
+
if (removed?.preview_url) URL.revokeObjectURL(removed.preview_url);
|
|
1134
|
+
return prev.filter((_, i) => i !== index);
|
|
1135
|
+
});
|
|
1136
|
+
}, []);
|
|
1137
|
+
const handlePaste = useCallback3(
|
|
1138
|
+
(e) => {
|
|
1139
|
+
const items = e.clipboardData.items;
|
|
1140
|
+
const files = [];
|
|
1141
|
+
for (const item of items) {
|
|
1142
|
+
if (item.kind === "file" && ALLOWED_TYPES.has(item.type)) {
|
|
1143
|
+
const file = item.getAsFile();
|
|
1144
|
+
if (file) files.push(file);
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
if (files.length > 0) {
|
|
1148
|
+
void addFiles(files);
|
|
1149
|
+
}
|
|
1150
|
+
},
|
|
1151
|
+
[addFiles]
|
|
1152
|
+
);
|
|
1153
|
+
const handleDragOver = useCallback3((e) => {
|
|
1154
|
+
e.preventDefault();
|
|
1155
|
+
e.stopPropagation();
|
|
1156
|
+
setDragOver(true);
|
|
1157
|
+
}, []);
|
|
1158
|
+
const handleDragLeave = useCallback3((e) => {
|
|
1159
|
+
e.preventDefault();
|
|
1160
|
+
e.stopPropagation();
|
|
1161
|
+
setDragOver(false);
|
|
1162
|
+
}, []);
|
|
1163
|
+
const handleDrop = useCallback3(
|
|
1164
|
+
(e) => {
|
|
1165
|
+
e.preventDefault();
|
|
1166
|
+
e.stopPropagation();
|
|
1167
|
+
setDragOver(false);
|
|
1168
|
+
if (e.dataTransfer.files.length > 0) {
|
|
1169
|
+
void addFiles(e.dataTransfer.files);
|
|
1170
|
+
}
|
|
1171
|
+
},
|
|
1172
|
+
[addFiles]
|
|
1173
|
+
);
|
|
838
1174
|
return /* @__PURE__ */ jsxs7(
|
|
839
1175
|
"div",
|
|
840
1176
|
{
|
|
841
1177
|
className: twMerge6(
|
|
842
|
-
"
|
|
1178
|
+
"relative shrink-0 rounded-3xl border bg-surface",
|
|
1179
|
+
"shadow-lg shadow-black/10",
|
|
1180
|
+
"transition-all duration-200",
|
|
1181
|
+
"focus-within:border-accent/40 focus-within:shadow-accent/5",
|
|
1182
|
+
dragOver ? "border-accent/60 bg-accent/5" : "border-border/60",
|
|
843
1183
|
className
|
|
844
1184
|
),
|
|
1185
|
+
onDragOver: handleDragOver,
|
|
1186
|
+
onDragLeave: handleDragLeave,
|
|
1187
|
+
onDrop: handleDrop,
|
|
845
1188
|
children: [
|
|
846
1189
|
/* @__PURE__ */ jsx9(
|
|
847
|
-
"
|
|
1190
|
+
"input",
|
|
848
1191
|
{
|
|
849
|
-
ref:
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
className: twMerge6(
|
|
857
|
-
"flex-1 resize-none rounded-xl border border-border bg-surface/80",
|
|
858
|
-
"px-4 py-2.5 text-sm text-text-primary placeholder:text-text-muted",
|
|
859
|
-
"focus:border-transparent focus:ring-2 focus:ring-accent/40 focus:outline-none",
|
|
860
|
-
"disabled:opacity-50 disabled:cursor-not-allowed",
|
|
861
|
-
"overflow-hidden",
|
|
862
|
-
"transition-all duration-200"
|
|
863
|
-
),
|
|
864
|
-
style: { colorScheme: "dark" },
|
|
865
|
-
"aria-label": "Message input"
|
|
1192
|
+
ref: fileInputRef,
|
|
1193
|
+
type: "file",
|
|
1194
|
+
multiple: true,
|
|
1195
|
+
accept: "image/png,image/jpeg,image/gif,image/webp,application/pdf",
|
|
1196
|
+
onChange: handleFileInputChange,
|
|
1197
|
+
className: "hidden",
|
|
1198
|
+
"aria-hidden": "true"
|
|
866
1199
|
}
|
|
867
1200
|
),
|
|
868
|
-
/* @__PURE__ */ jsx9(
|
|
869
|
-
|
|
1201
|
+
attachments.length > 0 && /* @__PURE__ */ jsx9("div", { className: "flex gap-2 px-4 pt-3 pb-1 overflow-x-auto", children: attachments.map((att, i) => /* @__PURE__ */ jsx9(
|
|
1202
|
+
AttachmentPreview,
|
|
870
1203
|
{
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
1204
|
+
attachment: att,
|
|
1205
|
+
onRemove: () => removeAttachment(i)
|
|
1206
|
+
},
|
|
1207
|
+
`${att.filename}-${i}`
|
|
1208
|
+
)) }),
|
|
1209
|
+
dragOver && /* @__PURE__ */ jsx9("div", { className: "absolute inset-0 rounded-3xl flex items-center justify-center bg-accent/10 border-2 border-dashed border-accent/40 z-10 pointer-events-none", children: /* @__PURE__ */ jsx9("span", { className: "text-sm font-display font-semibold text-accent", children: "Drop files here" }) }),
|
|
1210
|
+
/* @__PURE__ */ jsxs7("div", { className: "flex items-end", children: [
|
|
1211
|
+
/* @__PURE__ */ jsx9(
|
|
1212
|
+
"button",
|
|
1213
|
+
{
|
|
1214
|
+
type: "button",
|
|
1215
|
+
onClick: handleFileSelect,
|
|
1216
|
+
disabled: isLoading || attachments.length >= MAX_ATTACHMENTS,
|
|
1217
|
+
"aria-label": "Attach file",
|
|
1218
|
+
className: twMerge6(
|
|
1219
|
+
"flex-shrink-0 ml-2 mb-3",
|
|
1220
|
+
"inline-flex items-center justify-center",
|
|
1221
|
+
"w-9 h-9 rounded-full",
|
|
1222
|
+
"transition-all duration-200",
|
|
1223
|
+
"text-text-muted/60 hover:text-text-secondary hover:bg-text-muted/10",
|
|
1224
|
+
"focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent",
|
|
1225
|
+
"disabled:opacity-30 disabled:cursor-not-allowed disabled:hover:bg-transparent"
|
|
1226
|
+
),
|
|
1227
|
+
children: /* @__PURE__ */ jsx9(PaperclipIcon, {})
|
|
1228
|
+
}
|
|
1229
|
+
),
|
|
1230
|
+
/* @__PURE__ */ jsx9(
|
|
1231
|
+
"textarea",
|
|
1232
|
+
{
|
|
1233
|
+
ref: textareaRef,
|
|
1234
|
+
value,
|
|
1235
|
+
onChange: handleChange,
|
|
1236
|
+
onKeyDown: handleKeyDown,
|
|
1237
|
+
onPaste: handlePaste,
|
|
1238
|
+
placeholder,
|
|
1239
|
+
rows: 1,
|
|
1240
|
+
disabled: isLoading,
|
|
1241
|
+
className: twMerge6(
|
|
1242
|
+
"flex-1 resize-none bg-transparent",
|
|
1243
|
+
"pl-2 pr-14 pt-4 pb-4 text-[15px] leading-relaxed",
|
|
1244
|
+
"text-text-primary placeholder:text-text-muted/70",
|
|
1245
|
+
"focus:outline-none",
|
|
1246
|
+
"disabled:opacity-50 disabled:cursor-not-allowed",
|
|
1247
|
+
"overflow-hidden"
|
|
1248
|
+
),
|
|
1249
|
+
style: { colorScheme: "dark" },
|
|
1250
|
+
"aria-label": "Message input"
|
|
1251
|
+
}
|
|
1252
|
+
),
|
|
1253
|
+
/* @__PURE__ */ jsx9(
|
|
1254
|
+
"button",
|
|
1255
|
+
{
|
|
1256
|
+
type: "button",
|
|
1257
|
+
onClick: handleSend,
|
|
1258
|
+
disabled: !canSend,
|
|
1259
|
+
"aria-label": "Send message",
|
|
1260
|
+
className: twMerge6(
|
|
1261
|
+
"absolute bottom-3 right-3",
|
|
1262
|
+
"inline-flex items-center justify-center",
|
|
1263
|
+
"w-9 h-9 rounded-full",
|
|
1264
|
+
"transition-all duration-200",
|
|
1265
|
+
"focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent",
|
|
1266
|
+
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"
|
|
1267
|
+
),
|
|
1268
|
+
children: isLoading ? /* @__PURE__ */ jsx9(StopIcon, {}) : /* @__PURE__ */ jsx9(ArrowUpIcon, {})
|
|
1269
|
+
}
|
|
1270
|
+
)
|
|
1271
|
+
] })
|
|
885
1272
|
]
|
|
886
1273
|
}
|
|
887
1274
|
);
|
|
@@ -894,6 +1281,7 @@ function WelcomeScreen({
|
|
|
894
1281
|
title = "Welcome",
|
|
895
1282
|
message = "How can I help you today?",
|
|
896
1283
|
icon,
|
|
1284
|
+
iconClassName,
|
|
897
1285
|
suggestedQuestions = [],
|
|
898
1286
|
onQuestionSelect,
|
|
899
1287
|
className
|
|
@@ -906,12 +1294,15 @@ function WelcomeScreen({
|
|
|
906
1294
|
className
|
|
907
1295
|
),
|
|
908
1296
|
children: [
|
|
909
|
-
/* @__PURE__ */ jsx10(
|
|
1297
|
+
icon ? iconClassName ? /* @__PURE__ */ jsx10("div", { className: iconClassName, "aria-hidden": "true", children: icon }) : icon : /* @__PURE__ */ jsx10(
|
|
910
1298
|
"div",
|
|
911
1299
|
{
|
|
912
|
-
className:
|
|
1300
|
+
className: twMerge7(
|
|
1301
|
+
"w-14 h-14 rounded-2xl bg-accent/10 border border-border flex items-center justify-center pulse-glow",
|
|
1302
|
+
iconClassName
|
|
1303
|
+
),
|
|
913
1304
|
"aria-hidden": "true",
|
|
914
|
-
children:
|
|
1305
|
+
children: /* @__PURE__ */ jsx10("span", { className: "text-2xl", children: "\u2726" })
|
|
915
1306
|
}
|
|
916
1307
|
),
|
|
917
1308
|
/* @__PURE__ */ jsxs8("div", { className: "flex flex-col gap-2", children: [
|
|
@@ -921,7 +1312,7 @@ function WelcomeScreen({
|
|
|
921
1312
|
suggestedQuestions.length > 0 && /* @__PURE__ */ jsx10(
|
|
922
1313
|
"div",
|
|
923
1314
|
{
|
|
924
|
-
className: "flex flex-wrap justify-center gap-2 max-w-
|
|
1315
|
+
className: "flex flex-wrap justify-center gap-2 max-w-xl",
|
|
925
1316
|
role: "group",
|
|
926
1317
|
"aria-label": "Suggested questions",
|
|
927
1318
|
children: suggestedQuestions.map((question) => /* @__PURE__ */ jsx10(
|
|
@@ -930,7 +1321,7 @@ function WelcomeScreen({
|
|
|
930
1321
|
type: "button",
|
|
931
1322
|
onClick: () => onQuestionSelect?.(question),
|
|
932
1323
|
className: twMerge7(
|
|
933
|
-
"px-
|
|
1324
|
+
"px-3.5 py-1.5 rounded-full text-[12px]",
|
|
934
1325
|
"border border-border bg-transparent text-text-secondary",
|
|
935
1326
|
"hover:bg-accent/10 hover:border-interactive hover:text-text-primary",
|
|
936
1327
|
"focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent",
|
|
@@ -949,7 +1340,8 @@ function WelcomeScreen({
|
|
|
949
1340
|
|
|
950
1341
|
// src/streaming/StreamingMessage/StreamingMessage.tsx
|
|
951
1342
|
import { useEffect as useEffect3, useRef as useRef5 } from "react";
|
|
952
|
-
import {
|
|
1343
|
+
import { twMerge as twMerge8 } from "tailwind-merge";
|
|
1344
|
+
import { WaveLoader } from "@surf-kit/core";
|
|
953
1345
|
|
|
954
1346
|
// src/hooks/useCharacterDrain.ts
|
|
955
1347
|
import { useState as useState3, useRef as useRef4, useEffect as useEffect2 } from "react";
|
|
@@ -978,7 +1370,10 @@ function useCharacterDrain(target, msPerChar = 15) {
|
|
|
978
1370
|
const elapsed = now - lastTimeRef.current;
|
|
979
1371
|
const charsToAdvance = Math.floor(elapsed / msPerCharRef.current);
|
|
980
1372
|
if (charsToAdvance > 0 && indexRef.current < currentTarget.length) {
|
|
981
|
-
|
|
1373
|
+
let nextIndex = Math.min(indexRef.current + charsToAdvance, currentTarget.length);
|
|
1374
|
+
while (nextIndex < currentTarget.length && currentTarget[nextIndex - 1].trim() === "") {
|
|
1375
|
+
nextIndex++;
|
|
1376
|
+
}
|
|
982
1377
|
indexRef.current = nextIndex;
|
|
983
1378
|
lastTimeRef.current = now;
|
|
984
1379
|
setDisplayed(currentTarget.slice(0, nextIndex));
|
|
@@ -1023,14 +1418,35 @@ var phaseLabels = {
|
|
|
1023
1418
|
generating: "Writing...",
|
|
1024
1419
|
verifying: "Verifying..."
|
|
1025
1420
|
};
|
|
1421
|
+
var CURSOR_STYLES = `
|
|
1422
|
+
.sk-streaming-cursor > :not(ul,ol,blockquote):last-child::after,
|
|
1423
|
+
.sk-streaming-cursor > :is(ul,ol):last-child > li:last-child::after,
|
|
1424
|
+
.sk-streaming-cursor > blockquote:last-child > p:last-child::after {
|
|
1425
|
+
content: "";
|
|
1426
|
+
display: inline-block;
|
|
1427
|
+
width: 2px;
|
|
1428
|
+
height: 1em;
|
|
1429
|
+
background: var(--color-accent, #38bdf8);
|
|
1430
|
+
animation: sk-cursor-blink 0.8s steps(1) infinite;
|
|
1431
|
+
margin-left: 2px;
|
|
1432
|
+
vertical-align: text-bottom;
|
|
1433
|
+
}
|
|
1434
|
+
@keyframes sk-cursor-blink {
|
|
1435
|
+
0%, 60% { opacity: 1; }
|
|
1436
|
+
61%, 100% { opacity: 0; }
|
|
1437
|
+
}
|
|
1438
|
+
`;
|
|
1026
1439
|
function StreamingMessage({
|
|
1027
1440
|
stream,
|
|
1028
1441
|
onComplete,
|
|
1442
|
+
onDraining,
|
|
1029
1443
|
showPhases = true,
|
|
1030
1444
|
className
|
|
1031
1445
|
}) {
|
|
1032
1446
|
const onCompleteRef = useRef5(onComplete);
|
|
1033
1447
|
onCompleteRef.current = onComplete;
|
|
1448
|
+
const onDrainingRef = useRef5(onDraining);
|
|
1449
|
+
onDrainingRef.current = onDraining;
|
|
1034
1450
|
const wasActiveRef = useRef5(stream.active);
|
|
1035
1451
|
useEffect3(() => {
|
|
1036
1452
|
if (wasActiveRef.current && !stream.active) {
|
|
@@ -1039,35 +1455,40 @@ function StreamingMessage({
|
|
|
1039
1455
|
wasActiveRef.current = stream.active;
|
|
1040
1456
|
}, [stream.active]);
|
|
1041
1457
|
const phaseLabel = phaseLabels[stream.phase];
|
|
1042
|
-
const { displayed:
|
|
1043
|
-
|
|
1458
|
+
const { displayed: rawDisplayed, isDraining } = useCharacterDrain(stream.content);
|
|
1459
|
+
const displayedContent = stream.active || isDraining ? rawDisplayed.trimEnd() : rawDisplayed;
|
|
1460
|
+
useEffect3(() => {
|
|
1461
|
+
onDrainingRef.current?.(isDraining);
|
|
1462
|
+
}, [isDraining]);
|
|
1463
|
+
const agentLabel = stream.agent ? stream.agent.replace("_agent", "").replace("_", " ") : null;
|
|
1464
|
+
const showPhaseIndicator = showPhases && stream.active && stream.phase !== "idle" && !displayedContent;
|
|
1465
|
+
const showCursor = (stream.active || isDraining) && !!displayedContent;
|
|
1466
|
+
return /* @__PURE__ */ jsxs9("div", { className: twMerge8("flex w-full flex-col items-start", className), "data-testid": "streaming-message", children: [
|
|
1044
1467
|
/* @__PURE__ */ jsxs9("div", { "aria-live": "assertive", className: "sr-only", children: [
|
|
1045
1468
|
stream.active && stream.phase !== "idle" && "Response started",
|
|
1046
1469
|
!stream.active && stream.content && "Response complete"
|
|
1047
1470
|
] }),
|
|
1471
|
+
showCursor && /* @__PURE__ */ jsx11("style", { children: CURSOR_STYLES }),
|
|
1472
|
+
agentLabel && /* @__PURE__ */ jsx11("div", { className: "text-[11px] font-display font-semibold uppercase tracking-[0.08em] text-text-muted px-1 mb-1.5", children: agentLabel }),
|
|
1048
1473
|
/* @__PURE__ */ jsxs9("div", { className: "max-w-[88%] px-4 py-3 rounded-[18px] rounded-tl-[4px] bg-surface border border-border motion-safe:animate-springFromLeft", children: [
|
|
1049
|
-
|
|
1474
|
+
showPhaseIndicator && /* @__PURE__ */ jsxs9(
|
|
1050
1475
|
"div",
|
|
1051
1476
|
{
|
|
1052
|
-
className: "flex items-center gap-2
|
|
1477
|
+
className: "flex items-center gap-2 text-sm text-text-secondary",
|
|
1053
1478
|
"data-testid": "phase-indicator",
|
|
1054
1479
|
children: [
|
|
1055
|
-
/* @__PURE__ */ jsx11("span", { "aria-hidden": "true", children: /* @__PURE__ */ jsx11(
|
|
1480
|
+
/* @__PURE__ */ jsx11("span", { "aria-hidden": "true", children: /* @__PURE__ */ jsx11(WaveLoader, { size: "sm", color: "#38bdf8" }) }),
|
|
1056
1481
|
/* @__PURE__ */ jsx11("span", { children: phaseLabel })
|
|
1057
1482
|
]
|
|
1058
1483
|
}
|
|
1059
1484
|
),
|
|
1060
|
-
/* @__PURE__ */
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
"data-testid": "streaming-cursor"
|
|
1068
|
-
}
|
|
1069
|
-
)
|
|
1070
|
-
] })
|
|
1485
|
+
displayedContent && /* @__PURE__ */ jsx11(
|
|
1486
|
+
ResponseMessage,
|
|
1487
|
+
{
|
|
1488
|
+
content: displayedContent,
|
|
1489
|
+
className: showCursor ? "sk-streaming-cursor" : void 0
|
|
1490
|
+
}
|
|
1491
|
+
)
|
|
1071
1492
|
] })
|
|
1072
1493
|
] });
|
|
1073
1494
|
}
|
|
@@ -1098,7 +1519,7 @@ function AgentChat({
|
|
|
1098
1519
|
return /* @__PURE__ */ jsxs10(
|
|
1099
1520
|
"div",
|
|
1100
1521
|
{
|
|
1101
|
-
className:
|
|
1522
|
+
className: twMerge9(
|
|
1102
1523
|
"flex flex-col h-full bg-canvas border border-border rounded-xl overflow-hidden",
|
|
1103
1524
|
className
|
|
1104
1525
|
),
|
|
@@ -1141,7 +1562,7 @@ function AgentChat({
|
|
|
1141
1562
|
}
|
|
1142
1563
|
|
|
1143
1564
|
// src/chat/ConversationList/ConversationList.tsx
|
|
1144
|
-
import { twMerge as
|
|
1565
|
+
import { twMerge as twMerge10 } from "tailwind-merge";
|
|
1145
1566
|
import { jsx as jsx13, jsxs as jsxs11 } from "react/jsx-runtime";
|
|
1146
1567
|
function ConversationList({
|
|
1147
1568
|
conversations,
|
|
@@ -1155,7 +1576,7 @@ function ConversationList({
|
|
|
1155
1576
|
"nav",
|
|
1156
1577
|
{
|
|
1157
1578
|
"aria-label": "Conversation list",
|
|
1158
|
-
className:
|
|
1579
|
+
className: twMerge10("flex flex-col h-full bg-canvas", className),
|
|
1159
1580
|
children: [
|
|
1160
1581
|
onNew && /* @__PURE__ */ jsx13("div", { className: "p-3 border-b border-border", children: /* @__PURE__ */ jsx13(
|
|
1161
1582
|
"button",
|
|
@@ -1172,7 +1593,7 @@ function ConversationList({
|
|
|
1172
1593
|
return /* @__PURE__ */ jsxs11(
|
|
1173
1594
|
"li",
|
|
1174
1595
|
{
|
|
1175
|
-
className:
|
|
1596
|
+
className: twMerge10(
|
|
1176
1597
|
"flex items-start border-b border-border transition-colors duration-200",
|
|
1177
1598
|
"hover:bg-surface",
|
|
1178
1599
|
isActive && "bg-surface-raised border-l-2 border-l-accent"
|