@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.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,17 @@ 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_CONTENT_RESET":
|
|
38
|
+
return { ...state, streamingContent: "" };
|
|
39
|
+
case "STREAM_AGENT":
|
|
40
|
+
return { ...state, streamingAgent: action.agent };
|
|
35
41
|
case "SEND_SUCCESS":
|
|
36
42
|
return {
|
|
37
43
|
...state,
|
|
@@ -47,7 +53,8 @@ function reducer(state, action) {
|
|
|
47
53
|
isLoading: false,
|
|
48
54
|
error: action.error,
|
|
49
55
|
streamPhase: "idle",
|
|
50
|
-
streamingContent: ""
|
|
56
|
+
streamingContent: "",
|
|
57
|
+
streamingAgent: null
|
|
51
58
|
};
|
|
52
59
|
case "LOAD_CONVERSATION":
|
|
53
60
|
return {
|
|
@@ -73,115 +80,172 @@ function useAgentChat(config) {
|
|
|
73
80
|
const configRef = useRef(config);
|
|
74
81
|
configRef.current = config;
|
|
75
82
|
const lastUserMessageRef = useRef(null);
|
|
83
|
+
const lastUserAttachmentsRef = useRef(void 0);
|
|
84
|
+
const abortControllerRef = useRef(null);
|
|
76
85
|
const sendMessage = useCallback(
|
|
77
|
-
async (content) => {
|
|
78
|
-
const { apiUrl, streamPath = "/chat/stream", headers
|
|
86
|
+
async (content, attachments) => {
|
|
87
|
+
const { apiUrl, streamPath = "/chat/stream", headers: headersOrFn, timeout = 3e4, bodyExtra } = configRef.current;
|
|
88
|
+
const headers = typeof headersOrFn === "function" ? await headersOrFn() : headersOrFn ?? {};
|
|
79
89
|
lastUserMessageRef.current = content;
|
|
90
|
+
lastUserAttachmentsRef.current = attachments;
|
|
80
91
|
const userMessage = {
|
|
81
92
|
id: generateMessageId(),
|
|
82
93
|
role: "user",
|
|
83
94
|
content,
|
|
95
|
+
attachments,
|
|
84
96
|
timestamp: /* @__PURE__ */ new Date()
|
|
85
97
|
};
|
|
86
98
|
dispatch({ type: "SEND_START", message: userMessage });
|
|
87
99
|
const controller = new AbortController();
|
|
100
|
+
abortControllerRef.current = controller;
|
|
88
101
|
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
102
|
+
const ctx = {
|
|
103
|
+
accumulatedContent: "",
|
|
104
|
+
agentResponse: null,
|
|
105
|
+
capturedAgent: null,
|
|
106
|
+
capturedConversationId: null,
|
|
107
|
+
hadStreamError: false
|
|
108
|
+
};
|
|
89
109
|
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;
|
|
110
|
+
const url = `${apiUrl}${streamPath}`;
|
|
111
|
+
const mergedHeaders = {
|
|
112
|
+
"Content-Type": "application/json",
|
|
113
|
+
Accept: "text/event-stream",
|
|
114
|
+
...headers
|
|
115
|
+
};
|
|
116
|
+
const requestBody = {
|
|
117
|
+
message: content,
|
|
118
|
+
conversation_id: state.conversationId,
|
|
119
|
+
...bodyExtra
|
|
120
|
+
};
|
|
121
|
+
if (attachments && attachments.length > 0) {
|
|
122
|
+
requestBody.attachments = attachments.map((a) => ({
|
|
123
|
+
filename: a.filename,
|
|
124
|
+
content_type: a.content_type,
|
|
125
|
+
data: a.data
|
|
126
|
+
}));
|
|
114
127
|
}
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
128
|
+
const body = JSON.stringify(requestBody);
|
|
129
|
+
const handleEvent = (event) => {
|
|
130
|
+
switch (event.type) {
|
|
131
|
+
case "agent":
|
|
132
|
+
ctx.capturedAgent = event.agent;
|
|
133
|
+
dispatch({ type: "STREAM_AGENT", agent: ctx.capturedAgent });
|
|
134
|
+
break;
|
|
135
|
+
case "phase":
|
|
136
|
+
dispatch({ type: "STREAM_PHASE", phase: event.phase });
|
|
137
|
+
break;
|
|
138
|
+
case "delta":
|
|
139
|
+
ctx.accumulatedContent += event.content;
|
|
140
|
+
dispatch({ type: "STREAM_CONTENT", content: event.content });
|
|
141
|
+
break;
|
|
142
|
+
case "delta_reset":
|
|
143
|
+
ctx.accumulatedContent = "";
|
|
144
|
+
dispatch({ type: "STREAM_CONTENT_RESET" });
|
|
145
|
+
break;
|
|
146
|
+
case "done":
|
|
147
|
+
ctx.agentResponse = event.response;
|
|
148
|
+
ctx.capturedConversationId = event.conversation_id ?? null;
|
|
149
|
+
break;
|
|
150
|
+
case "error":
|
|
151
|
+
ctx.hadStreamError = true;
|
|
152
|
+
dispatch({ type: "SEND_ERROR", error: event.error });
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
const { streamAdapter } = configRef.current;
|
|
157
|
+
if (streamAdapter) {
|
|
158
|
+
await streamAdapter(
|
|
159
|
+
url,
|
|
160
|
+
{ method: "POST", headers: mergedHeaders, body, signal: controller.signal },
|
|
161
|
+
handleEvent
|
|
162
|
+
);
|
|
163
|
+
clearTimeout(timeoutId);
|
|
164
|
+
} else {
|
|
165
|
+
const response = await fetch(url, {
|
|
166
|
+
method: "POST",
|
|
167
|
+
headers: mergedHeaders,
|
|
168
|
+
body,
|
|
169
|
+
signal: controller.signal
|
|
120
170
|
});
|
|
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;
|
|
171
|
+
clearTimeout(timeoutId);
|
|
172
|
+
if (!response.ok) {
|
|
173
|
+
dispatch({
|
|
174
|
+
type: "SEND_ERROR",
|
|
175
|
+
error: {
|
|
176
|
+
code: "API_ERROR",
|
|
177
|
+
message: `HTTP ${response.status}: ${response.statusText}`,
|
|
178
|
+
retryable: response.status >= 500
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
const reader = response.body?.getReader();
|
|
184
|
+
if (!reader) {
|
|
185
|
+
dispatch({
|
|
186
|
+
type: "SEND_ERROR",
|
|
187
|
+
error: { code: "STREAM_ERROR", message: "No response body", retryable: true }
|
|
188
|
+
});
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
const decoder = new TextDecoder();
|
|
192
|
+
let buffer = "";
|
|
193
|
+
while (true) {
|
|
194
|
+
const { done, value } = await reader.read();
|
|
195
|
+
if (done) break;
|
|
196
|
+
buffer += decoder.decode(value, { stream: true });
|
|
197
|
+
const lines = buffer.split("\n");
|
|
198
|
+
buffer = lines.pop() ?? "";
|
|
199
|
+
for (const line of lines) {
|
|
200
|
+
if (!line.startsWith("data: ")) continue;
|
|
201
|
+
const data = line.slice(6).trim();
|
|
202
|
+
if (data === "[DONE]") continue;
|
|
203
|
+
try {
|
|
204
|
+
const event = JSON.parse(data);
|
|
205
|
+
handleEvent(event);
|
|
206
|
+
} catch {
|
|
159
207
|
}
|
|
160
|
-
} catch {
|
|
161
208
|
}
|
|
162
209
|
}
|
|
163
210
|
}
|
|
211
|
+
if (ctx.hadStreamError) return;
|
|
164
212
|
const assistantMessage = {
|
|
165
213
|
id: generateMessageId(),
|
|
166
214
|
role: "assistant",
|
|
167
|
-
content: agentResponse?.message ?? accumulatedContent,
|
|
168
|
-
response: agentResponse ?? void 0,
|
|
169
|
-
agent: capturedAgent ?? void 0,
|
|
215
|
+
content: ctx.agentResponse?.message ?? ctx.accumulatedContent,
|
|
216
|
+
response: ctx.agentResponse ?? void 0,
|
|
217
|
+
agent: ctx.capturedAgent ?? void 0,
|
|
170
218
|
timestamp: /* @__PURE__ */ new Date()
|
|
171
219
|
};
|
|
172
220
|
dispatch({
|
|
173
221
|
type: "SEND_SUCCESS",
|
|
174
222
|
message: assistantMessage,
|
|
175
|
-
streamingContent: accumulatedContent,
|
|
176
|
-
conversationId: capturedConversationId
|
|
223
|
+
streamingContent: ctx.accumulatedContent,
|
|
224
|
+
conversationId: ctx.capturedConversationId
|
|
177
225
|
});
|
|
178
226
|
} catch (err) {
|
|
179
227
|
clearTimeout(timeoutId);
|
|
180
228
|
if (err.name === "AbortError") {
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
229
|
+
if (ctx.accumulatedContent) {
|
|
230
|
+
const partialMessage = {
|
|
231
|
+
id: generateMessageId(),
|
|
232
|
+
role: "assistant",
|
|
233
|
+
content: ctx.accumulatedContent,
|
|
234
|
+
agent: ctx.capturedAgent ?? void 0,
|
|
235
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
236
|
+
};
|
|
237
|
+
dispatch({
|
|
238
|
+
type: "SEND_SUCCESS",
|
|
239
|
+
message: partialMessage,
|
|
240
|
+
streamingContent: ctx.accumulatedContent,
|
|
241
|
+
conversationId: ctx.capturedConversationId
|
|
242
|
+
});
|
|
243
|
+
} else {
|
|
244
|
+
dispatch({
|
|
245
|
+
type: "SEND_ERROR",
|
|
246
|
+
error: { code: "ABORTED", message: "Request stopped", retryable: true }
|
|
247
|
+
});
|
|
248
|
+
}
|
|
185
249
|
} else {
|
|
186
250
|
dispatch({
|
|
187
251
|
type: "SEND_ERROR",
|
|
@@ -192,6 +256,8 @@ function useAgentChat(config) {
|
|
|
192
256
|
}
|
|
193
257
|
});
|
|
194
258
|
}
|
|
259
|
+
} finally {
|
|
260
|
+
abortControllerRef.current = null;
|
|
195
261
|
}
|
|
196
262
|
},
|
|
197
263
|
[state.conversationId]
|
|
@@ -204,7 +270,8 @@ function useAgentChat(config) {
|
|
|
204
270
|
}, []);
|
|
205
271
|
const submitFeedback = useCallback(
|
|
206
272
|
async (messageId, rating, comment) => {
|
|
207
|
-
const { apiUrl, feedbackPath = "/feedback", headers
|
|
273
|
+
const { apiUrl, feedbackPath = "/feedback", headers: headersOrFn } = configRef.current;
|
|
274
|
+
const headers = typeof headersOrFn === "function" ? await headersOrFn() : headersOrFn ?? {};
|
|
208
275
|
await fetch(`${apiUrl}${feedbackPath}`, {
|
|
209
276
|
method: "POST",
|
|
210
277
|
headers: { "Content-Type": "application/json", ...headers },
|
|
@@ -215,12 +282,16 @@ function useAgentChat(config) {
|
|
|
215
282
|
);
|
|
216
283
|
const retry = useCallback(async () => {
|
|
217
284
|
if (lastUserMessageRef.current) {
|
|
218
|
-
await sendMessage(lastUserMessageRef.current);
|
|
285
|
+
await sendMessage(lastUserMessageRef.current, lastUserAttachmentsRef.current);
|
|
219
286
|
}
|
|
220
287
|
}, [sendMessage]);
|
|
288
|
+
const stop = useCallback(() => {
|
|
289
|
+
abortControllerRef.current?.abort();
|
|
290
|
+
}, []);
|
|
221
291
|
const reset = useCallback(() => {
|
|
222
292
|
dispatch({ type: "RESET" });
|
|
223
293
|
lastUserMessageRef.current = null;
|
|
294
|
+
lastUserAttachmentsRef.current = void 0;
|
|
224
295
|
}, []);
|
|
225
296
|
const actions = {
|
|
226
297
|
sendMessage,
|
|
@@ -228,6 +299,7 @@ function useAgentChat(config) {
|
|
|
228
299
|
loadConversation,
|
|
229
300
|
submitFeedback,
|
|
230
301
|
retry,
|
|
302
|
+
stop,
|
|
231
303
|
reset
|
|
232
304
|
};
|
|
233
305
|
return { state, actions };
|
|
@@ -235,7 +307,7 @@ function useAgentChat(config) {
|
|
|
235
307
|
|
|
236
308
|
// src/chat/MessageThread/MessageThread.tsx
|
|
237
309
|
import { twMerge as twMerge5 } from "tailwind-merge";
|
|
238
|
-
import { useEffect, useRef as useRef2 } from "react";
|
|
310
|
+
import { useCallback as useCallback2, useEffect, useRef as useRef2 } from "react";
|
|
239
311
|
|
|
240
312
|
// src/chat/MessageBubble/MessageBubble.tsx
|
|
241
313
|
import { twMerge as twMerge4 } from "tailwind-merge";
|
|
@@ -244,8 +316,10 @@ import { twMerge as twMerge4 } from "tailwind-merge";
|
|
|
244
316
|
import { Badge as Badge2 } from "@surf-kit/core";
|
|
245
317
|
|
|
246
318
|
// src/response/ResponseMessage/ResponseMessage.tsx
|
|
319
|
+
import React from "react";
|
|
247
320
|
import ReactMarkdown from "react-markdown";
|
|
248
321
|
import rehypeSanitize from "rehype-sanitize";
|
|
322
|
+
import remarkGfm from "remark-gfm";
|
|
249
323
|
import { twMerge } from "tailwind-merge";
|
|
250
324
|
import { jsx } from "react/jsx-runtime";
|
|
251
325
|
function normalizeMarkdownLists(content) {
|
|
@@ -268,7 +342,12 @@ function ResponseMessage({ content, className }) {
|
|
|
268
342
|
"[&_h3]:text-sm [&_h3]:font-semibold [&_h3]:text-accent [&_h3]:mt-2 [&_h3]:mb-1",
|
|
269
343
|
"[&_code]:bg-surface-raised [&_code]:text-accent [&_code]:px-1.5 [&_code]:py-0.5 [&_code]:rounded [&_code]:text-xs [&_code]:font-mono",
|
|
270
344
|
"[&_pre]:bg-surface-raised [&_pre]:border [&_pre]:border-border [&_pre]:rounded-xl [&_pre]:p-4 [&_pre]:overflow-x-auto",
|
|
345
|
+
"[&_hr]:my-3 [&_hr]:border-border",
|
|
271
346
|
"[&_blockquote]:border-l-2 [&_blockquote]:border-border-strong [&_blockquote]:pl-4 [&_blockquote]:text-text-secondary",
|
|
347
|
+
"[&_table]:w-full [&_table]:text-sm [&_table]:border-collapse [&_table]:my-2",
|
|
348
|
+
"[&_thead]:border-b [&_thead]:border-border",
|
|
349
|
+
"[&_th]:text-left [&_th]:px-2 [&_th]:py-1.5 [&_th]:font-semibold",
|
|
350
|
+
"[&_td]:px-2 [&_td]:py-1.5 [&_td]:border-t [&_td]:border-border/50",
|
|
272
351
|
"[&_a]:text-accent [&_a]:underline-offset-2 [&_a]:hover:text-accent/80",
|
|
273
352
|
className
|
|
274
353
|
),
|
|
@@ -276,6 +355,7 @@ function ResponseMessage({ content, className }) {
|
|
|
276
355
|
children: /* @__PURE__ */ jsx(
|
|
277
356
|
ReactMarkdown,
|
|
278
357
|
{
|
|
358
|
+
remarkPlugins: [remarkGfm],
|
|
279
359
|
rehypePlugins: [rehypeSanitize],
|
|
280
360
|
components: {
|
|
281
361
|
script: () => null,
|
|
@@ -283,12 +363,29 @@ function ResponseMessage({ content, className }) {
|
|
|
283
363
|
p: ({ children }) => /* @__PURE__ */ jsx("p", { className: "my-2", children }),
|
|
284
364
|
ul: ({ children }) => /* @__PURE__ */ jsx("ul", { className: "my-2 list-disc pl-6", children }),
|
|
285
365
|
ol: ({ children }) => /* @__PURE__ */ jsx("ol", { className: "my-2 list-decimal pl-6", children }),
|
|
286
|
-
li: ({ children }) =>
|
|
366
|
+
li: ({ children, ...props }) => {
|
|
367
|
+
let content2 = children;
|
|
368
|
+
if (props.ordered) {
|
|
369
|
+
content2 = React.Children.map(children, (child, i) => {
|
|
370
|
+
if (i === 0 && typeof child === "string") {
|
|
371
|
+
return child.replace(/^\d+[.)]\s*/, "");
|
|
372
|
+
}
|
|
373
|
+
return child;
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
return /* @__PURE__ */ jsx("li", { className: "my-1", children: content2 });
|
|
377
|
+
},
|
|
287
378
|
strong: ({ children }) => /* @__PURE__ */ jsx("strong", { className: "font-semibold", children }),
|
|
379
|
+
em: ({ children }) => /* @__PURE__ */ jsx("em", { className: "italic text-text-secondary", children }),
|
|
288
380
|
h1: ({ children }) => /* @__PURE__ */ jsx("h1", { className: "text-base font-bold mt-4 mb-2", children }),
|
|
289
381
|
h2: ({ children }) => /* @__PURE__ */ jsx("h2", { className: "text-sm font-bold mt-3 mb-1", children }),
|
|
290
382
|
h3: ({ children }) => /* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold mt-2 mb-1", children }),
|
|
291
|
-
|
|
383
|
+
hr: () => /* @__PURE__ */ jsx("hr", { className: "my-3 border-border" }),
|
|
384
|
+
code: ({ children }) => /* @__PURE__ */ jsx("code", { className: "bg-surface-sunken rounded px-1 py-0.5 text-xs font-mono", children }),
|
|
385
|
+
table: ({ children }) => /* @__PURE__ */ jsx("div", { className: "overflow-x-auto my-2", children: /* @__PURE__ */ jsx("table", { className: "w-full text-sm border-collapse", children }) }),
|
|
386
|
+
thead: ({ children }) => /* @__PURE__ */ jsx("thead", { className: "border-b border-border", children }),
|
|
387
|
+
th: ({ children }) => /* @__PURE__ */ jsx("th", { className: "text-left px-2 py-1.5 font-semibold", children }),
|
|
388
|
+
td: ({ children }) => /* @__PURE__ */ jsx("td", { className: "px-2 py-1.5 border-t border-border/50", children })
|
|
292
389
|
},
|
|
293
390
|
children: normalizeMarkdownLists(content)
|
|
294
391
|
}
|
|
@@ -298,7 +395,9 @@ function ResponseMessage({ content, className }) {
|
|
|
298
395
|
}
|
|
299
396
|
|
|
300
397
|
// src/response/StructuredResponse/StructuredResponse.tsx
|
|
301
|
-
import
|
|
398
|
+
import ReactMarkdown2 from "react-markdown";
|
|
399
|
+
import rehypeSanitize2 from "rehype-sanitize";
|
|
400
|
+
import { Fragment, jsx as jsx2, jsxs } from "react/jsx-runtime";
|
|
302
401
|
function tryParse(value) {
|
|
303
402
|
if (value === void 0 || value === null) return null;
|
|
304
403
|
if (typeof value === "string") {
|
|
@@ -310,6 +409,25 @@ function tryParse(value) {
|
|
|
310
409
|
}
|
|
311
410
|
return value;
|
|
312
411
|
}
|
|
412
|
+
function InlineMarkdown({ text }) {
|
|
413
|
+
return /* @__PURE__ */ jsx2(
|
|
414
|
+
ReactMarkdown2,
|
|
415
|
+
{
|
|
416
|
+
rehypePlugins: [rehypeSanitize2],
|
|
417
|
+
components: {
|
|
418
|
+
// Unwrap block-level <p> so content stays inline within its parent
|
|
419
|
+
p: ({ children }) => /* @__PURE__ */ jsx2(Fragment, { children }),
|
|
420
|
+
strong: ({ children }) => /* @__PURE__ */ jsx2("strong", { className: "font-semibold", children }),
|
|
421
|
+
em: ({ children }) => /* @__PURE__ */ jsx2("em", { className: "italic", children }),
|
|
422
|
+
code: ({ children }) => /* @__PURE__ */ jsx2("code", { className: "bg-surface-sunken rounded px-1 py-0.5 text-xs font-mono", children }),
|
|
423
|
+
// Prevent block elements that would break layout
|
|
424
|
+
script: () => null,
|
|
425
|
+
iframe: () => null
|
|
426
|
+
},
|
|
427
|
+
children: text
|
|
428
|
+
}
|
|
429
|
+
);
|
|
430
|
+
}
|
|
313
431
|
function renderSteps(data) {
|
|
314
432
|
const steps = tryParse(data.steps);
|
|
315
433
|
if (!steps || !Array.isArray(steps)) return null;
|
|
@@ -322,7 +440,7 @@ function renderSteps(data) {
|
|
|
322
440
|
children: i + 1
|
|
323
441
|
}
|
|
324
442
|
),
|
|
325
|
-
/* @__PURE__ */ jsx2("span", { className: "text-sm text-text-primary leading-relaxed", children: step })
|
|
443
|
+
/* @__PURE__ */ jsx2("span", { className: "text-sm text-text-primary leading-relaxed", children: /* @__PURE__ */ jsx2(InlineMarkdown, { text: step }) })
|
|
326
444
|
] }, i)) });
|
|
327
445
|
}
|
|
328
446
|
function renderTable(data) {
|
|
@@ -388,7 +506,7 @@ function renderList(data) {
|
|
|
388
506
|
title && /* @__PURE__ */ jsx2("p", { className: "text-xs font-semibold uppercase tracking-wider text-text-secondary mb-1", children: title }),
|
|
389
507
|
/* @__PURE__ */ jsx2("ul", { className: "flex flex-col gap-1.5", children: items.map((item, i) => /* @__PURE__ */ jsxs("li", { className: "flex items-start gap-2.5", children: [
|
|
390
508
|
/* @__PURE__ */ jsx2("span", { className: "mt-1.5 h-1.5 w-1.5 shrink-0 rounded-full bg-accent", "aria-hidden": "true" }),
|
|
391
|
-
/* @__PURE__ */ jsx2("span", { className: "text-sm text-text-primary leading-relaxed", children: item })
|
|
509
|
+
/* @__PURE__ */ jsx2("span", { className: "text-sm text-text-primary leading-relaxed", children: /* @__PURE__ */ jsx2(InlineMarkdown, { text: item }) })
|
|
392
510
|
] }, i)) })
|
|
393
511
|
] });
|
|
394
512
|
}
|
|
@@ -423,7 +541,14 @@ function renderWarning(data) {
|
|
|
423
541
|
}
|
|
424
542
|
);
|
|
425
543
|
}
|
|
426
|
-
function StructuredResponse({ uiHint, data, className }) {
|
|
544
|
+
function StructuredResponse({ uiHint, data: rawData, className }) {
|
|
545
|
+
const data = typeof rawData === "string" ? (() => {
|
|
546
|
+
try {
|
|
547
|
+
return JSON.parse(rawData);
|
|
548
|
+
} catch {
|
|
549
|
+
return null;
|
|
550
|
+
}
|
|
551
|
+
})() : rawData;
|
|
427
552
|
if (!data) return null;
|
|
428
553
|
let content;
|
|
429
554
|
switch (uiHint) {
|
|
@@ -503,7 +628,36 @@ function SourceCard({ source, variant = "compact", onNavigate, className }) {
|
|
|
503
628
|
children: [
|
|
504
629
|
/* @__PURE__ */ jsxs2("div", { className: "flex items-start justify-between gap-2", children: [
|
|
505
630
|
/* @__PURE__ */ jsxs2("div", { className: "flex-1 min-w-0", children: [
|
|
506
|
-
/* @__PURE__ */
|
|
631
|
+
source.url ? /* @__PURE__ */ jsxs2(
|
|
632
|
+
"a",
|
|
633
|
+
{
|
|
634
|
+
href: source.url,
|
|
635
|
+
target: "_blank",
|
|
636
|
+
rel: "noopener noreferrer",
|
|
637
|
+
className: "text-sm font-medium text-accent hover:underline truncate block",
|
|
638
|
+
onClick: (e) => e.stopPropagation(),
|
|
639
|
+
children: [
|
|
640
|
+
source.title,
|
|
641
|
+
/* @__PURE__ */ jsxs2(
|
|
642
|
+
"svg",
|
|
643
|
+
{
|
|
644
|
+
className: "inline-block ml-1 w-3 h-3 opacity-60",
|
|
645
|
+
viewBox: "0 0 24 24",
|
|
646
|
+
fill: "none",
|
|
647
|
+
stroke: "currentColor",
|
|
648
|
+
strokeWidth: "2",
|
|
649
|
+
strokeLinecap: "round",
|
|
650
|
+
strokeLinejoin: "round",
|
|
651
|
+
children: [
|
|
652
|
+
/* @__PURE__ */ jsx3("path", { d: "M18 13v6a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h6" }),
|
|
653
|
+
/* @__PURE__ */ jsx3("polyline", { points: "15 3 21 3 21 9" }),
|
|
654
|
+
/* @__PURE__ */ jsx3("line", { x1: "10", y1: "14", x2: "21", y2: "3" })
|
|
655
|
+
]
|
|
656
|
+
}
|
|
657
|
+
)
|
|
658
|
+
]
|
|
659
|
+
}
|
|
660
|
+
) : /* @__PURE__ */ jsx3("p", { className: "text-sm font-medium text-text-primary truncate", children: source.title }),
|
|
507
661
|
source.section && /* @__PURE__ */ jsx3("p", { className: "text-[11px] font-semibold uppercase tracking-wider text-text-secondary truncate mt-0.5", children: source.section })
|
|
508
662
|
] }),
|
|
509
663
|
/* @__PURE__ */ jsx3(
|
|
@@ -637,13 +791,16 @@ function AgentResponse({
|
|
|
637
791
|
}) {
|
|
638
792
|
return /* @__PURE__ */ jsxs4("div", { className: `flex flex-col gap-4 ${className ?? ""}`, "data-testid": "agent-response", children: [
|
|
639
793
|
/* @__PURE__ */ jsx6(ResponseMessage, { content: response.message }),
|
|
640
|
-
response.ui_hint !== "text" && response.structured_data &&
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
794
|
+
response.ui_hint !== "text" && response.structured_data && (() => {
|
|
795
|
+
const parsed = typeof response.structured_data === "string" ? (() => {
|
|
796
|
+
try {
|
|
797
|
+
return JSON.parse(response.structured_data);
|
|
798
|
+
} catch {
|
|
799
|
+
return null;
|
|
800
|
+
}
|
|
801
|
+
})() : response.structured_data;
|
|
802
|
+
return parsed ? /* @__PURE__ */ jsx6(StructuredResponse, { uiHint: response.ui_hint, data: parsed }) : null;
|
|
803
|
+
})(),
|
|
647
804
|
(showConfidence || showVerification) && /* @__PURE__ */ jsxs4("div", { className: "flex flex-wrap items-center gap-2 mt-1", "data-testid": "response-meta", children: [
|
|
648
805
|
showConfidence && /* @__PURE__ */ jsxs4(
|
|
649
806
|
Badge2,
|
|
@@ -694,6 +851,31 @@ function AgentResponse({
|
|
|
694
851
|
|
|
695
852
|
// src/chat/MessageBubble/MessageBubble.tsx
|
|
696
853
|
import { jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
854
|
+
function DocumentIcon() {
|
|
855
|
+
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: [
|
|
856
|
+
/* @__PURE__ */ jsx7("path", { d: "M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z" }),
|
|
857
|
+
/* @__PURE__ */ jsx7("polyline", { points: "14 2 14 8 20 8" }),
|
|
858
|
+
/* @__PURE__ */ jsx7("line", { x1: "16", y1: "13", x2: "8", y2: "13" }),
|
|
859
|
+
/* @__PURE__ */ jsx7("line", { x1: "16", y1: "17", x2: "8", y2: "17" })
|
|
860
|
+
] });
|
|
861
|
+
}
|
|
862
|
+
function AttachmentThumbnail({ attachment }) {
|
|
863
|
+
const isImage = attachment.content_type.startsWith("image/");
|
|
864
|
+
if (isImage) {
|
|
865
|
+
return /* @__PURE__ */ jsx7("div", { className: "rounded-lg overflow-hidden border border-black/10 max-w-[240px]", children: /* @__PURE__ */ jsx7(
|
|
866
|
+
"img",
|
|
867
|
+
{
|
|
868
|
+
src: attachment.preview_url ?? `data:${attachment.content_type};base64,${attachment.data}`,
|
|
869
|
+
alt: attachment.filename,
|
|
870
|
+
className: "max-w-full max-h-[200px] object-contain"
|
|
871
|
+
}
|
|
872
|
+
) });
|
|
873
|
+
}
|
|
874
|
+
return /* @__PURE__ */ jsxs5("div", { className: "flex items-center gap-2 px-3 py-2 rounded-lg border border-black/10 bg-black/5", children: [
|
|
875
|
+
/* @__PURE__ */ jsx7(DocumentIcon, {}),
|
|
876
|
+
/* @__PURE__ */ jsx7("span", { className: "text-xs truncate max-w-[160px]", children: attachment.filename })
|
|
877
|
+
] });
|
|
878
|
+
}
|
|
697
879
|
function MessageBubble({
|
|
698
880
|
message,
|
|
699
881
|
showAgent,
|
|
@@ -701,23 +883,29 @@ function MessageBubble({
|
|
|
701
883
|
showConfidence = true,
|
|
702
884
|
showVerification = true,
|
|
703
885
|
animated = true,
|
|
886
|
+
userBubbleClassName,
|
|
704
887
|
className
|
|
705
888
|
}) {
|
|
706
889
|
const isUser = message.role === "user";
|
|
890
|
+
const hasAttachments = message.attachments && message.attachments.length > 0;
|
|
707
891
|
if (isUser) {
|
|
708
892
|
return /* @__PURE__ */ jsx7(
|
|
709
893
|
"div",
|
|
710
894
|
{
|
|
711
895
|
"data-message-id": message.id,
|
|
712
896
|
className: twMerge4("flex w-full justify-end", className),
|
|
713
|
-
children: /* @__PURE__ */
|
|
897
|
+
children: /* @__PURE__ */ jsxs5(
|
|
714
898
|
"div",
|
|
715
899
|
{
|
|
716
900
|
className: twMerge4(
|
|
717
|
-
"max-w-[70%] rounded-[18px] rounded-br-[4px] px-4 py-2.5 bg-
|
|
718
|
-
animated && "motion-safe:animate-slideFromRight"
|
|
901
|
+
"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",
|
|
902
|
+
animated && "motion-safe:animate-slideFromRight",
|
|
903
|
+
userBubbleClassName
|
|
719
904
|
),
|
|
720
|
-
children:
|
|
905
|
+
children: [
|
|
906
|
+
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}`)) }),
|
|
907
|
+
message.content
|
|
908
|
+
]
|
|
721
909
|
}
|
|
722
910
|
)
|
|
723
911
|
}
|
|
@@ -729,7 +917,7 @@ function MessageBubble({
|
|
|
729
917
|
"data-message-id": message.id,
|
|
730
918
|
className: twMerge4("flex w-full flex-col items-start gap-1.5", className),
|
|
731
919
|
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("_", " ") }),
|
|
920
|
+
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
921
|
/* @__PURE__ */ jsx7(
|
|
734
922
|
"div",
|
|
735
923
|
{
|
|
@@ -755,34 +943,97 @@ function MessageBubble({
|
|
|
755
943
|
|
|
756
944
|
// src/chat/MessageThread/MessageThread.tsx
|
|
757
945
|
import { jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
758
|
-
function MessageThread({ messages, streamingSlot, showSources, showConfidence, showVerification, className }) {
|
|
759
|
-
const
|
|
946
|
+
function MessageThread({ messages, streamingSlot, showAgent, showSources, showConfidence, showVerification, hideLastAssistant, userBubbleClassName, className }) {
|
|
947
|
+
const scrollRef = useRef2(null);
|
|
948
|
+
const shouldAutoScroll = useRef2(true);
|
|
949
|
+
const hasStreaming = !!streamingSlot;
|
|
950
|
+
const scrollToBottom = useCallback2(() => {
|
|
951
|
+
const el = scrollRef.current;
|
|
952
|
+
if (el && shouldAutoScroll.current) {
|
|
953
|
+
el.scrollTop = el.scrollHeight;
|
|
954
|
+
}
|
|
955
|
+
}, []);
|
|
956
|
+
useEffect(() => {
|
|
957
|
+
const el = scrollRef.current;
|
|
958
|
+
if (!el) return;
|
|
959
|
+
const onWheel = (e) => {
|
|
960
|
+
if (e.deltaY < 0) {
|
|
961
|
+
shouldAutoScroll.current = false;
|
|
962
|
+
}
|
|
963
|
+
};
|
|
964
|
+
const onPointerDown = () => {
|
|
965
|
+
el.dataset.userPointer = "1";
|
|
966
|
+
};
|
|
967
|
+
const onPointerUp = () => {
|
|
968
|
+
delete el.dataset.userPointer;
|
|
969
|
+
};
|
|
970
|
+
el.addEventListener("wheel", onWheel, { passive: true });
|
|
971
|
+
el.addEventListener("pointerdown", onPointerDown);
|
|
972
|
+
window.addEventListener("pointerup", onPointerUp);
|
|
973
|
+
return () => {
|
|
974
|
+
el.removeEventListener("wheel", onWheel);
|
|
975
|
+
el.removeEventListener("pointerdown", onPointerDown);
|
|
976
|
+
window.removeEventListener("pointerup", onPointerUp);
|
|
977
|
+
};
|
|
978
|
+
}, []);
|
|
979
|
+
const handleScroll = useCallback2(() => {
|
|
980
|
+
const el = scrollRef.current;
|
|
981
|
+
if (!el) return;
|
|
982
|
+
const nearBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 80;
|
|
983
|
+
if (nearBottom) {
|
|
984
|
+
shouldAutoScroll.current = true;
|
|
985
|
+
} else if (el.dataset.userPointer) {
|
|
986
|
+
shouldAutoScroll.current = false;
|
|
987
|
+
}
|
|
988
|
+
}, []);
|
|
989
|
+
useEffect(scrollToBottom, [messages.length, scrollToBottom]);
|
|
990
|
+
useEffect(() => {
|
|
991
|
+
if (!hasStreaming) return;
|
|
992
|
+
let raf;
|
|
993
|
+
const tick = () => {
|
|
994
|
+
scrollToBottom();
|
|
995
|
+
raf = requestAnimationFrame(tick);
|
|
996
|
+
};
|
|
997
|
+
raf = requestAnimationFrame(tick);
|
|
998
|
+
return () => cancelAnimationFrame(raf);
|
|
999
|
+
}, [hasStreaming, scrollToBottom]);
|
|
760
1000
|
useEffect(() => {
|
|
761
|
-
|
|
762
|
-
|
|
1001
|
+
if (!hasStreaming) {
|
|
1002
|
+
shouldAutoScroll.current = true;
|
|
1003
|
+
}
|
|
1004
|
+
}, [hasStreaming]);
|
|
763
1005
|
return /* @__PURE__ */ jsxs6(
|
|
764
1006
|
"div",
|
|
765
1007
|
{
|
|
1008
|
+
ref: scrollRef,
|
|
766
1009
|
role: "log",
|
|
767
1010
|
"aria-live": "polite",
|
|
768
1011
|
"aria-label": "Message thread",
|
|
1012
|
+
onScroll: handleScroll,
|
|
769
1013
|
className: twMerge5(
|
|
770
1014
|
"flex flex-col gap-4 overflow-y-auto flex-1 px-4 py-6",
|
|
771
1015
|
className
|
|
772
1016
|
),
|
|
773
1017
|
children: [
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
{
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
1018
|
+
/* @__PURE__ */ jsx8("div", { className: "flex-1 shrink-0" }),
|
|
1019
|
+
messages.map((message, i) => {
|
|
1020
|
+
if (hideLastAssistant && i === messages.length - 1 && message.role === "assistant") {
|
|
1021
|
+
return null;
|
|
1022
|
+
}
|
|
1023
|
+
return /* @__PURE__ */ jsx8(
|
|
1024
|
+
MessageBubble,
|
|
1025
|
+
{
|
|
1026
|
+
message,
|
|
1027
|
+
showAgent,
|
|
1028
|
+
showSources,
|
|
1029
|
+
showConfidence,
|
|
1030
|
+
showVerification,
|
|
1031
|
+
userBubbleClassName
|
|
1032
|
+
},
|
|
1033
|
+
message.id
|
|
1034
|
+
);
|
|
1035
|
+
}),
|
|
1036
|
+
streamingSlot
|
|
786
1037
|
]
|
|
787
1038
|
}
|
|
788
1039
|
);
|
|
@@ -790,32 +1041,127 @@ function MessageThread({ messages, streamingSlot, showSources, showConfidence, s
|
|
|
790
1041
|
|
|
791
1042
|
// src/chat/MessageComposer/MessageComposer.tsx
|
|
792
1043
|
import { twMerge as twMerge6 } from "tailwind-merge";
|
|
793
|
-
import { useState as useState2, useRef as useRef3, useCallback as
|
|
1044
|
+
import { useState as useState2, useRef as useRef3, useCallback as useCallback3 } from "react";
|
|
794
1045
|
import { jsx as jsx9, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
1046
|
+
var ALLOWED_TYPES = /* @__PURE__ */ new Set([
|
|
1047
|
+
"image/png",
|
|
1048
|
+
"image/jpeg",
|
|
1049
|
+
"image/gif",
|
|
1050
|
+
"image/webp",
|
|
1051
|
+
"application/pdf"
|
|
1052
|
+
]);
|
|
1053
|
+
var MAX_FILE_SIZE = 10 * 1024 * 1024;
|
|
1054
|
+
var MAX_ATTACHMENTS = 5;
|
|
1055
|
+
function ArrowUpIcon() {
|
|
1056
|
+
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: [
|
|
1057
|
+
/* @__PURE__ */ jsx9("path", { d: "M10 16V4" }),
|
|
1058
|
+
/* @__PURE__ */ jsx9("path", { d: "M4 10l6-6 6 6" })
|
|
1059
|
+
] });
|
|
1060
|
+
}
|
|
1061
|
+
function StopIcon() {
|
|
1062
|
+
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" }) });
|
|
1063
|
+
}
|
|
1064
|
+
function PaperclipIcon() {
|
|
1065
|
+
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" }) });
|
|
1066
|
+
}
|
|
1067
|
+
function XIcon({ size = 14 }) {
|
|
1068
|
+
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: [
|
|
1069
|
+
/* @__PURE__ */ jsx9("path", { d: "M18 6L6 18" }),
|
|
1070
|
+
/* @__PURE__ */ jsx9("path", { d: "M6 6l12 12" })
|
|
1071
|
+
] });
|
|
1072
|
+
}
|
|
1073
|
+
function DocumentIcon2() {
|
|
1074
|
+
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: [
|
|
1075
|
+
/* @__PURE__ */ jsx9("path", { d: "M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z" }),
|
|
1076
|
+
/* @__PURE__ */ jsx9("polyline", { points: "14 2 14 8 20 8" }),
|
|
1077
|
+
/* @__PURE__ */ jsx9("line", { x1: "16", y1: "13", x2: "8", y2: "13" }),
|
|
1078
|
+
/* @__PURE__ */ jsx9("line", { x1: "16", y1: "17", x2: "8", y2: "17" }),
|
|
1079
|
+
/* @__PURE__ */ jsx9("polyline", { points: "10 9 9 9 8 9" })
|
|
1080
|
+
] });
|
|
1081
|
+
}
|
|
1082
|
+
function fileToBase64(file) {
|
|
1083
|
+
return new Promise((resolve, reject) => {
|
|
1084
|
+
const reader = new FileReader();
|
|
1085
|
+
reader.onload = () => {
|
|
1086
|
+
const result = reader.result;
|
|
1087
|
+
const base64 = result.split(",")[1];
|
|
1088
|
+
resolve(base64);
|
|
1089
|
+
};
|
|
1090
|
+
reader.onerror = reject;
|
|
1091
|
+
reader.readAsDataURL(file);
|
|
1092
|
+
});
|
|
1093
|
+
}
|
|
1094
|
+
function AttachmentPreview({
|
|
1095
|
+
attachment,
|
|
1096
|
+
onRemove
|
|
1097
|
+
}) {
|
|
1098
|
+
const isImage = attachment.content_type.startsWith("image/");
|
|
1099
|
+
return /* @__PURE__ */ jsxs7("div", { className: "relative group flex-shrink-0", children: [
|
|
1100
|
+
isImage ? /* @__PURE__ */ jsx9("div", { className: "w-16 h-16 rounded-lg overflow-hidden border border-border/60 bg-surface-alt", children: /* @__PURE__ */ jsx9(
|
|
1101
|
+
"img",
|
|
1102
|
+
{
|
|
1103
|
+
src: attachment.preview_url ?? `data:${attachment.content_type};base64,${attachment.data}`,
|
|
1104
|
+
alt: attachment.filename,
|
|
1105
|
+
className: "w-full h-full object-cover"
|
|
1106
|
+
}
|
|
1107
|
+
) }) : /* @__PURE__ */ jsxs7("div", { className: "h-16 px-3 rounded-lg border border-border/60 bg-surface-alt flex items-center gap-2", children: [
|
|
1108
|
+
/* @__PURE__ */ jsx9("div", { className: "text-text-muted", children: /* @__PURE__ */ jsx9(DocumentIcon2, {}) }),
|
|
1109
|
+
/* @__PURE__ */ jsxs7("div", { className: "flex flex-col min-w-0", children: [
|
|
1110
|
+
/* @__PURE__ */ jsx9("span", { className: "text-xs text-text-primary truncate max-w-[120px]", children: attachment.filename }),
|
|
1111
|
+
/* @__PURE__ */ jsx9("span", { className: "text-[10px] text-text-muted", children: "PDF" })
|
|
1112
|
+
] })
|
|
1113
|
+
] }),
|
|
1114
|
+
/* @__PURE__ */ jsx9(
|
|
1115
|
+
"button",
|
|
1116
|
+
{
|
|
1117
|
+
type: "button",
|
|
1118
|
+
onClick: onRemove,
|
|
1119
|
+
className: twMerge6(
|
|
1120
|
+
"absolute -top-1.5 -right-1.5",
|
|
1121
|
+
"w-5 h-5 rounded-full",
|
|
1122
|
+
"bg-text-muted/80 text-white",
|
|
1123
|
+
"flex items-center justify-center",
|
|
1124
|
+
"opacity-0 group-hover:opacity-100",
|
|
1125
|
+
"transition-opacity duration-150",
|
|
1126
|
+
"hover:bg-text-primary"
|
|
1127
|
+
),
|
|
1128
|
+
"aria-label": `Remove ${attachment.filename}`,
|
|
1129
|
+
children: /* @__PURE__ */ jsx9(XIcon, { size: 10 })
|
|
1130
|
+
}
|
|
1131
|
+
)
|
|
1132
|
+
] });
|
|
1133
|
+
}
|
|
795
1134
|
function MessageComposer({
|
|
796
1135
|
onSend,
|
|
1136
|
+
onStop,
|
|
797
1137
|
isLoading = false,
|
|
798
1138
|
placeholder = "Type a message...",
|
|
799
1139
|
className
|
|
800
1140
|
}) {
|
|
801
1141
|
const [value, setValue] = useState2("");
|
|
1142
|
+
const [attachments, setAttachments] = useState2([]);
|
|
1143
|
+
const [dragOver, setDragOver] = useState2(false);
|
|
802
1144
|
const textareaRef = useRef3(null);
|
|
803
|
-
const
|
|
804
|
-
const
|
|
1145
|
+
const fileInputRef = useRef3(null);
|
|
1146
|
+
const canSend = (value.trim().length > 0 || attachments.length > 0) && !isLoading;
|
|
1147
|
+
const resetHeight = useCallback3(() => {
|
|
805
1148
|
const el = textareaRef.current;
|
|
806
1149
|
if (el) {
|
|
807
1150
|
el.style.height = "auto";
|
|
808
1151
|
el.style.overflowY = "hidden";
|
|
809
1152
|
}
|
|
810
1153
|
}, []);
|
|
811
|
-
const handleSend =
|
|
1154
|
+
const handleSend = useCallback3(() => {
|
|
812
1155
|
if (!canSend) return;
|
|
813
|
-
|
|
1156
|
+
const message = value.trim() || (attachments.length > 0 ? "Please analyse the attached file(s)." : "");
|
|
1157
|
+
if (!message && attachments.length === 0) return;
|
|
1158
|
+
onSend(message, attachments.length > 0 ? attachments : void 0);
|
|
814
1159
|
setValue("");
|
|
1160
|
+
setAttachments([]);
|
|
815
1161
|
resetHeight();
|
|
816
1162
|
textareaRef.current?.focus();
|
|
817
|
-
}, [canSend, onSend, value, resetHeight]);
|
|
818
|
-
const handleKeyDown =
|
|
1163
|
+
}, [canSend, onSend, value, attachments, resetHeight]);
|
|
1164
|
+
const handleKeyDown = useCallback3(
|
|
819
1165
|
(e) => {
|
|
820
1166
|
if (e.key === "Enter" && !e.shiftKey) {
|
|
821
1167
|
e.preventDefault();
|
|
@@ -824,64 +1170,194 @@ function MessageComposer({
|
|
|
824
1170
|
},
|
|
825
1171
|
[handleSend]
|
|
826
1172
|
);
|
|
827
|
-
const handleChange =
|
|
1173
|
+
const handleChange = useCallback3(
|
|
828
1174
|
(e) => {
|
|
829
1175
|
setValue(e.target.value);
|
|
830
1176
|
const el = e.target;
|
|
831
1177
|
el.style.height = "auto";
|
|
832
|
-
const capped = Math.min(el.scrollHeight,
|
|
1178
|
+
const capped = Math.min(el.scrollHeight, 200);
|
|
833
1179
|
el.style.height = `${capped}px`;
|
|
834
|
-
el.style.overflowY = el.scrollHeight >
|
|
1180
|
+
el.style.overflowY = el.scrollHeight > 200 ? "auto" : "hidden";
|
|
835
1181
|
},
|
|
836
1182
|
[]
|
|
837
1183
|
);
|
|
1184
|
+
const addFiles = useCallback3(async (files) => {
|
|
1185
|
+
const fileArray = Array.from(files);
|
|
1186
|
+
for (const file of fileArray) {
|
|
1187
|
+
if (attachments.length >= MAX_ATTACHMENTS) break;
|
|
1188
|
+
if (!ALLOWED_TYPES.has(file.type)) continue;
|
|
1189
|
+
if (file.size > MAX_FILE_SIZE) continue;
|
|
1190
|
+
try {
|
|
1191
|
+
const data = await fileToBase64(file);
|
|
1192
|
+
const previewUrl = file.type.startsWith("image/") ? URL.createObjectURL(file) : void 0;
|
|
1193
|
+
const attachment = {
|
|
1194
|
+
filename: file.name,
|
|
1195
|
+
content_type: file.type,
|
|
1196
|
+
data,
|
|
1197
|
+
preview_url: previewUrl
|
|
1198
|
+
};
|
|
1199
|
+
setAttachments((prev) => {
|
|
1200
|
+
if (prev.length >= MAX_ATTACHMENTS) return prev;
|
|
1201
|
+
return [...prev, attachment];
|
|
1202
|
+
});
|
|
1203
|
+
} catch {
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
}, [attachments.length]);
|
|
1207
|
+
const handleFileSelect = useCallback3(() => {
|
|
1208
|
+
fileInputRef.current?.click();
|
|
1209
|
+
}, []);
|
|
1210
|
+
const handleFileInputChange = useCallback3(
|
|
1211
|
+
(e) => {
|
|
1212
|
+
if (e.target.files) {
|
|
1213
|
+
void addFiles(e.target.files);
|
|
1214
|
+
e.target.value = "";
|
|
1215
|
+
}
|
|
1216
|
+
},
|
|
1217
|
+
[addFiles]
|
|
1218
|
+
);
|
|
1219
|
+
const removeAttachment = useCallback3((index) => {
|
|
1220
|
+
setAttachments((prev) => {
|
|
1221
|
+
const removed = prev[index];
|
|
1222
|
+
if (removed?.preview_url) URL.revokeObjectURL(removed.preview_url);
|
|
1223
|
+
return prev.filter((_, i) => i !== index);
|
|
1224
|
+
});
|
|
1225
|
+
}, []);
|
|
1226
|
+
const handlePaste = useCallback3(
|
|
1227
|
+
(e) => {
|
|
1228
|
+
const items = e.clipboardData.items;
|
|
1229
|
+
const files = [];
|
|
1230
|
+
for (const item of items) {
|
|
1231
|
+
if (item.kind === "file" && ALLOWED_TYPES.has(item.type)) {
|
|
1232
|
+
const file = item.getAsFile();
|
|
1233
|
+
if (file) files.push(file);
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
if (files.length > 0) {
|
|
1237
|
+
void addFiles(files);
|
|
1238
|
+
}
|
|
1239
|
+
},
|
|
1240
|
+
[addFiles]
|
|
1241
|
+
);
|
|
1242
|
+
const handleDragOver = useCallback3((e) => {
|
|
1243
|
+
e.preventDefault();
|
|
1244
|
+
e.stopPropagation();
|
|
1245
|
+
setDragOver(true);
|
|
1246
|
+
}, []);
|
|
1247
|
+
const handleDragLeave = useCallback3((e) => {
|
|
1248
|
+
e.preventDefault();
|
|
1249
|
+
e.stopPropagation();
|
|
1250
|
+
setDragOver(false);
|
|
1251
|
+
}, []);
|
|
1252
|
+
const handleDrop = useCallback3(
|
|
1253
|
+
(e) => {
|
|
1254
|
+
e.preventDefault();
|
|
1255
|
+
e.stopPropagation();
|
|
1256
|
+
setDragOver(false);
|
|
1257
|
+
if (e.dataTransfer.files.length > 0) {
|
|
1258
|
+
void addFiles(e.dataTransfer.files);
|
|
1259
|
+
}
|
|
1260
|
+
},
|
|
1261
|
+
[addFiles]
|
|
1262
|
+
);
|
|
838
1263
|
return /* @__PURE__ */ jsxs7(
|
|
839
1264
|
"div",
|
|
840
1265
|
{
|
|
841
1266
|
className: twMerge6(
|
|
842
|
-
"
|
|
1267
|
+
"relative shrink-0 rounded-3xl border bg-surface",
|
|
1268
|
+
"shadow-lg shadow-black/10",
|
|
1269
|
+
"transition-all duration-200",
|
|
1270
|
+
"focus-within:border-accent/40 focus-within:shadow-accent/5",
|
|
1271
|
+
dragOver ? "border-accent/60 bg-accent/5" : "border-border/60",
|
|
843
1272
|
className
|
|
844
1273
|
),
|
|
1274
|
+
onDragOver: handleDragOver,
|
|
1275
|
+
onDragLeave: handleDragLeave,
|
|
1276
|
+
onDrop: handleDrop,
|
|
845
1277
|
children: [
|
|
846
1278
|
/* @__PURE__ */ jsx9(
|
|
847
|
-
"
|
|
1279
|
+
"input",
|
|
848
1280
|
{
|
|
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"
|
|
1281
|
+
ref: fileInputRef,
|
|
1282
|
+
type: "file",
|
|
1283
|
+
multiple: true,
|
|
1284
|
+
accept: "image/png,image/jpeg,image/gif,image/webp,application/pdf",
|
|
1285
|
+
onChange: handleFileInputChange,
|
|
1286
|
+
className: "hidden",
|
|
1287
|
+
"aria-hidden": "true"
|
|
866
1288
|
}
|
|
867
1289
|
),
|
|
868
|
-
/* @__PURE__ */ jsx9(
|
|
869
|
-
|
|
1290
|
+
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(
|
|
1291
|
+
AttachmentPreview,
|
|
870
1292
|
{
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
1293
|
+
attachment: att,
|
|
1294
|
+
onRemove: () => removeAttachment(i)
|
|
1295
|
+
},
|
|
1296
|
+
`${att.filename}-${i}`
|
|
1297
|
+
)) }),
|
|
1298
|
+
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" }) }),
|
|
1299
|
+
/* @__PURE__ */ jsxs7("div", { className: "flex items-end", children: [
|
|
1300
|
+
/* @__PURE__ */ jsx9(
|
|
1301
|
+
"button",
|
|
1302
|
+
{
|
|
1303
|
+
type: "button",
|
|
1304
|
+
onClick: handleFileSelect,
|
|
1305
|
+
disabled: isLoading || attachments.length >= MAX_ATTACHMENTS,
|
|
1306
|
+
"aria-label": "Attach file",
|
|
1307
|
+
className: twMerge6(
|
|
1308
|
+
"flex-shrink-0 ml-2 mb-3",
|
|
1309
|
+
"inline-flex items-center justify-center",
|
|
1310
|
+
"w-9 h-9 rounded-full",
|
|
1311
|
+
"transition-all duration-200",
|
|
1312
|
+
"text-text-muted/60 hover:text-text-secondary hover:bg-text-muted/10",
|
|
1313
|
+
"focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent",
|
|
1314
|
+
"disabled:opacity-30 disabled:cursor-not-allowed disabled:hover:bg-transparent"
|
|
1315
|
+
),
|
|
1316
|
+
children: /* @__PURE__ */ jsx9(PaperclipIcon, {})
|
|
1317
|
+
}
|
|
1318
|
+
),
|
|
1319
|
+
/* @__PURE__ */ jsx9(
|
|
1320
|
+
"textarea",
|
|
1321
|
+
{
|
|
1322
|
+
ref: textareaRef,
|
|
1323
|
+
value,
|
|
1324
|
+
onChange: handleChange,
|
|
1325
|
+
onKeyDown: handleKeyDown,
|
|
1326
|
+
onPaste: handlePaste,
|
|
1327
|
+
placeholder,
|
|
1328
|
+
rows: 1,
|
|
1329
|
+
disabled: isLoading,
|
|
1330
|
+
className: twMerge6(
|
|
1331
|
+
"flex-1 resize-none bg-transparent",
|
|
1332
|
+
"pl-2 pr-14 pt-4 pb-4 text-[15px] leading-relaxed",
|
|
1333
|
+
"text-text-primary placeholder:text-text-muted/70",
|
|
1334
|
+
"focus:outline-none",
|
|
1335
|
+
"disabled:opacity-50 disabled:cursor-not-allowed",
|
|
1336
|
+
"overflow-hidden"
|
|
1337
|
+
),
|
|
1338
|
+
style: { colorScheme: "dark" },
|
|
1339
|
+
"aria-label": "Message input"
|
|
1340
|
+
}
|
|
1341
|
+
),
|
|
1342
|
+
/* @__PURE__ */ jsx9(
|
|
1343
|
+
"button",
|
|
1344
|
+
{
|
|
1345
|
+
type: "button",
|
|
1346
|
+
onClick: isLoading && onStop ? onStop : handleSend,
|
|
1347
|
+
disabled: !canSend && !isLoading,
|
|
1348
|
+
"aria-label": isLoading ? "Stop generating" : "Send message",
|
|
1349
|
+
className: twMerge6(
|
|
1350
|
+
"absolute bottom-3 right-3",
|
|
1351
|
+
"inline-flex items-center justify-center",
|
|
1352
|
+
"w-9 h-9 rounded-full",
|
|
1353
|
+
"transition-all duration-200",
|
|
1354
|
+
"focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent",
|
|
1355
|
+
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"
|
|
1356
|
+
),
|
|
1357
|
+
children: isLoading ? /* @__PURE__ */ jsx9(StopIcon, {}) : /* @__PURE__ */ jsx9(ArrowUpIcon, {})
|
|
1358
|
+
}
|
|
1359
|
+
)
|
|
1360
|
+
] })
|
|
885
1361
|
]
|
|
886
1362
|
}
|
|
887
1363
|
);
|
|
@@ -894,6 +1370,7 @@ function WelcomeScreen({
|
|
|
894
1370
|
title = "Welcome",
|
|
895
1371
|
message = "How can I help you today?",
|
|
896
1372
|
icon,
|
|
1373
|
+
iconClassName,
|
|
897
1374
|
suggestedQuestions = [],
|
|
898
1375
|
onQuestionSelect,
|
|
899
1376
|
className
|
|
@@ -906,12 +1383,15 @@ function WelcomeScreen({
|
|
|
906
1383
|
className
|
|
907
1384
|
),
|
|
908
1385
|
children: [
|
|
909
|
-
/* @__PURE__ */ jsx10(
|
|
1386
|
+
icon ? iconClassName ? /* @__PURE__ */ jsx10("div", { className: iconClassName, "aria-hidden": "true", children: icon }) : icon : /* @__PURE__ */ jsx10(
|
|
910
1387
|
"div",
|
|
911
1388
|
{
|
|
912
|
-
className:
|
|
1389
|
+
className: twMerge7(
|
|
1390
|
+
"w-14 h-14 rounded-2xl bg-accent/10 border border-border flex items-center justify-center pulse-glow",
|
|
1391
|
+
iconClassName
|
|
1392
|
+
),
|
|
913
1393
|
"aria-hidden": "true",
|
|
914
|
-
children:
|
|
1394
|
+
children: /* @__PURE__ */ jsx10("span", { className: "text-2xl", children: "\u2726" })
|
|
915
1395
|
}
|
|
916
1396
|
),
|
|
917
1397
|
/* @__PURE__ */ jsxs8("div", { className: "flex flex-col gap-2", children: [
|
|
@@ -921,7 +1401,7 @@ function WelcomeScreen({
|
|
|
921
1401
|
suggestedQuestions.length > 0 && /* @__PURE__ */ jsx10(
|
|
922
1402
|
"div",
|
|
923
1403
|
{
|
|
924
|
-
className: "flex flex-wrap justify-center gap-2 max-w-
|
|
1404
|
+
className: "flex flex-wrap justify-center gap-2 max-w-xl",
|
|
925
1405
|
role: "group",
|
|
926
1406
|
"aria-label": "Suggested questions",
|
|
927
1407
|
children: suggestedQuestions.map((question) => /* @__PURE__ */ jsx10(
|
|
@@ -930,7 +1410,7 @@ function WelcomeScreen({
|
|
|
930
1410
|
type: "button",
|
|
931
1411
|
onClick: () => onQuestionSelect?.(question),
|
|
932
1412
|
className: twMerge7(
|
|
933
|
-
"px-
|
|
1413
|
+
"px-3.5 py-1.5 rounded-full text-[12px]",
|
|
934
1414
|
"border border-border bg-transparent text-text-secondary",
|
|
935
1415
|
"hover:bg-accent/10 hover:border-interactive hover:text-text-primary",
|
|
936
1416
|
"focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent",
|
|
@@ -949,7 +1429,8 @@ function WelcomeScreen({
|
|
|
949
1429
|
|
|
950
1430
|
// src/streaming/StreamingMessage/StreamingMessage.tsx
|
|
951
1431
|
import { useEffect as useEffect3, useRef as useRef5 } from "react";
|
|
952
|
-
import {
|
|
1432
|
+
import { twMerge as twMerge8 } from "tailwind-merge";
|
|
1433
|
+
import { WaveLoader } from "@surf-kit/core";
|
|
953
1434
|
|
|
954
1435
|
// src/hooks/useCharacterDrain.ts
|
|
955
1436
|
import { useState as useState3, useRef as useRef4, useEffect as useEffect2 } from "react";
|
|
@@ -978,7 +1459,10 @@ function useCharacterDrain(target, msPerChar = 15) {
|
|
|
978
1459
|
const elapsed = now - lastTimeRef.current;
|
|
979
1460
|
const charsToAdvance = Math.floor(elapsed / msPerCharRef.current);
|
|
980
1461
|
if (charsToAdvance > 0 && indexRef.current < currentTarget.length) {
|
|
981
|
-
|
|
1462
|
+
let nextIndex = Math.min(indexRef.current + charsToAdvance, currentTarget.length);
|
|
1463
|
+
while (nextIndex < currentTarget.length && currentTarget[nextIndex - 1].trim() === "") {
|
|
1464
|
+
nextIndex++;
|
|
1465
|
+
}
|
|
982
1466
|
indexRef.current = nextIndex;
|
|
983
1467
|
lastTimeRef.current = now;
|
|
984
1468
|
setDisplayed(currentTarget.slice(0, nextIndex));
|
|
@@ -1023,14 +1507,36 @@ var phaseLabels = {
|
|
|
1023
1507
|
generating: "Writing...",
|
|
1024
1508
|
verifying: "Verifying..."
|
|
1025
1509
|
};
|
|
1510
|
+
var CURSOR_STYLES = `
|
|
1511
|
+
.sk-streaming-cursor > :not(ul,ol,blockquote,div:has(table)):last-child::after,
|
|
1512
|
+
.sk-streaming-cursor > :is(ul,ol):last-child > li:last-child::after,
|
|
1513
|
+
.sk-streaming-cursor > blockquote:last-child > p:last-child::after,
|
|
1514
|
+
.sk-streaming-cursor > div:has(table):last-child table tbody tr:last-child td:last-child::after {
|
|
1515
|
+
content: "";
|
|
1516
|
+
display: inline-block;
|
|
1517
|
+
width: 2px;
|
|
1518
|
+
height: 1em;
|
|
1519
|
+
background: var(--color-accent, #38bdf8);
|
|
1520
|
+
animation: sk-cursor-blink 0.8s steps(1) infinite;
|
|
1521
|
+
margin-left: 2px;
|
|
1522
|
+
vertical-align: text-bottom;
|
|
1523
|
+
}
|
|
1524
|
+
@keyframes sk-cursor-blink {
|
|
1525
|
+
0%, 60% { opacity: 1; }
|
|
1526
|
+
61%, 100% { opacity: 0; }
|
|
1527
|
+
}
|
|
1528
|
+
`;
|
|
1026
1529
|
function StreamingMessage({
|
|
1027
1530
|
stream,
|
|
1028
1531
|
onComplete,
|
|
1532
|
+
onDraining,
|
|
1029
1533
|
showPhases = true,
|
|
1030
1534
|
className
|
|
1031
1535
|
}) {
|
|
1032
1536
|
const onCompleteRef = useRef5(onComplete);
|
|
1033
1537
|
onCompleteRef.current = onComplete;
|
|
1538
|
+
const onDrainingRef = useRef5(onDraining);
|
|
1539
|
+
onDrainingRef.current = onDraining;
|
|
1034
1540
|
const wasActiveRef = useRef5(stream.active);
|
|
1035
1541
|
useEffect3(() => {
|
|
1036
1542
|
if (wasActiveRef.current && !stream.active) {
|
|
@@ -1039,35 +1545,40 @@ function StreamingMessage({
|
|
|
1039
1545
|
wasActiveRef.current = stream.active;
|
|
1040
1546
|
}, [stream.active]);
|
|
1041
1547
|
const phaseLabel = phaseLabels[stream.phase];
|
|
1042
|
-
const { displayed:
|
|
1043
|
-
|
|
1548
|
+
const { displayed: rawDisplayed, isDraining } = useCharacterDrain(stream.content);
|
|
1549
|
+
const displayedContent = stream.active || isDraining ? rawDisplayed.trimEnd() : rawDisplayed;
|
|
1550
|
+
useEffect3(() => {
|
|
1551
|
+
onDrainingRef.current?.(isDraining);
|
|
1552
|
+
}, [isDraining]);
|
|
1553
|
+
const agentLabel = stream.agent ? stream.agent.replace("_agent", "").replace("_", " ") : null;
|
|
1554
|
+
const showPhaseIndicator = showPhases && stream.active && stream.phase !== "idle" && !displayedContent;
|
|
1555
|
+
const showCursor = (stream.active || isDraining) && !!displayedContent;
|
|
1556
|
+
return /* @__PURE__ */ jsxs9("div", { className: twMerge8("flex w-full flex-col items-start", className), "data-testid": "streaming-message", children: [
|
|
1044
1557
|
/* @__PURE__ */ jsxs9("div", { "aria-live": "assertive", className: "sr-only", children: [
|
|
1045
1558
|
stream.active && stream.phase !== "idle" && "Response started",
|
|
1046
1559
|
!stream.active && stream.content && "Response complete"
|
|
1047
1560
|
] }),
|
|
1561
|
+
showCursor && /* @__PURE__ */ jsx11("style", { children: CURSOR_STYLES }),
|
|
1562
|
+
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
1563
|
/* @__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
|
-
|
|
1564
|
+
showPhaseIndicator && /* @__PURE__ */ jsxs9(
|
|
1050
1565
|
"div",
|
|
1051
1566
|
{
|
|
1052
|
-
className: "flex items-center gap-2
|
|
1567
|
+
className: "flex items-center gap-2 text-sm text-text-secondary",
|
|
1053
1568
|
"data-testid": "phase-indicator",
|
|
1054
1569
|
children: [
|
|
1055
|
-
/* @__PURE__ */ jsx11("span", { "aria-hidden": "true", children: /* @__PURE__ */ jsx11(
|
|
1570
|
+
/* @__PURE__ */ jsx11("span", { "aria-hidden": "true", children: /* @__PURE__ */ jsx11(WaveLoader, { size: "sm", color: "#38bdf8" }) }),
|
|
1056
1571
|
/* @__PURE__ */ jsx11("span", { children: phaseLabel })
|
|
1057
1572
|
]
|
|
1058
1573
|
}
|
|
1059
1574
|
),
|
|
1060
|
-
/* @__PURE__ */
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
"data-testid": "streaming-cursor"
|
|
1068
|
-
}
|
|
1069
|
-
)
|
|
1070
|
-
] })
|
|
1575
|
+
displayedContent && /* @__PURE__ */ jsx11(
|
|
1576
|
+
ResponseMessage,
|
|
1577
|
+
{
|
|
1578
|
+
content: displayedContent,
|
|
1579
|
+
className: showCursor ? "sk-streaming-cursor" : void 0
|
|
1580
|
+
}
|
|
1581
|
+
)
|
|
1071
1582
|
] })
|
|
1072
1583
|
] });
|
|
1073
1584
|
}
|
|
@@ -1098,7 +1609,7 @@ function AgentChat({
|
|
|
1098
1609
|
return /* @__PURE__ */ jsxs10(
|
|
1099
1610
|
"div",
|
|
1100
1611
|
{
|
|
1101
|
-
className:
|
|
1612
|
+
className: twMerge9(
|
|
1102
1613
|
"flex flex-col h-full bg-canvas border border-border rounded-xl overflow-hidden",
|
|
1103
1614
|
className
|
|
1104
1615
|
),
|
|
@@ -1134,14 +1645,14 @@ function AgentChat({
|
|
|
1134
1645
|
onQuestionSelect: handleQuestionSelect
|
|
1135
1646
|
}
|
|
1136
1647
|
),
|
|
1137
|
-
/* @__PURE__ */ jsx12(MessageComposer, { onSend: handleSend, isLoading: state.isLoading })
|
|
1648
|
+
/* @__PURE__ */ jsx12(MessageComposer, { onSend: handleSend, onStop: actions.stop, isLoading: state.isLoading })
|
|
1138
1649
|
]
|
|
1139
1650
|
}
|
|
1140
1651
|
);
|
|
1141
1652
|
}
|
|
1142
1653
|
|
|
1143
1654
|
// src/chat/ConversationList/ConversationList.tsx
|
|
1144
|
-
import { twMerge as
|
|
1655
|
+
import { twMerge as twMerge10 } from "tailwind-merge";
|
|
1145
1656
|
import { jsx as jsx13, jsxs as jsxs11 } from "react/jsx-runtime";
|
|
1146
1657
|
function ConversationList({
|
|
1147
1658
|
conversations,
|
|
@@ -1155,14 +1666,14 @@ function ConversationList({
|
|
|
1155
1666
|
"nav",
|
|
1156
1667
|
{
|
|
1157
1668
|
"aria-label": "Conversation list",
|
|
1158
|
-
className:
|
|
1669
|
+
className: twMerge10("flex flex-col flex-1 min-h-0", className),
|
|
1159
1670
|
children: [
|
|
1160
|
-
onNew && /* @__PURE__ */ jsx13("div", { className: "
|
|
1671
|
+
onNew && /* @__PURE__ */ jsx13("div", { className: "px-5 pt-1 pb-3 border-b border-border", children: /* @__PURE__ */ jsx13(
|
|
1161
1672
|
"button",
|
|
1162
1673
|
{
|
|
1163
1674
|
type: "button",
|
|
1164
1675
|
onClick: onNew,
|
|
1165
|
-
className: "w-full px-4 py-2
|
|
1676
|
+
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",
|
|
1166
1677
|
children: "New conversation"
|
|
1167
1678
|
}
|
|
1168
1679
|
) }),
|
|
@@ -1172,10 +1683,10 @@ function ConversationList({
|
|
|
1172
1683
|
return /* @__PURE__ */ jsxs11(
|
|
1173
1684
|
"li",
|
|
1174
1685
|
{
|
|
1175
|
-
className:
|
|
1176
|
-
"flex items-start
|
|
1177
|
-
"hover:bg-surface",
|
|
1178
|
-
isActive
|
|
1686
|
+
className: twMerge10(
|
|
1687
|
+
"flex items-start transition-colors duration-150",
|
|
1688
|
+
"hover:bg-surface-raised",
|
|
1689
|
+
isActive ? "bg-accent-subtlest border-l-[3px] border-l-accent" : "border-l-[3px] border-l-transparent"
|
|
1179
1690
|
),
|
|
1180
1691
|
children: [
|
|
1181
1692
|
/* @__PURE__ */ jsxs11(
|
|
@@ -1184,10 +1695,10 @@ function ConversationList({
|
|
|
1184
1695
|
type: "button",
|
|
1185
1696
|
onClick: () => onSelect(conversation.id),
|
|
1186
1697
|
"aria-current": isActive ? "true" : void 0,
|
|
1187
|
-
className: "flex-1 min-w-0 text-left px-
|
|
1698
|
+
className: "flex-1 min-w-0 text-left px-5 py-2.5",
|
|
1188
1699
|
children: [
|
|
1189
|
-
/* @__PURE__ */ jsx13("div", { className: "text-sm font-medium text-
|
|
1190
|
-
/* @__PURE__ */ jsx13("div", { className: "text-xs text-
|
|
1700
|
+
/* @__PURE__ */ jsx13("div", { className: "text-sm font-medium text-text-primary truncate", children: conversation.title }),
|
|
1701
|
+
/* @__PURE__ */ jsx13("div", { className: "text-xs text-text-muted truncate mt-0.5 leading-relaxed", children: conversation.lastMessage })
|
|
1191
1702
|
]
|
|
1192
1703
|
}
|
|
1193
1704
|
),
|
|
@@ -1197,7 +1708,7 @@ function ConversationList({
|
|
|
1197
1708
|
type: "button",
|
|
1198
1709
|
onClick: () => onDelete(conversation.id),
|
|
1199
1710
|
"aria-label": `Delete ${conversation.title}`,
|
|
1200
|
-
className: "shrink-0 p-1.5 m-2 rounded-lg text-
|
|
1711
|
+
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",
|
|
1201
1712
|
children: /* @__PURE__ */ jsxs11(
|
|
1202
1713
|
"svg",
|
|
1203
1714
|
{
|
|
@@ -1224,7 +1735,7 @@ function ConversationList({
|
|
|
1224
1735
|
conversation.id
|
|
1225
1736
|
);
|
|
1226
1737
|
}),
|
|
1227
|
-
conversations.length === 0 && /* @__PURE__ */ jsx13("li", { className: "px-
|
|
1738
|
+
conversations.length === 0 && /* @__PURE__ */ jsx13("li", { className: "px-5 py-12 text-center", children: /* @__PURE__ */ jsx13("span", { className: "text-sm text-text-muted font-body", children: "No conversations yet" }) })
|
|
1228
1739
|
] })
|
|
1229
1740
|
]
|
|
1230
1741
|
}
|