@yushaw/sanqian-chat 0.1.1
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/core/index.d.mts +156 -0
- package/dist/core/index.d.ts +156 -0
- package/dist/core/index.js +176 -0
- package/dist/core/index.mjs +149 -0
- package/dist/main/index.d.mts +58 -0
- package/dist/main/index.d.ts +58 -0
- package/dist/main/index.js +299 -0
- package/dist/main/index.mjs +272 -0
- package/dist/preload/index.d.ts +67 -0
- package/dist/preload/index.js +38 -0
- package/dist/renderer/index.d.mts +340 -0
- package/dist/renderer/index.d.ts +340 -0
- package/dist/renderer/index.js +1171 -0
- package/dist/renderer/index.mjs +1131 -0
- package/package.json +84 -0
|
@@ -0,0 +1,1171 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/renderer/index.ts
|
|
21
|
+
var renderer_exports = {};
|
|
22
|
+
__export(renderer_exports, {
|
|
23
|
+
ChatInput: () => ChatInput,
|
|
24
|
+
FloatingChat: () => FloatingChat,
|
|
25
|
+
I18nProvider: () => I18nProvider,
|
|
26
|
+
MessageBubble: () => MessageBubble,
|
|
27
|
+
MessageList: () => MessageList,
|
|
28
|
+
ThemeProvider: () => ThemeProvider,
|
|
29
|
+
createIpcAdapter: () => createIpcAdapter,
|
|
30
|
+
createSdkAdapter: () => createSdkAdapter,
|
|
31
|
+
getTranslations: () => getTranslations,
|
|
32
|
+
useChat: () => useChat,
|
|
33
|
+
useI18n: () => useI18n,
|
|
34
|
+
useStandaloneI18n: () => useStandaloneI18n,
|
|
35
|
+
useStandaloneTheme: () => useStandaloneTheme,
|
|
36
|
+
useTheme: () => useTheme
|
|
37
|
+
});
|
|
38
|
+
module.exports = __toCommonJS(renderer_exports);
|
|
39
|
+
|
|
40
|
+
// src/renderer/hooks/useChat.ts
|
|
41
|
+
var import_react = require("react");
|
|
42
|
+
var TYPEWRITER_DELAYS = { VERY_FAST: 2, FAST: 5, NORMAL: 10, SLOW: 20 };
|
|
43
|
+
var TYPEWRITER_THRESHOLDS = { VERY_FAST: 100, FAST: 50, NORMAL: 20 };
|
|
44
|
+
function useChat(options) {
|
|
45
|
+
const { adapter, onError, onConversationChange } = options;
|
|
46
|
+
const [messages, setMessages] = (0, import_react.useState)([]);
|
|
47
|
+
const [isLoading, setIsLoading] = (0, import_react.useState)(false);
|
|
48
|
+
const [isStreaming, setIsStreaming] = (0, import_react.useState)(false);
|
|
49
|
+
const [error, setError] = (0, import_react.useState)(null);
|
|
50
|
+
const [conversationId, setConversationId] = (0, import_react.useState)(options.conversationId ?? null);
|
|
51
|
+
const [conversationTitle, setConversationTitle] = (0, import_react.useState)(null);
|
|
52
|
+
const [pendingInterrupt, setPendingInterrupt] = (0, import_react.useState)(null);
|
|
53
|
+
const cancelRef = (0, import_react.useRef)(null);
|
|
54
|
+
const isMountedRef = (0, import_react.useRef)(true);
|
|
55
|
+
const messagesRef = (0, import_react.useRef)(messages);
|
|
56
|
+
const conversationIdRef = (0, import_react.useRef)(conversationId);
|
|
57
|
+
const currentRunIdRef = (0, import_react.useRef)(null);
|
|
58
|
+
const currentBlocksRef = (0, import_react.useRef)([]);
|
|
59
|
+
const currentTextBlockIndexRef = (0, import_react.useRef)(-1);
|
|
60
|
+
const needsContentClearRef = (0, import_react.useRef)(false);
|
|
61
|
+
const fullContentRef = (0, import_react.useRef)("");
|
|
62
|
+
const tokenQueueRef = (0, import_react.useRef)([]);
|
|
63
|
+
const displayedContentRef = (0, import_react.useRef)("");
|
|
64
|
+
const typewriterIntervalRef = (0, import_react.useRef)(null);
|
|
65
|
+
const currentAssistantMessageIdRef = (0, import_react.useRef)(null);
|
|
66
|
+
(0, import_react.useEffect)(() => {
|
|
67
|
+
messagesRef.current = messages;
|
|
68
|
+
}, [messages]);
|
|
69
|
+
(0, import_react.useEffect)(() => {
|
|
70
|
+
conversationIdRef.current = conversationId;
|
|
71
|
+
}, [conversationId]);
|
|
72
|
+
(0, import_react.useEffect)(() => {
|
|
73
|
+
isMountedRef.current = true;
|
|
74
|
+
return () => {
|
|
75
|
+
isMountedRef.current = false;
|
|
76
|
+
cancelRef.current?.();
|
|
77
|
+
if (typewriterIntervalRef.current) clearTimeout(typewriterIntervalRef.current);
|
|
78
|
+
};
|
|
79
|
+
}, []);
|
|
80
|
+
const flushTypewriter = (0, import_react.useCallback)(() => {
|
|
81
|
+
if (typewriterIntervalRef.current) {
|
|
82
|
+
clearTimeout(typewriterIntervalRef.current);
|
|
83
|
+
typewriterIntervalRef.current = null;
|
|
84
|
+
}
|
|
85
|
+
if (tokenQueueRef.current.length > 0 && currentAssistantMessageIdRef.current) {
|
|
86
|
+
displayedContentRef.current += tokenQueueRef.current.join("");
|
|
87
|
+
tokenQueueRef.current = [];
|
|
88
|
+
setMessages((prev) => {
|
|
89
|
+
const idx = prev.findIndex((m) => m.id === currentAssistantMessageIdRef.current);
|
|
90
|
+
if (idx === -1) return prev;
|
|
91
|
+
const updated = [...prev];
|
|
92
|
+
updated[idx] = { ...prev[idx], content: displayedContentRef.current, blocks: [...currentBlocksRef.current] };
|
|
93
|
+
return updated;
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}, []);
|
|
97
|
+
const handleStreamEvent = (0, import_react.useCallback)((event, assistantMessageId) => {
|
|
98
|
+
if (!isMountedRef.current) return;
|
|
99
|
+
currentAssistantMessageIdRef.current = assistantMessageId;
|
|
100
|
+
switch (event.type) {
|
|
101
|
+
case "text": {
|
|
102
|
+
const content = event.content;
|
|
103
|
+
if (!content) break;
|
|
104
|
+
if (needsContentClearRef.current || fullContentRef.current === "") {
|
|
105
|
+
flushTypewriter();
|
|
106
|
+
needsContentClearRef.current = false;
|
|
107
|
+
fullContentRef.current = "";
|
|
108
|
+
displayedContentRef.current = "";
|
|
109
|
+
currentBlocksRef.current.forEach((b) => {
|
|
110
|
+
if (b.type === "text") b.isIntermediate = true;
|
|
111
|
+
});
|
|
112
|
+
currentBlocksRef.current.push({ type: "text", content: "", timestamp: Date.now(), isIntermediate: false });
|
|
113
|
+
currentTextBlockIndexRef.current = currentBlocksRef.current.length - 1;
|
|
114
|
+
}
|
|
115
|
+
const textToAdd = fullContentRef.current === "" ? content.trimStart() : content;
|
|
116
|
+
fullContentRef.current += textToAdd;
|
|
117
|
+
if (currentTextBlockIndexRef.current >= 0) {
|
|
118
|
+
currentBlocksRef.current[currentTextBlockIndexRef.current].content += textToAdd;
|
|
119
|
+
}
|
|
120
|
+
tokenQueueRef.current.push(...textToAdd.split(""));
|
|
121
|
+
if (!typewriterIntervalRef.current) {
|
|
122
|
+
const tick = () => {
|
|
123
|
+
const char = tokenQueueRef.current.shift();
|
|
124
|
+
if (char !== void 0) {
|
|
125
|
+
displayedContentRef.current += char;
|
|
126
|
+
setMessages((prev) => {
|
|
127
|
+
const idx = prev.findIndex((m) => m.id === assistantMessageId);
|
|
128
|
+
if (idx === -1) return prev;
|
|
129
|
+
const updated = [...prev];
|
|
130
|
+
updated[idx] = { ...prev[idx], content: displayedContentRef.current, isStreaming: true, blocks: [...currentBlocksRef.current] };
|
|
131
|
+
return updated;
|
|
132
|
+
});
|
|
133
|
+
const qLen = tokenQueueRef.current.length;
|
|
134
|
+
const delay = qLen > TYPEWRITER_THRESHOLDS.VERY_FAST ? TYPEWRITER_DELAYS.VERY_FAST : qLen > TYPEWRITER_THRESHOLDS.FAST ? TYPEWRITER_DELAYS.FAST : qLen > TYPEWRITER_THRESHOLDS.NORMAL ? TYPEWRITER_DELAYS.NORMAL : TYPEWRITER_DELAYS.SLOW;
|
|
135
|
+
if (typewriterIntervalRef.current !== null) {
|
|
136
|
+
typewriterIntervalRef.current = setTimeout(tick, delay);
|
|
137
|
+
}
|
|
138
|
+
} else {
|
|
139
|
+
typewriterIntervalRef.current = null;
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
typewriterIntervalRef.current = setTimeout(tick, TYPEWRITER_DELAYS.SLOW);
|
|
143
|
+
}
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
case "thinking": {
|
|
147
|
+
const content = event.content;
|
|
148
|
+
if (!content) break;
|
|
149
|
+
let thinkingBlockIdx = currentBlocksRef.current.findIndex((b) => b.type === "thinking" && !b.isIntermediate);
|
|
150
|
+
if (thinkingBlockIdx === -1) {
|
|
151
|
+
currentBlocksRef.current.push({ type: "thinking", content: "", timestamp: Date.now(), isIntermediate: false });
|
|
152
|
+
thinkingBlockIdx = currentBlocksRef.current.length - 1;
|
|
153
|
+
}
|
|
154
|
+
currentBlocksRef.current[thinkingBlockIdx].content += content;
|
|
155
|
+
setMessages((prev) => {
|
|
156
|
+
const idx = prev.findIndex((m) => m.id === assistantMessageId);
|
|
157
|
+
if (idx === -1) return prev;
|
|
158
|
+
const updated = [...prev];
|
|
159
|
+
const msg = prev[idx];
|
|
160
|
+
updated[idx] = {
|
|
161
|
+
...msg,
|
|
162
|
+
thinking: (msg.thinking || "") + content,
|
|
163
|
+
currentThinking: (msg.currentThinking || "") + content,
|
|
164
|
+
isThinkingStreaming: true,
|
|
165
|
+
blocks: [...currentBlocksRef.current]
|
|
166
|
+
};
|
|
167
|
+
return updated;
|
|
168
|
+
});
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
case "tool_call": {
|
|
172
|
+
flushTypewriter();
|
|
173
|
+
const tc = event.tool_call;
|
|
174
|
+
if (!tc) break;
|
|
175
|
+
needsContentClearRef.current = true;
|
|
176
|
+
let args = {};
|
|
177
|
+
try {
|
|
178
|
+
args = JSON.parse(tc.function?.arguments || "{}");
|
|
179
|
+
} catch (e) {
|
|
180
|
+
console.warn("[useChat] Failed to parse tool arguments:", e);
|
|
181
|
+
}
|
|
182
|
+
currentBlocksRef.current.push({
|
|
183
|
+
type: "tool_call",
|
|
184
|
+
content: "",
|
|
185
|
+
timestamp: Date.now(),
|
|
186
|
+
toolName: tc.function?.name || "",
|
|
187
|
+
toolArgs: args,
|
|
188
|
+
toolCallId: tc.id,
|
|
189
|
+
toolStatus: "running",
|
|
190
|
+
isIntermediate: true
|
|
191
|
+
});
|
|
192
|
+
setMessages((prev) => {
|
|
193
|
+
const idx = prev.findIndex((m) => m.id === assistantMessageId);
|
|
194
|
+
if (idx === -1) return prev;
|
|
195
|
+
const msg = prev[idx];
|
|
196
|
+
const newTc = { id: tc.id, name: tc.function?.name || "", arguments: args, status: "running" };
|
|
197
|
+
const updated = [...prev];
|
|
198
|
+
updated[idx] = { ...msg, toolCalls: [...msg.toolCalls || [], newTc], blocks: [...currentBlocksRef.current] };
|
|
199
|
+
return updated;
|
|
200
|
+
});
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
203
|
+
case "tool_result": {
|
|
204
|
+
flushTypewriter();
|
|
205
|
+
const toolId = event.tool_call_id;
|
|
206
|
+
const result = typeof event.result === "string" ? event.result : JSON.stringify(event.result);
|
|
207
|
+
const blockIdx = currentBlocksRef.current.findIndex((b) => b.type === "tool_call" && b.toolCallId === toolId);
|
|
208
|
+
if (blockIdx !== -1) currentBlocksRef.current[blockIdx].toolStatus = "completed";
|
|
209
|
+
currentBlocksRef.current.push({ type: "tool_result", content: result, timestamp: Date.now(), toolCallId: toolId, isIntermediate: true });
|
|
210
|
+
setMessages((prev) => {
|
|
211
|
+
const idx = prev.findIndex((m) => m.id === assistantMessageId);
|
|
212
|
+
if (idx === -1) return prev;
|
|
213
|
+
const msg = prev[idx];
|
|
214
|
+
const updated = [...prev];
|
|
215
|
+
updated[idx] = {
|
|
216
|
+
...msg,
|
|
217
|
+
toolCalls: msg.toolCalls?.map((t) => t.id === toolId ? { ...t, status: "completed", result: event.result } : t),
|
|
218
|
+
blocks: [...currentBlocksRef.current]
|
|
219
|
+
};
|
|
220
|
+
return updated;
|
|
221
|
+
});
|
|
222
|
+
break;
|
|
223
|
+
}
|
|
224
|
+
case "done": {
|
|
225
|
+
flushTypewriter();
|
|
226
|
+
const finalContent = fullContentRef.current;
|
|
227
|
+
if (currentTextBlockIndexRef.current >= 0 && currentTextBlockIndexRef.current < currentBlocksRef.current.length) {
|
|
228
|
+
currentBlocksRef.current[currentTextBlockIndexRef.current].content = finalContent;
|
|
229
|
+
currentBlocksRef.current[currentTextBlockIndexRef.current].isIntermediate = false;
|
|
230
|
+
}
|
|
231
|
+
setMessages((prev) => {
|
|
232
|
+
const idx = prev.findIndex((m) => m.id === assistantMessageId);
|
|
233
|
+
if (idx === -1) return prev;
|
|
234
|
+
const updated = [...prev];
|
|
235
|
+
const msg = prev[idx];
|
|
236
|
+
updated[idx] = {
|
|
237
|
+
...msg,
|
|
238
|
+
content: finalContent || msg.content,
|
|
239
|
+
isStreaming: false,
|
|
240
|
+
isComplete: true,
|
|
241
|
+
isThinkingStreaming: false,
|
|
242
|
+
blocks: [...currentBlocksRef.current]
|
|
243
|
+
};
|
|
244
|
+
return updated;
|
|
245
|
+
});
|
|
246
|
+
currentBlocksRef.current = [];
|
|
247
|
+
currentTextBlockIndexRef.current = -1;
|
|
248
|
+
fullContentRef.current = "";
|
|
249
|
+
needsContentClearRef.current = false;
|
|
250
|
+
tokenQueueRef.current = [];
|
|
251
|
+
displayedContentRef.current = "";
|
|
252
|
+
setConversationId(event.conversationId);
|
|
253
|
+
if (event.title) setConversationTitle(event.title);
|
|
254
|
+
onConversationChange?.(event.conversationId, event.title);
|
|
255
|
+
setIsStreaming(false);
|
|
256
|
+
setIsLoading(false);
|
|
257
|
+
break;
|
|
258
|
+
}
|
|
259
|
+
case "error": {
|
|
260
|
+
flushTypewriter();
|
|
261
|
+
setMessages((prev) => {
|
|
262
|
+
const idx = prev.findIndex((m) => m.id === assistantMessageId);
|
|
263
|
+
if (idx === -1) return prev;
|
|
264
|
+
const updated = [...prev];
|
|
265
|
+
updated[idx] = { ...prev[idx], isStreaming: false, isComplete: true, content: prev[idx].content || `Error: ${event.error}` };
|
|
266
|
+
return updated;
|
|
267
|
+
});
|
|
268
|
+
currentBlocksRef.current = [];
|
|
269
|
+
currentTextBlockIndexRef.current = -1;
|
|
270
|
+
fullContentRef.current = "";
|
|
271
|
+
setError(event.error);
|
|
272
|
+
onError?.(new Error(event.error));
|
|
273
|
+
setIsStreaming(false);
|
|
274
|
+
setIsLoading(false);
|
|
275
|
+
break;
|
|
276
|
+
}
|
|
277
|
+
case "interrupt": {
|
|
278
|
+
flushTypewriter();
|
|
279
|
+
currentRunIdRef.current = event.run_id ?? null;
|
|
280
|
+
const payload = event.interrupt_payload;
|
|
281
|
+
if (payload) {
|
|
282
|
+
setPendingInterrupt(payload);
|
|
283
|
+
}
|
|
284
|
+
break;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}, [onError, onConversationChange, flushTypewriter]);
|
|
288
|
+
const sendMessage = (0, import_react.useCallback)(async (content) => {
|
|
289
|
+
if (!content.trim()) return;
|
|
290
|
+
setError(null);
|
|
291
|
+
if (typewriterIntervalRef.current) {
|
|
292
|
+
clearTimeout(typewriterIntervalRef.current);
|
|
293
|
+
typewriterIntervalRef.current = null;
|
|
294
|
+
}
|
|
295
|
+
currentBlocksRef.current = [];
|
|
296
|
+
currentTextBlockIndexRef.current = -1;
|
|
297
|
+
fullContentRef.current = "";
|
|
298
|
+
needsContentClearRef.current = false;
|
|
299
|
+
tokenQueueRef.current = [];
|
|
300
|
+
displayedContentRef.current = "";
|
|
301
|
+
const userMessage = { id: crypto.randomUUID(), role: "user", content: content.trim(), timestamp: (/* @__PURE__ */ new Date()).toISOString() };
|
|
302
|
+
const assistantMessage = { id: crypto.randomUUID(), role: "assistant", content: "", timestamp: (/* @__PURE__ */ new Date()).toISOString(), isStreaming: true, toolCalls: [], blocks: [] };
|
|
303
|
+
setMessages((prev) => [...prev, userMessage, assistantMessage]);
|
|
304
|
+
setIsLoading(true);
|
|
305
|
+
setIsStreaming(true);
|
|
306
|
+
try {
|
|
307
|
+
if (!adapter.isConnected()) await adapter.connect();
|
|
308
|
+
const apiMessages = messagesRef.current.filter((m) => m.role === "user" || m.role === "assistant").map((m) => ({ role: m.role, content: m.content })).concat({ role: "user", content: content.trim() });
|
|
309
|
+
const { cancel } = await adapter.chatStream(apiMessages, conversationIdRef.current ?? void 0, (event) => handleStreamEvent(event, assistantMessage.id));
|
|
310
|
+
cancelRef.current = cancel;
|
|
311
|
+
} catch (err) {
|
|
312
|
+
const errorMessage = err instanceof Error ? err.message : "Failed to send message";
|
|
313
|
+
setError(errorMessage);
|
|
314
|
+
onError?.(err instanceof Error ? err : new Error(errorMessage));
|
|
315
|
+
setMessages((prev) => {
|
|
316
|
+
const idx = prev.findIndex((m) => m.id === assistantMessage.id);
|
|
317
|
+
if (idx === -1) return prev;
|
|
318
|
+
const updated = [...prev];
|
|
319
|
+
updated[idx] = { ...prev[idx], isStreaming: false, content: `Error: ${errorMessage}` };
|
|
320
|
+
return updated;
|
|
321
|
+
});
|
|
322
|
+
setIsLoading(false);
|
|
323
|
+
setIsStreaming(false);
|
|
324
|
+
}
|
|
325
|
+
}, [adapter, handleStreamEvent, onError]);
|
|
326
|
+
const stopStreaming = (0, import_react.useCallback)(() => {
|
|
327
|
+
cancelRef.current?.();
|
|
328
|
+
cancelRef.current = null;
|
|
329
|
+
flushTypewriter();
|
|
330
|
+
setMessages((prev) => {
|
|
331
|
+
const last = [...prev].reverse().find((m) => m.role === "assistant");
|
|
332
|
+
if (!last?.isStreaming) return prev;
|
|
333
|
+
return prev.map((m) => m.id === last.id ? { ...m, content: fullContentRef.current || m.content, isStreaming: false, isComplete: true } : m);
|
|
334
|
+
});
|
|
335
|
+
currentBlocksRef.current = [];
|
|
336
|
+
fullContentRef.current = "";
|
|
337
|
+
setIsStreaming(false);
|
|
338
|
+
setIsLoading(false);
|
|
339
|
+
}, [flushTypewriter]);
|
|
340
|
+
const clearMessages = (0, import_react.useCallback)(() => {
|
|
341
|
+
cancelRef.current?.();
|
|
342
|
+
if (typewriterIntervalRef.current) clearTimeout(typewriterIntervalRef.current);
|
|
343
|
+
setMessages([]);
|
|
344
|
+
setError(null);
|
|
345
|
+
setIsLoading(false);
|
|
346
|
+
setIsStreaming(false);
|
|
347
|
+
currentBlocksRef.current = [];
|
|
348
|
+
fullContentRef.current = "";
|
|
349
|
+
}, []);
|
|
350
|
+
const loadConversation = (0, import_react.useCallback)(async (id) => {
|
|
351
|
+
try {
|
|
352
|
+
setIsLoading(true);
|
|
353
|
+
setError(null);
|
|
354
|
+
const detail = await adapter.getConversation(id);
|
|
355
|
+
if (!isMountedRef.current) return;
|
|
356
|
+
setMessages(detail.messages);
|
|
357
|
+
setConversationId(detail.id);
|
|
358
|
+
setConversationTitle(detail.title);
|
|
359
|
+
} catch (err) {
|
|
360
|
+
const msg = err instanceof Error ? err.message : "Failed to load conversation";
|
|
361
|
+
setError(msg);
|
|
362
|
+
onError?.(err instanceof Error ? err : new Error(msg));
|
|
363
|
+
} finally {
|
|
364
|
+
if (isMountedRef.current) setIsLoading(false);
|
|
365
|
+
}
|
|
366
|
+
}, [adapter, onError]);
|
|
367
|
+
const newConversation = (0, import_react.useCallback)(() => {
|
|
368
|
+
cancelRef.current?.();
|
|
369
|
+
if (typewriterIntervalRef.current) clearTimeout(typewriterIntervalRef.current);
|
|
370
|
+
setMessages([]);
|
|
371
|
+
setConversationId(null);
|
|
372
|
+
setConversationTitle(null);
|
|
373
|
+
setError(null);
|
|
374
|
+
setIsLoading(false);
|
|
375
|
+
setIsStreaming(false);
|
|
376
|
+
setPendingInterrupt(null);
|
|
377
|
+
currentBlocksRef.current = [];
|
|
378
|
+
fullContentRef.current = "";
|
|
379
|
+
}, []);
|
|
380
|
+
const sendHitlResponse = (0, import_react.useCallback)((response) => {
|
|
381
|
+
adapter.sendHitlResponse?.(response, currentRunIdRef.current ?? void 0);
|
|
382
|
+
}, [adapter]);
|
|
383
|
+
const approveHitl = (0, import_react.useCallback)((remember = false) => {
|
|
384
|
+
sendHitlResponse({ approved: true, remember });
|
|
385
|
+
setPendingInterrupt(null);
|
|
386
|
+
}, [sendHitlResponse]);
|
|
387
|
+
const rejectHitl = (0, import_react.useCallback)((remember = false) => {
|
|
388
|
+
sendHitlResponse({ approved: false, remember });
|
|
389
|
+
setPendingInterrupt(null);
|
|
390
|
+
setIsLoading(false);
|
|
391
|
+
}, [sendHitlResponse]);
|
|
392
|
+
const submitHitlInput = (0, import_react.useCallback)((response) => {
|
|
393
|
+
sendHitlResponse(response);
|
|
394
|
+
setPendingInterrupt(null);
|
|
395
|
+
if (response.cancelled || response.timed_out) setIsLoading(false);
|
|
396
|
+
}, [sendHitlResponse]);
|
|
397
|
+
return {
|
|
398
|
+
messages,
|
|
399
|
+
isLoading,
|
|
400
|
+
isStreaming,
|
|
401
|
+
error,
|
|
402
|
+
conversationId,
|
|
403
|
+
conversationTitle,
|
|
404
|
+
pendingInterrupt,
|
|
405
|
+
sendMessage,
|
|
406
|
+
stopStreaming,
|
|
407
|
+
clearMessages,
|
|
408
|
+
setError,
|
|
409
|
+
approveHitl,
|
|
410
|
+
rejectHitl,
|
|
411
|
+
submitHitlInput,
|
|
412
|
+
loadConversation,
|
|
413
|
+
newConversation
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// src/renderer/hooks/useTheme.tsx
|
|
418
|
+
var import_react2 = require("react");
|
|
419
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
420
|
+
var ThemeContext = (0, import_react2.createContext)(null);
|
|
421
|
+
function ThemeProvider({
|
|
422
|
+
children,
|
|
423
|
+
defaultTheme = "system",
|
|
424
|
+
storageKey = "sanqian-chat-theme"
|
|
425
|
+
}) {
|
|
426
|
+
const [theme, setThemeState] = (0, import_react2.useState)(() => {
|
|
427
|
+
if (typeof window !== "undefined") {
|
|
428
|
+
const stored = localStorage.getItem(storageKey);
|
|
429
|
+
if (stored === "light" || stored === "dark" || stored === "system") {
|
|
430
|
+
return stored;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
return defaultTheme;
|
|
434
|
+
});
|
|
435
|
+
const [resolvedTheme, setResolvedTheme] = (0, import_react2.useState)("light");
|
|
436
|
+
(0, import_react2.useEffect)(() => {
|
|
437
|
+
const applyTheme = (resolved) => {
|
|
438
|
+
const root = document.documentElement;
|
|
439
|
+
if (resolved === "dark") {
|
|
440
|
+
root.classList.add("dark");
|
|
441
|
+
} else {
|
|
442
|
+
root.classList.remove("dark");
|
|
443
|
+
}
|
|
444
|
+
setResolvedTheme(resolved);
|
|
445
|
+
};
|
|
446
|
+
if (theme === "system") {
|
|
447
|
+
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
|
|
448
|
+
applyTheme(mediaQuery.matches ? "dark" : "light");
|
|
449
|
+
const handler = (e) => {
|
|
450
|
+
applyTheme(e.matches ? "dark" : "light");
|
|
451
|
+
};
|
|
452
|
+
mediaQuery.addEventListener("change", handler);
|
|
453
|
+
return () => mediaQuery.removeEventListener("change", handler);
|
|
454
|
+
} else {
|
|
455
|
+
applyTheme(theme);
|
|
456
|
+
}
|
|
457
|
+
}, [theme]);
|
|
458
|
+
const setTheme = (0, import_react2.useCallback)((newTheme) => {
|
|
459
|
+
setThemeState(newTheme);
|
|
460
|
+
if (typeof window !== "undefined") {
|
|
461
|
+
localStorage.setItem(storageKey, newTheme);
|
|
462
|
+
}
|
|
463
|
+
}, [storageKey]);
|
|
464
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ThemeContext.Provider, { value: { theme, resolvedTheme, setTheme }, children });
|
|
465
|
+
}
|
|
466
|
+
function useTheme() {
|
|
467
|
+
const context = (0, import_react2.useContext)(ThemeContext);
|
|
468
|
+
if (!context) {
|
|
469
|
+
throw new Error("useTheme must be used within a ThemeProvider");
|
|
470
|
+
}
|
|
471
|
+
return context;
|
|
472
|
+
}
|
|
473
|
+
function useStandaloneTheme(defaultTheme = "system") {
|
|
474
|
+
const [theme, setThemeState] = (0, import_react2.useState)(defaultTheme);
|
|
475
|
+
const [resolvedTheme, setResolvedTheme] = (0, import_react2.useState)("light");
|
|
476
|
+
(0, import_react2.useEffect)(() => {
|
|
477
|
+
const applyTheme = (resolved) => {
|
|
478
|
+
const root = document.documentElement;
|
|
479
|
+
if (resolved === "dark") {
|
|
480
|
+
root.classList.add("dark");
|
|
481
|
+
} else {
|
|
482
|
+
root.classList.remove("dark");
|
|
483
|
+
}
|
|
484
|
+
setResolvedTheme(resolved);
|
|
485
|
+
};
|
|
486
|
+
if (theme === "system") {
|
|
487
|
+
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
|
|
488
|
+
applyTheme(mediaQuery.matches ? "dark" : "light");
|
|
489
|
+
const handler = (e) => {
|
|
490
|
+
applyTheme(e.matches ? "dark" : "light");
|
|
491
|
+
};
|
|
492
|
+
mediaQuery.addEventListener("change", handler);
|
|
493
|
+
return () => mediaQuery.removeEventListener("change", handler);
|
|
494
|
+
} else {
|
|
495
|
+
applyTheme(theme);
|
|
496
|
+
}
|
|
497
|
+
}, [theme]);
|
|
498
|
+
const setTheme = (0, import_react2.useCallback)((newTheme) => {
|
|
499
|
+
setThemeState(newTheme);
|
|
500
|
+
}, []);
|
|
501
|
+
return { theme, resolvedTheme, setTheme };
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// src/renderer/hooks/useI18n.tsx
|
|
505
|
+
var import_react3 = require("react");
|
|
506
|
+
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
507
|
+
var translations = {
|
|
508
|
+
en: {
|
|
509
|
+
hitl: {
|
|
510
|
+
approvalRequired: "Approval Required",
|
|
511
|
+
inputRequired: "Input Required",
|
|
512
|
+
tool: "Tool",
|
|
513
|
+
approve: "Approve",
|
|
514
|
+
reject: "Reject",
|
|
515
|
+
submit: "Submit",
|
|
516
|
+
cancel: "Cancel"
|
|
517
|
+
},
|
|
518
|
+
input: {
|
|
519
|
+
placeholder: "Type a message...",
|
|
520
|
+
send: "Send",
|
|
521
|
+
stop: "Stop"
|
|
522
|
+
},
|
|
523
|
+
message: {
|
|
524
|
+
thinking: "Thinking...",
|
|
525
|
+
error: "Error",
|
|
526
|
+
loading: "Loading..."
|
|
527
|
+
},
|
|
528
|
+
connection: {
|
|
529
|
+
connecting: "Connecting...",
|
|
530
|
+
connected: "Connected",
|
|
531
|
+
disconnected: "Disconnected",
|
|
532
|
+
reconnecting: "Reconnecting...",
|
|
533
|
+
error: "Connection error"
|
|
534
|
+
},
|
|
535
|
+
conversation: {
|
|
536
|
+
new: "New conversation",
|
|
537
|
+
untitled: "Untitled",
|
|
538
|
+
delete: "Delete",
|
|
539
|
+
deleteConfirm: "Are you sure you want to delete this conversation?"
|
|
540
|
+
}
|
|
541
|
+
},
|
|
542
|
+
zh: {
|
|
543
|
+
hitl: {
|
|
544
|
+
approvalRequired: "\u9700\u8981\u5BA1\u6279",
|
|
545
|
+
inputRequired: "\u9700\u8981\u8F93\u5165",
|
|
546
|
+
tool: "\u5DE5\u5177",
|
|
547
|
+
approve: "\u6279\u51C6",
|
|
548
|
+
reject: "\u62D2\u7EDD",
|
|
549
|
+
submit: "\u63D0\u4EA4",
|
|
550
|
+
cancel: "\u53D6\u6D88"
|
|
551
|
+
},
|
|
552
|
+
input: {
|
|
553
|
+
placeholder: "\u8F93\u5165\u6D88\u606F...",
|
|
554
|
+
send: "\u53D1\u9001",
|
|
555
|
+
stop: "\u505C\u6B62"
|
|
556
|
+
},
|
|
557
|
+
message: {
|
|
558
|
+
thinking: "\u601D\u8003\u4E2D...",
|
|
559
|
+
error: "\u9519\u8BEF",
|
|
560
|
+
loading: "\u52A0\u8F7D\u4E2D..."
|
|
561
|
+
},
|
|
562
|
+
connection: {
|
|
563
|
+
connecting: "\u8FDE\u63A5\u4E2D...",
|
|
564
|
+
connected: "\u5DF2\u8FDE\u63A5",
|
|
565
|
+
disconnected: "\u5DF2\u65AD\u5F00",
|
|
566
|
+
reconnecting: "\u91CD\u65B0\u8FDE\u63A5\u4E2D...",
|
|
567
|
+
error: "\u8FDE\u63A5\u9519\u8BEF"
|
|
568
|
+
},
|
|
569
|
+
conversation: {
|
|
570
|
+
new: "\u65B0\u5BF9\u8BDD",
|
|
571
|
+
untitled: "\u672A\u547D\u540D",
|
|
572
|
+
delete: "\u5220\u9664",
|
|
573
|
+
deleteConfirm: "\u786E\u5B9A\u8981\u5220\u9664\u8FD9\u4E2A\u5BF9\u8BDD\u5417\uFF1F"
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
};
|
|
577
|
+
var I18nContext = (0, import_react3.createContext)(null);
|
|
578
|
+
function I18nProvider({
|
|
579
|
+
children,
|
|
580
|
+
defaultLocale = "en",
|
|
581
|
+
storageKey = "sanqian-chat-locale",
|
|
582
|
+
customTranslations
|
|
583
|
+
}) {
|
|
584
|
+
const [locale, setLocaleState] = (0, import_react3.useState)(() => {
|
|
585
|
+
if (typeof window !== "undefined") {
|
|
586
|
+
const stored = localStorage.getItem(storageKey);
|
|
587
|
+
if (stored === "en" || stored === "zh") {
|
|
588
|
+
return stored;
|
|
589
|
+
}
|
|
590
|
+
const browserLang = navigator.language.toLowerCase();
|
|
591
|
+
if (browserLang.startsWith("zh")) {
|
|
592
|
+
return "zh";
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
return defaultLocale;
|
|
596
|
+
});
|
|
597
|
+
const setLocale = (0, import_react3.useCallback)((newLocale) => {
|
|
598
|
+
setLocaleState(newLocale);
|
|
599
|
+
if (typeof window !== "undefined") {
|
|
600
|
+
localStorage.setItem(storageKey, newLocale);
|
|
601
|
+
}
|
|
602
|
+
}, [storageKey]);
|
|
603
|
+
const t = (0, import_react3.useMemo)(() => {
|
|
604
|
+
const base = translations[locale];
|
|
605
|
+
const custom = customTranslations?.[locale];
|
|
606
|
+
if (!custom) return base;
|
|
607
|
+
return {
|
|
608
|
+
hitl: { ...base.hitl, ...custom.hitl },
|
|
609
|
+
input: { ...base.input, ...custom.input },
|
|
610
|
+
message: { ...base.message, ...custom.message },
|
|
611
|
+
connection: { ...base.connection, ...custom.connection },
|
|
612
|
+
conversation: { ...base.conversation, ...custom.conversation }
|
|
613
|
+
};
|
|
614
|
+
}, [locale, customTranslations]);
|
|
615
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(I18nContext.Provider, { value: { locale, setLocale, t }, children });
|
|
616
|
+
}
|
|
617
|
+
function useI18n() {
|
|
618
|
+
const context = (0, import_react3.useContext)(I18nContext);
|
|
619
|
+
if (!context) {
|
|
620
|
+
throw new Error("useI18n must be used within an I18nProvider");
|
|
621
|
+
}
|
|
622
|
+
return context;
|
|
623
|
+
}
|
|
624
|
+
function useStandaloneI18n(defaultLocale = "en") {
|
|
625
|
+
const [locale, setLocale] = (0, import_react3.useState)(defaultLocale);
|
|
626
|
+
const t = (0, import_react3.useMemo)(() => translations[locale], [locale]);
|
|
627
|
+
return { locale, setLocale, t };
|
|
628
|
+
}
|
|
629
|
+
function getTranslations(locale) {
|
|
630
|
+
return translations[locale];
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// src/renderer/adapters/ipc.ts
|
|
634
|
+
function createIpcAdapter() {
|
|
635
|
+
let connectionStatus = "disconnected";
|
|
636
|
+
const connectionListeners = /* @__PURE__ */ new Set();
|
|
637
|
+
const streamCallbacks = /* @__PURE__ */ new Map();
|
|
638
|
+
let streamEventCleanup = null;
|
|
639
|
+
let currentRunId = null;
|
|
640
|
+
const api = window.sanqianChat;
|
|
641
|
+
if (!api) {
|
|
642
|
+
throw new Error("sanqianChat API not available. Ensure preload script is configured.");
|
|
643
|
+
}
|
|
644
|
+
const updateStatus = (status, error, errorCode) => {
|
|
645
|
+
connectionStatus = status;
|
|
646
|
+
connectionListeners.forEach((cb) => cb(status, error, errorCode));
|
|
647
|
+
};
|
|
648
|
+
streamEventCleanup = api.onStreamEvent((streamId, event) => {
|
|
649
|
+
const callback = streamCallbacks.get(streamId);
|
|
650
|
+
if (callback && isValidStreamEvent(event)) {
|
|
651
|
+
callback(event);
|
|
652
|
+
if (event.type === "done" || event.type === "error") {
|
|
653
|
+
streamCallbacks.delete(streamId);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
});
|
|
657
|
+
return {
|
|
658
|
+
async connect() {
|
|
659
|
+
updateStatus("connecting");
|
|
660
|
+
try {
|
|
661
|
+
const result = await api.connect();
|
|
662
|
+
if (result.success) {
|
|
663
|
+
updateStatus("connected");
|
|
664
|
+
} else {
|
|
665
|
+
throw new Error(result.error || "Connection failed");
|
|
666
|
+
}
|
|
667
|
+
} catch (e) {
|
|
668
|
+
updateStatus("error", e instanceof Error ? e.message : "Connection failed", "CONNECTION_FAILED");
|
|
669
|
+
throw e;
|
|
670
|
+
}
|
|
671
|
+
},
|
|
672
|
+
async disconnect() {
|
|
673
|
+
updateStatus("disconnected");
|
|
674
|
+
},
|
|
675
|
+
isConnected() {
|
|
676
|
+
return connectionStatus === "connected";
|
|
677
|
+
},
|
|
678
|
+
getConnectionStatus() {
|
|
679
|
+
return connectionStatus;
|
|
680
|
+
},
|
|
681
|
+
onConnectionChange(callback) {
|
|
682
|
+
connectionListeners.add(callback);
|
|
683
|
+
callback(connectionStatus);
|
|
684
|
+
return () => connectionListeners.delete(callback);
|
|
685
|
+
},
|
|
686
|
+
async listConversations(options) {
|
|
687
|
+
const result = await api.listConversations(options);
|
|
688
|
+
if (!result.success) throw new Error(result.error || "Failed to list");
|
|
689
|
+
const data = result.data;
|
|
690
|
+
return {
|
|
691
|
+
conversations: data.conversations.map((c) => ({
|
|
692
|
+
id: c.conversation_id,
|
|
693
|
+
title: c.title || "Untitled",
|
|
694
|
+
createdAt: c.created_at || "",
|
|
695
|
+
updatedAt: c.updated_at || "",
|
|
696
|
+
messageCount: c.message_count || 0
|
|
697
|
+
})),
|
|
698
|
+
total: data.total
|
|
699
|
+
};
|
|
700
|
+
},
|
|
701
|
+
async getConversation(id, options) {
|
|
702
|
+
const result = await api.getConversation({ conversationId: id, messageLimit: options?.messageLimit });
|
|
703
|
+
if (!result.success) throw new Error(result.error || "Failed to get");
|
|
704
|
+
const data = result.data;
|
|
705
|
+
return {
|
|
706
|
+
id: data.conversation_id,
|
|
707
|
+
title: data.title || "Untitled",
|
|
708
|
+
createdAt: data.created_at || "",
|
|
709
|
+
updatedAt: data.updated_at || "",
|
|
710
|
+
messageCount: data.message_count || 0,
|
|
711
|
+
messages: (data.messages || []).map((m, i) => ({
|
|
712
|
+
id: `msg-${i}`,
|
|
713
|
+
role: m.role,
|
|
714
|
+
content: m.content,
|
|
715
|
+
timestamp: m.created_at || (/* @__PURE__ */ new Date()).toISOString()
|
|
716
|
+
}))
|
|
717
|
+
};
|
|
718
|
+
},
|
|
719
|
+
async deleteConversation(id) {
|
|
720
|
+
const result = await api.deleteConversation({ conversationId: id });
|
|
721
|
+
if (!result.success) throw new Error(result.error || "Failed to delete");
|
|
722
|
+
},
|
|
723
|
+
async chatStream(messages, conversationId, onEvent) {
|
|
724
|
+
const streamId = crypto.randomUUID();
|
|
725
|
+
streamCallbacks.set(streamId, (event) => {
|
|
726
|
+
if (event.type === "interrupt") {
|
|
727
|
+
currentRunId = event.run_id || null;
|
|
728
|
+
}
|
|
729
|
+
onEvent(event);
|
|
730
|
+
});
|
|
731
|
+
try {
|
|
732
|
+
await api.stream({ streamId, messages, conversationId });
|
|
733
|
+
return {
|
|
734
|
+
cancel: async () => {
|
|
735
|
+
await api.cancelStream({ streamId });
|
|
736
|
+
streamCallbacks.delete(streamId);
|
|
737
|
+
}
|
|
738
|
+
};
|
|
739
|
+
} catch (e) {
|
|
740
|
+
streamCallbacks.delete(streamId);
|
|
741
|
+
throw e;
|
|
742
|
+
}
|
|
743
|
+
},
|
|
744
|
+
sendHitlResponse(response, runId) {
|
|
745
|
+
api.sendHitlResponse({ response, runId: runId || currentRunId || void 0 });
|
|
746
|
+
},
|
|
747
|
+
cleanup() {
|
|
748
|
+
streamEventCleanup?.();
|
|
749
|
+
connectionListeners.clear();
|
|
750
|
+
streamCallbacks.clear();
|
|
751
|
+
}
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
function isValidStreamEvent(event) {
|
|
755
|
+
if (!event || typeof event !== "object") return false;
|
|
756
|
+
const e = event;
|
|
757
|
+
return typeof e.type === "string";
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
// src/core/adapter.ts
|
|
761
|
+
function createSdkAdapter(config) {
|
|
762
|
+
let connectionStatus = "disconnected";
|
|
763
|
+
const connectionListeners = /* @__PURE__ */ new Set();
|
|
764
|
+
let currentRunId = null;
|
|
765
|
+
const updateStatus = (status, error, errorCode) => {
|
|
766
|
+
connectionStatus = status;
|
|
767
|
+
connectionListeners.forEach((cb) => cb(status, error, errorCode));
|
|
768
|
+
};
|
|
769
|
+
return {
|
|
770
|
+
async connect() {
|
|
771
|
+
const sdk = config.getSdk();
|
|
772
|
+
if (!sdk) throw new Error("SDK not available");
|
|
773
|
+
updateStatus("connecting");
|
|
774
|
+
try {
|
|
775
|
+
await sdk.ensureReady();
|
|
776
|
+
updateStatus("connected");
|
|
777
|
+
} catch (e) {
|
|
778
|
+
updateStatus("error", e instanceof Error ? e.message : "Connection failed", "CONNECTION_FAILED");
|
|
779
|
+
throw e;
|
|
780
|
+
}
|
|
781
|
+
},
|
|
782
|
+
async disconnect() {
|
|
783
|
+
updateStatus("disconnected");
|
|
784
|
+
},
|
|
785
|
+
isConnected() {
|
|
786
|
+
const sdk = config.getSdk();
|
|
787
|
+
return sdk?.isConnected() ?? false;
|
|
788
|
+
},
|
|
789
|
+
getConnectionStatus() {
|
|
790
|
+
return connectionStatus;
|
|
791
|
+
},
|
|
792
|
+
onConnectionChange(callback) {
|
|
793
|
+
connectionListeners.add(callback);
|
|
794
|
+
callback(connectionStatus);
|
|
795
|
+
return () => connectionListeners.delete(callback);
|
|
796
|
+
},
|
|
797
|
+
async listConversations(options) {
|
|
798
|
+
const sdk = config.getSdk();
|
|
799
|
+
const agentId = config.getAgentId();
|
|
800
|
+
if (!sdk || !agentId) throw new Error("SDK or agent not ready");
|
|
801
|
+
const result = await sdk.listConversations({ agentId, ...options });
|
|
802
|
+
return {
|
|
803
|
+
conversations: result.conversations.map((c) => ({
|
|
804
|
+
id: c.conversation_id,
|
|
805
|
+
title: c.title || "Untitled",
|
|
806
|
+
createdAt: c.created_at || "",
|
|
807
|
+
updatedAt: c.updated_at || "",
|
|
808
|
+
messageCount: c.message_count
|
|
809
|
+
})),
|
|
810
|
+
total: result.total
|
|
811
|
+
};
|
|
812
|
+
},
|
|
813
|
+
async getConversation(id, options) {
|
|
814
|
+
const sdk = config.getSdk();
|
|
815
|
+
if (!sdk) throw new Error("SDK not ready");
|
|
816
|
+
const detail = await sdk.getConversation(id, { messageLimit: options?.messageLimit });
|
|
817
|
+
return {
|
|
818
|
+
id: detail.conversation_id,
|
|
819
|
+
title: detail.title || "Untitled",
|
|
820
|
+
createdAt: detail.created_at || "",
|
|
821
|
+
updatedAt: detail.updated_at || "",
|
|
822
|
+
messageCount: detail.message_count,
|
|
823
|
+
messages: (detail.messages || []).map((m, i) => ({
|
|
824
|
+
id: `msg-${i}`,
|
|
825
|
+
role: m.role,
|
|
826
|
+
content: m.content,
|
|
827
|
+
timestamp: m.created_at || (/* @__PURE__ */ new Date()).toISOString()
|
|
828
|
+
}))
|
|
829
|
+
};
|
|
830
|
+
},
|
|
831
|
+
async deleteConversation(id) {
|
|
832
|
+
const sdk = config.getSdk();
|
|
833
|
+
if (!sdk) throw new Error("SDK not ready");
|
|
834
|
+
await sdk.deleteConversation(id);
|
|
835
|
+
},
|
|
836
|
+
async chatStream(messages, conversationId, onEvent) {
|
|
837
|
+
const sdk = config.getSdk();
|
|
838
|
+
const agentId = config.getAgentId();
|
|
839
|
+
if (!sdk || !agentId) throw new Error("SDK or agent not ready");
|
|
840
|
+
await sdk.ensureReady();
|
|
841
|
+
const sdkMessages = messages.map((m) => ({ role: m.role, content: m.content }));
|
|
842
|
+
const stream = sdk.chatStream(agentId, sdkMessages, { conversationId });
|
|
843
|
+
const controller = new AbortController();
|
|
844
|
+
const signal = controller.signal;
|
|
845
|
+
(async () => {
|
|
846
|
+
try {
|
|
847
|
+
for await (const event of stream) {
|
|
848
|
+
if (signal.aborted) break;
|
|
849
|
+
switch (event.type) {
|
|
850
|
+
case "text":
|
|
851
|
+
onEvent({ type: "text", content: event.content || "" });
|
|
852
|
+
break;
|
|
853
|
+
case "thinking":
|
|
854
|
+
onEvent({ type: "thinking", content: event.content || "" });
|
|
855
|
+
break;
|
|
856
|
+
case "tool_call":
|
|
857
|
+
if (event.tool_call) {
|
|
858
|
+
onEvent({ type: "tool_call", tool_call: event.tool_call });
|
|
859
|
+
}
|
|
860
|
+
break;
|
|
861
|
+
case "tool_result":
|
|
862
|
+
onEvent({ type: "tool_result", tool_call_id: event.tool_call_id || "", result: event.result });
|
|
863
|
+
break;
|
|
864
|
+
case "done":
|
|
865
|
+
onEvent({ type: "done", conversationId: event.conversationId || "", title: event.title });
|
|
866
|
+
break;
|
|
867
|
+
case "error":
|
|
868
|
+
onEvent({ type: "error", error: event.error || "Unknown error" });
|
|
869
|
+
break;
|
|
870
|
+
default:
|
|
871
|
+
const evt = event;
|
|
872
|
+
if (evt.type === "interrupt") {
|
|
873
|
+
currentRunId = evt.run_id || null;
|
|
874
|
+
onEvent({
|
|
875
|
+
type: "interrupt",
|
|
876
|
+
interrupt_type: evt.interrupt_type || "",
|
|
877
|
+
interrupt_payload: evt.interrupt_payload,
|
|
878
|
+
run_id: evt.run_id
|
|
879
|
+
});
|
|
880
|
+
}
|
|
881
|
+
break;
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
} catch (e) {
|
|
885
|
+
if (!signal.aborted) {
|
|
886
|
+
onEvent({ type: "error", error: e instanceof Error ? e.message : "Stream error" });
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
})();
|
|
890
|
+
return {
|
|
891
|
+
cancel: () => {
|
|
892
|
+
controller.abort();
|
|
893
|
+
}
|
|
894
|
+
};
|
|
895
|
+
},
|
|
896
|
+
sendHitlResponse(response, runId) {
|
|
897
|
+
const sdk = config.getSdk();
|
|
898
|
+
if (!sdk) return;
|
|
899
|
+
const id = runId || currentRunId;
|
|
900
|
+
if (id) {
|
|
901
|
+
sdk.sendHitlResponse(id, response);
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
};
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
// src/renderer/components/MessageList.tsx
|
|
908
|
+
var import_react4 = require("react");
|
|
909
|
+
var import_jsx_runtime3 = require("react/jsx-runtime");
|
|
910
|
+
var SCROLL_THRESHOLD = 100;
|
|
911
|
+
var MessageList = (0, import_react4.memo)(function MessageList2({
|
|
912
|
+
messages,
|
|
913
|
+
className = "",
|
|
914
|
+
renderMessage,
|
|
915
|
+
autoScroll = true,
|
|
916
|
+
scrollBehavior = "smooth"
|
|
917
|
+
}) {
|
|
918
|
+
const containerRef = (0, import_react4.useRef)(null);
|
|
919
|
+
const isNearBottomRef = (0, import_react4.useRef)(true);
|
|
920
|
+
const checkIfNearBottom = (0, import_react4.useCallback)(() => {
|
|
921
|
+
const container = containerRef.current;
|
|
922
|
+
if (!container) return true;
|
|
923
|
+
return container.scrollTop <= SCROLL_THRESHOLD;
|
|
924
|
+
}, []);
|
|
925
|
+
const handleScroll = (0, import_react4.useCallback)(() => {
|
|
926
|
+
isNearBottomRef.current = checkIfNearBottom();
|
|
927
|
+
}, [checkIfNearBottom]);
|
|
928
|
+
const scrollToBottom = (0, import_react4.useCallback)(
|
|
929
|
+
(behavior = scrollBehavior) => {
|
|
930
|
+
containerRef.current?.scrollTo({ top: 0, behavior });
|
|
931
|
+
},
|
|
932
|
+
[scrollBehavior]
|
|
933
|
+
);
|
|
934
|
+
(0, import_react4.useEffect)(() => {
|
|
935
|
+
if (autoScroll && isNearBottomRef.current) {
|
|
936
|
+
scrollToBottom();
|
|
937
|
+
}
|
|
938
|
+
}, [messages, autoScroll, scrollToBottom]);
|
|
939
|
+
(0, import_react4.useEffect)(() => {
|
|
940
|
+
scrollToBottom("instant");
|
|
941
|
+
isNearBottomRef.current = true;
|
|
942
|
+
}, []);
|
|
943
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
944
|
+
"div",
|
|
945
|
+
{
|
|
946
|
+
ref: containerRef,
|
|
947
|
+
className: `${className} flex flex-col-reverse overflow-y-auto`,
|
|
948
|
+
role: "log",
|
|
949
|
+
"aria-live": "polite",
|
|
950
|
+
onScroll: handleScroll,
|
|
951
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "flex flex-col", children: messages.map((message, index) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { children: renderMessage(message, index) }, message.id)) })
|
|
952
|
+
}
|
|
953
|
+
);
|
|
954
|
+
});
|
|
955
|
+
|
|
956
|
+
// src/renderer/components/MessageBubble.tsx
|
|
957
|
+
var import_react5 = require("react");
|
|
958
|
+
var import_jsx_runtime4 = require("react/jsx-runtime");
|
|
959
|
+
var MessageBubble = (0, import_react5.memo)(function MessageBubble2({
|
|
960
|
+
message,
|
|
961
|
+
className = "",
|
|
962
|
+
children,
|
|
963
|
+
renderContent
|
|
964
|
+
}) {
|
|
965
|
+
const isStreaming = message.isStreaming ?? false;
|
|
966
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className, "data-role": message.role, "data-streaming": isStreaming, children: [
|
|
967
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "message-content", children: renderContent ? renderContent(message.content, isStreaming) : /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
|
|
968
|
+
message.content,
|
|
969
|
+
isStreaming && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "streaming-cursor", children: "\u258C" })
|
|
970
|
+
] }) }),
|
|
971
|
+
children
|
|
972
|
+
] });
|
|
973
|
+
});
|
|
974
|
+
|
|
975
|
+
// src/renderer/components/ChatInput.tsx
|
|
976
|
+
var import_react6 = require("react");
|
|
977
|
+
var import_jsx_runtime5 = require("react/jsx-runtime");
|
|
978
|
+
var ChatInput = (0, import_react6.memo)(function ChatInput2({
|
|
979
|
+
onSend,
|
|
980
|
+
onStop,
|
|
981
|
+
placeholder = "Type a message...",
|
|
982
|
+
disabled = false,
|
|
983
|
+
isStreaming = false,
|
|
984
|
+
isLoading = false,
|
|
985
|
+
className = "",
|
|
986
|
+
textareaClassName = "",
|
|
987
|
+
sendButtonClassName = "",
|
|
988
|
+
stopButtonClassName = "",
|
|
989
|
+
sendButtonContent = "Send",
|
|
990
|
+
stopButtonContent = "Stop",
|
|
991
|
+
maxRows = 6,
|
|
992
|
+
autoFocus = false,
|
|
993
|
+
focusRef
|
|
994
|
+
}) {
|
|
995
|
+
const [text, setText] = (0, import_react6.useState)("");
|
|
996
|
+
const textareaRef = (0, import_react6.useRef)(null);
|
|
997
|
+
const canSend = text.trim().length > 0 && !disabled && !isLoading;
|
|
998
|
+
const maxHeight = maxRows * 20;
|
|
999
|
+
(0, import_react6.useEffect)(() => {
|
|
1000
|
+
if (focusRef) {
|
|
1001
|
+
focusRef.current = () => textareaRef.current?.focus();
|
|
1002
|
+
}
|
|
1003
|
+
}, [focusRef]);
|
|
1004
|
+
(0, import_react6.useEffect)(() => {
|
|
1005
|
+
if (autoFocus) {
|
|
1006
|
+
const timer = setTimeout(() => textareaRef.current?.focus(), 100);
|
|
1007
|
+
return () => clearTimeout(timer);
|
|
1008
|
+
}
|
|
1009
|
+
}, [autoFocus]);
|
|
1010
|
+
(0, import_react6.useEffect)(() => {
|
|
1011
|
+
const textarea = textareaRef.current;
|
|
1012
|
+
if (!textarea) return;
|
|
1013
|
+
textarea.style.height = "auto";
|
|
1014
|
+
textarea.style.height = `${Math.min(textarea.scrollHeight, maxHeight)}px`;
|
|
1015
|
+
}, [text, maxHeight]);
|
|
1016
|
+
const handleSubmit = (0, import_react6.useCallback)(
|
|
1017
|
+
(e) => {
|
|
1018
|
+
e?.preventDefault();
|
|
1019
|
+
if (!canSend) return;
|
|
1020
|
+
onSend(text.trim());
|
|
1021
|
+
setText("");
|
|
1022
|
+
if (textareaRef.current) textareaRef.current.style.height = "auto";
|
|
1023
|
+
},
|
|
1024
|
+
[text, canSend, onSend]
|
|
1025
|
+
);
|
|
1026
|
+
const handleKeyDown = (0, import_react6.useCallback)(
|
|
1027
|
+
(e) => {
|
|
1028
|
+
if (e.key === "Enter" && !e.shiftKey && !e.nativeEvent.isComposing) {
|
|
1029
|
+
e.preventDefault();
|
|
1030
|
+
handleSubmit();
|
|
1031
|
+
}
|
|
1032
|
+
},
|
|
1033
|
+
[handleSubmit]
|
|
1034
|
+
);
|
|
1035
|
+
return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("form", { onSubmit: handleSubmit, className, children: [
|
|
1036
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
1037
|
+
"textarea",
|
|
1038
|
+
{
|
|
1039
|
+
ref: textareaRef,
|
|
1040
|
+
value: text,
|
|
1041
|
+
onChange: (e) => setText(e.target.value),
|
|
1042
|
+
onKeyDown: handleKeyDown,
|
|
1043
|
+
placeholder,
|
|
1044
|
+
disabled,
|
|
1045
|
+
rows: 1,
|
|
1046
|
+
className: textareaClassName
|
|
1047
|
+
}
|
|
1048
|
+
),
|
|
1049
|
+
isStreaming ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("button", { type: "button", onClick: onStop, className: stopButtonClassName, children: stopButtonContent }) : /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("button", { type: "submit", disabled: !canSend, className: sendButtonClassName, children: sendButtonContent })
|
|
1050
|
+
] });
|
|
1051
|
+
});
|
|
1052
|
+
|
|
1053
|
+
// src/renderer/components/FloatingChat.tsx
|
|
1054
|
+
var import_react7 = require("react");
|
|
1055
|
+
var import_jsx_runtime6 = require("react/jsx-runtime");
|
|
1056
|
+
var FloatingChat = (0, import_react7.memo)(function FloatingChat2({
|
|
1057
|
+
messages,
|
|
1058
|
+
isLoading,
|
|
1059
|
+
isStreaming,
|
|
1060
|
+
error,
|
|
1061
|
+
pendingInterrupt,
|
|
1062
|
+
onSendMessage,
|
|
1063
|
+
onStopStreaming,
|
|
1064
|
+
onApproveHitl,
|
|
1065
|
+
onRejectHitl,
|
|
1066
|
+
onHide,
|
|
1067
|
+
className = "",
|
|
1068
|
+
placeholder,
|
|
1069
|
+
locale = "en",
|
|
1070
|
+
renderMessage,
|
|
1071
|
+
renderContent,
|
|
1072
|
+
renderHitl,
|
|
1073
|
+
header,
|
|
1074
|
+
footer
|
|
1075
|
+
}) {
|
|
1076
|
+
const t = getTranslations(locale);
|
|
1077
|
+
const inputPlaceholder = placeholder ?? t.input.placeholder;
|
|
1078
|
+
const defaultRenderMessage = (0, import_react7.useCallback)(
|
|
1079
|
+
(message) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
|
1080
|
+
MessageBubble,
|
|
1081
|
+
{
|
|
1082
|
+
message,
|
|
1083
|
+
className: `p-3 my-1 rounded-lg ${message.role === "user" ? "bg-blue-100 dark:bg-blue-900 ml-8" : "bg-gray-100 dark:bg-gray-800 mr-8"}`,
|
|
1084
|
+
renderContent
|
|
1085
|
+
}
|
|
1086
|
+
),
|
|
1087
|
+
[renderContent]
|
|
1088
|
+
);
|
|
1089
|
+
const defaultRenderHitl = (0, import_react7.useCallback)(
|
|
1090
|
+
(interrupt, onApprove, onReject) => /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "p-4 bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg m-2", children: [
|
|
1091
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { className: "font-medium mb-2", children: interrupt.type === "approval_request" ? t.hitl.approvalRequired : t.hitl.inputRequired }),
|
|
1092
|
+
interrupt.tool && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("p", { className: "text-sm mb-2", children: [
|
|
1093
|
+
t.hitl.tool,
|
|
1094
|
+
": ",
|
|
1095
|
+
interrupt.tool
|
|
1096
|
+
] }),
|
|
1097
|
+
interrupt.reason && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { className: "text-sm mb-2", children: interrupt.reason }),
|
|
1098
|
+
interrupt.question && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { className: "text-sm mb-2", children: interrupt.question }),
|
|
1099
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "flex gap-2 mt-3", children: [
|
|
1100
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
|
1101
|
+
"button",
|
|
1102
|
+
{
|
|
1103
|
+
onClick: onApprove,
|
|
1104
|
+
className: "px-3 py-1 bg-green-500 text-white rounded hover:bg-green-600",
|
|
1105
|
+
children: t.hitl.approve
|
|
1106
|
+
}
|
|
1107
|
+
),
|
|
1108
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
|
1109
|
+
"button",
|
|
1110
|
+
{
|
|
1111
|
+
onClick: onReject,
|
|
1112
|
+
className: "px-3 py-1 bg-red-500 text-white rounded hover:bg-red-600",
|
|
1113
|
+
children: t.hitl.reject
|
|
1114
|
+
}
|
|
1115
|
+
)
|
|
1116
|
+
] })
|
|
1117
|
+
] }),
|
|
1118
|
+
[t]
|
|
1119
|
+
);
|
|
1120
|
+
return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: `flex flex-col h-full ${className}`, children: [
|
|
1121
|
+
header && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "flex-shrink-0", children: header }),
|
|
1122
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
|
1123
|
+
MessageList,
|
|
1124
|
+
{
|
|
1125
|
+
messages,
|
|
1126
|
+
className: "flex-1 p-2 min-h-0",
|
|
1127
|
+
renderMessage: renderMessage || defaultRenderMessage
|
|
1128
|
+
}
|
|
1129
|
+
),
|
|
1130
|
+
pendingInterrupt && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "flex-shrink-0", children: (renderHitl || defaultRenderHitl)(
|
|
1131
|
+
pendingInterrupt,
|
|
1132
|
+
() => onApproveHitl?.(),
|
|
1133
|
+
() => onRejectHitl?.()
|
|
1134
|
+
) }),
|
|
1135
|
+
error && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "flex-shrink-0 p-2 text-red-500 text-sm bg-red-50 dark:bg-red-900/20", children: error }),
|
|
1136
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "flex-shrink-0 p-2 border-t dark:border-gray-700", children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
|
1137
|
+
ChatInput,
|
|
1138
|
+
{
|
|
1139
|
+
onSend: onSendMessage,
|
|
1140
|
+
onStop: onStopStreaming,
|
|
1141
|
+
placeholder: inputPlaceholder,
|
|
1142
|
+
isStreaming,
|
|
1143
|
+
isLoading,
|
|
1144
|
+
disabled: !!pendingInterrupt,
|
|
1145
|
+
autoFocus: true,
|
|
1146
|
+
className: "flex gap-2",
|
|
1147
|
+
textareaClassName: "flex-1 resize-none rounded-lg border dark:border-gray-600 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-800",
|
|
1148
|
+
sendButtonClassName: "px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 disabled:opacity-50",
|
|
1149
|
+
stopButtonClassName: "px-4 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600"
|
|
1150
|
+
}
|
|
1151
|
+
) }),
|
|
1152
|
+
footer && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "flex-shrink-0", children: footer })
|
|
1153
|
+
] });
|
|
1154
|
+
});
|
|
1155
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1156
|
+
0 && (module.exports = {
|
|
1157
|
+
ChatInput,
|
|
1158
|
+
FloatingChat,
|
|
1159
|
+
I18nProvider,
|
|
1160
|
+
MessageBubble,
|
|
1161
|
+
MessageList,
|
|
1162
|
+
ThemeProvider,
|
|
1163
|
+
createIpcAdapter,
|
|
1164
|
+
createSdkAdapter,
|
|
1165
|
+
getTranslations,
|
|
1166
|
+
useChat,
|
|
1167
|
+
useI18n,
|
|
1168
|
+
useStandaloneI18n,
|
|
1169
|
+
useStandaloneTheme,
|
|
1170
|
+
useTheme
|
|
1171
|
+
});
|