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