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