samlax12 commited on
Commit
7be508e
·
verified ·
1 Parent(s): 72332e4

Upload 19 files

Browse files
App.tsx ADDED
@@ -0,0 +1,1060 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useEffect, useRef } from 'react';
2
+ import { ChatMessage, MessageSender, MessagePurpose } from './types';
3
+ import { generateResponse } from './services/openaiService';
4
+ import ChatInput from './components/ChatInput';
5
+ import MessageBubble from './components/MessageBubble';
6
+ import Notepad from './components/Notepad';
7
+ import ModelConfigManager from './components/ModelConfigManager';
8
+ import {
9
+ AiModel,
10
+ AiRole,
11
+ ApiChannel,
12
+ ModelConfigManager as ConfigManager,
13
+ DEFAULT_MANUAL_FIXED_TURNS,
14
+ MIN_MANUAL_FIXED_TURNS,
15
+ MAX_MANUAL_FIXED_TURNS,
16
+ MAX_AI_DRIVEN_DISCUSSION_TURNS_PER_MODEL,
17
+ INITIAL_NOTEPAD_CONTENT,
18
+ NOTEPAD_INSTRUCTION_PROMPT_PART,
19
+ NOTEPAD_UPDATE_TAG_START,
20
+ NOTEPAD_UPDATE_TAG_END,
21
+ DISCUSSION_COMPLETE_TAG,
22
+ AI_DRIVEN_DISCUSSION_INSTRUCTION_PROMPT_PART,
23
+ DiscussionMode
24
+ } from './constants';
25
+ import {
26
+ BotMessageSquare,
27
+ AlertTriangle,
28
+ RefreshCcw,
29
+ SlidersHorizontal,
30
+ Users,
31
+ MessagesSquare,
32
+ Bot,
33
+ ChevronDown,
34
+ Settings,
35
+ Play,
36
+ Pause,
37
+ Square,
38
+ Download
39
+ } from 'lucide-react';
40
+
41
+ interface ParsedAIResponse {
42
+ spokenText: string;
43
+ newNotepadContent: string | null;
44
+ discussionShouldEnd?: boolean;
45
+ }
46
+
47
+ interface ActiveRole extends AiRole {
48
+ model: AiModel;
49
+ channel: ApiChannel;
50
+ isProcessing?: boolean;
51
+ }
52
+
53
+ interface DiscussionState {
54
+ currentRoleIndex: number;
55
+ currentTurn: number;
56
+ discussionLog: string[];
57
+ isFirstMessage: boolean;
58
+ previousAISignaledStop: boolean;
59
+ discussionEndCount: number;
60
+ userQuery: string;
61
+ imageApiPart?: any;
62
+ commonPromptInstructions: string;
63
+ roleOrder: ActiveRole[];
64
+ maxTurnsForLoop: number;
65
+ }
66
+
67
+ const parseAIResponse = (responseText: string): ParsedAIResponse => {
68
+ let currentText = responseText.trim();
69
+ let spokenText = "";
70
+ let newNotepadContent: string | null = null;
71
+ let discussionShouldEnd = false;
72
+
73
+ let notepadActionText = "";
74
+ let discussionActionText = "";
75
+
76
+ const notepadStartIndex = currentText.lastIndexOf(NOTEPAD_UPDATE_TAG_START);
77
+ const notepadEndIndex = currentText.lastIndexOf(NOTEPAD_UPDATE_TAG_END);
78
+
79
+ if (notepadStartIndex !== -1 && notepadEndIndex !== -1 && notepadEndIndex > notepadStartIndex && currentText.endsWith(NOTEPAD_UPDATE_TAG_END)) {
80
+ newNotepadContent = currentText.substring(notepadStartIndex + NOTEPAD_UPDATE_TAG_START.length, notepadEndIndex).trim();
81
+ spokenText = currentText.substring(0, notepadStartIndex).trim();
82
+
83
+ if (newNotepadContent) {
84
+ notepadActionText = "更新了记事本";
85
+ } else {
86
+ notepadActionText = "尝试更新记事本但内容为空";
87
+ }
88
+ } else {
89
+ spokenText = currentText;
90
+ }
91
+
92
+ if (spokenText.includes(DISCUSSION_COMPLETE_TAG)) {
93
+ discussionShouldEnd = true;
94
+ spokenText = spokenText.replace(new RegExp(DISCUSSION_COMPLETE_TAG.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), "").trim();
95
+ discussionActionText = "建议结束讨论";
96
+ }
97
+
98
+ if (!spokenText.trim()) {
99
+ if (notepadActionText && discussionActionText) {
100
+ spokenText = `(AI ${notepadActionText}并${discussionActionText})`;
101
+ } else if (notepadActionText) {
102
+ spokenText = `(AI ${notepadActionText})`;
103
+ } else if (discussionActionText) {
104
+ spokenText = `(AI ${discussionActionText})`;
105
+ } else {
106
+ spokenText = "(AI 未提供额外文本回复)";
107
+ }
108
+ }
109
+
110
+ return { spokenText: spokenText.trim(), newNotepadContent, discussionShouldEnd };
111
+ };
112
+
113
+ const fileToBase64 = (file: File): Promise<string> => {
114
+ return new Promise((resolve, reject) => {
115
+ const reader = new FileReader();
116
+ reader.readAsDataURL(file);
117
+ reader.onload = () => {
118
+ const result = reader.result as string;
119
+ resolve(result.split(',')[1]);
120
+ };
121
+ reader.onerror = (error) => reject(error);
122
+ });
123
+ };
124
+
125
+ const createDynamicMessageSender = (roleName: string): MessageSender => {
126
+ return roleName as MessageSender;
127
+ };
128
+
129
+ const App: React.FC = () => {
130
+ const [messages, setMessages] = useState<ChatMessage[]>([]);
131
+ const [isLoading, setIsLoading] = useState<boolean>(false);
132
+ const [currentTotalProcessingTimeMs, setCurrentTotalProcessingTimeMs] = useState<number>(0);
133
+
134
+ const [notepadContent, setNotepadContent] = useState<string>(INITIAL_NOTEPAD_CONTENT);
135
+ const [lastNotepadUpdateBy, setLastNotepadUpdateBy] = useState<MessageSender | null>(null);
136
+
137
+ const [discussionMode, setDiscussionMode] = useState<DiscussionMode>(DiscussionMode.FixedTurns);
138
+ const [manualFixedTurns, setManualFixedTurns] = useState<number>(DEFAULT_MANUAL_FIXED_TURNS);
139
+ const [isReducedCapacityEnabled, setIsReducedCapacityEnabled] = useState<boolean>(false);
140
+
141
+ const [activeRoles, setActiveRoles] = useState<ActiveRole[]>([]);
142
+ const [channels, setChannels] = useState<ApiChannel[]>([]);
143
+ const [models, setModels] = useState<AiModel[]>([]);
144
+ const [isConfigManagerOpen, setIsConfigManagerOpen] = useState<boolean>(false);
145
+ const [isRoleSelectorOpen, setIsRoleSelectorOpen] = useState<boolean>(false);
146
+
147
+ const [isDiscussionActive, setIsDiscussionActive] = useState<boolean>(false);
148
+ const [streamingMessages, setStreamingMessages] = useState<Map<string, { text: string; isComplete: boolean }>>(new Map());
149
+ const [currentDiscussion, setCurrentDiscussion] = useState<DiscussionState | null>(null);
150
+
151
+ const chatContainerRef = useRef<HTMLDivElement>(null);
152
+ const currentQueryStartTimeRef = useRef<number | null>(null);
153
+ const cancelRequestRef = useRef<boolean>(false);
154
+
155
+ // 实时流式消息管理
156
+ const createStreamingMessage = (sender: MessageSender, purpose: MessagePurpose): string => {
157
+ const messageId = Date.now().toString() + Math.random().toString(36).substr(2, 9);
158
+ const message: ChatMessage = {
159
+ id: messageId,
160
+ text: '',
161
+ sender,
162
+ purpose,
163
+ timestamp: new Date()
164
+ };
165
+
166
+ setMessages(prev => [...prev, message]);
167
+ setStreamingMessages(prev => new Map(prev.set(messageId, { text: '', isComplete: false })));
168
+ return messageId;
169
+ };
170
+
171
+ const updateStreamingMessage = (messageId: string, fullText: string, isComplete: boolean, durationMs?: number) => {
172
+ setMessages(prev => prev.map(msg =>
173
+ msg.id === messageId ? {
174
+ ...msg,
175
+ text: fullText,
176
+ durationMs: isComplete ? durationMs : msg.durationMs
177
+ } : msg
178
+ ));
179
+ };
180
+
181
+ const loadConfiguration = () => {
182
+ const allChannels = ConfigManager.getChannels();
183
+ const allModels = ConfigManager.getModels();
184
+ const allRoles = ConfigManager.getActiveRoles();
185
+
186
+ setChannels(allChannels);
187
+ setModels(allModels);
188
+
189
+ const rolesWithModelsAndChannels: ActiveRole[] = allRoles.map(role => {
190
+ const model = allModels.find(m => m.id === role.modelId);
191
+ if (!model) {
192
+ console.warn(`Role ${role.name} references non-existent model ${role.modelId}`);
193
+ return null;
194
+ }
195
+
196
+ const channel = allChannels.find(ch => ch.id === model.channelId);
197
+ if (!channel) {
198
+ console.warn(`Model ${model.name} references non-existent channel ${model.channelId}`);
199
+ return null;
200
+ }
201
+
202
+ return { ...role, model, channel };
203
+ }).filter(Boolean) as ActiveRole[];
204
+
205
+ setActiveRoles(rolesWithModelsAndChannels);
206
+ };
207
+
208
+ useEffect(() => {
209
+ loadConfiguration();
210
+ }, []);
211
+
212
+ const addMessage = (
213
+ text: string,
214
+ sender: MessageSender,
215
+ purpose: MessagePurpose,
216
+ durationMs?: number,
217
+ image?: ChatMessage['image']
218
+ ) => {
219
+ const messageId = Date.now().toString() + Math.random().toString(36).substr(2, 9);
220
+ const message: ChatMessage = {
221
+ id: messageId,
222
+ text,
223
+ sender,
224
+ purpose,
225
+ timestamp: new Date(),
226
+ durationMs,
227
+ image
228
+ };
229
+
230
+ setMessages(prev => [...prev, message]);
231
+ return messageId;
232
+ };
233
+
234
+ const interruptDiscussion = () => {
235
+ if (isLoading && isDiscussionActive) {
236
+ cancelRequestRef.current = true;
237
+ setIsLoading(false);
238
+ setIsDiscussionActive(false);
239
+ setCurrentDiscussion(null);
240
+
241
+ if (currentTotalProcessingTimeMs > 0) {
242
+ addMessage(
243
+ `讨论已被用户中断 (已进行 ${(currentTotalProcessingTimeMs / 1000).toFixed(2)}秒)`,
244
+ MessageSender.System,
245
+ MessagePurpose.SystemNotification
246
+ );
247
+ }
248
+
249
+ setCurrentTotalProcessingTimeMs(0);
250
+ if (currentQueryStartTimeRef.current) {
251
+ currentQueryStartTimeRef.current = null;
252
+ }
253
+ }
254
+ };
255
+
256
+ const exportDiscussionRecord = () => {
257
+ if (messages.length === 0) {
258
+ addMessage('当前没有可导出的消息记录', MessageSender.System, MessagePurpose.SystemNotification);
259
+ return;
260
+ }
261
+
262
+ // 生成简洁的文本格式导出
263
+ let exportText = `=== Multi-Mind Chat 对话记录 ===\n`;
264
+ exportText += `导出时间: ${new Date().toLocaleString()}\n`;
265
+ exportText += `消息总数: ${messages.length}\n\n`;
266
+
267
+ messages.forEach(msg => {
268
+ if (msg.purpose !== MessagePurpose.SystemNotification) {
269
+ const timeStr = msg.timestamp.toLocaleTimeString();
270
+ const durationStr = msg.durationMs ? ` (${(msg.durationMs / 1000).toFixed(2)}s)` : '';
271
+ exportText += `[${timeStr}] ${msg.sender}${durationStr}: ${msg.text}\n\n`;
272
+
273
+ if (msg.image) {
274
+ exportText += ` [附件: ${msg.image.name} - ${msg.image.type}]\n\n`;
275
+ }
276
+ }
277
+ });
278
+
279
+ if (notepadContent !== INITIAL_NOTEPAD_CONTENT) {
280
+ exportText += `=== 最终记事本内容 ===\n`;
281
+ exportText += `${notepadContent}\n\n`;
282
+ }
283
+
284
+ const blob = new Blob([exportText], { type: 'text/plain;charset=utf-8' });
285
+ const url = URL.createObjectURL(blob);
286
+ const a = document.createElement('a');
287
+ a.href = url;
288
+ a.download = `对话记录-${new Date().toISOString().split('T')[0]}-${Date.now()}.txt`;
289
+ document.body.appendChild(a);
290
+ a.click();
291
+ document.body.removeChild(a);
292
+ URL.revokeObjectURL(url);
293
+
294
+ addMessage('对话记录已导出', MessageSender.System, MessagePurpose.SystemNotification);
295
+ };
296
+
297
+ const getWelcomeMessageText = () => {
298
+ const modeDescription = discussionMode === DiscussionMode.FixedTurns
299
+ ? `固定轮次对话 (${manualFixedTurns}轮)`
300
+ : "AI驱动对话";
301
+
302
+ const roleNames = activeRoles.map(role => role.name).join(' 和 ');
303
+ const roleCount = activeRoles.length;
304
+ const channelCount = channels.length;
305
+
306
+ if (channelCount === 0) {
307
+ return `欢迎使用Multi-Mind Chat 智囊团!请先配置API渠道。点击设置按钮开始配置。`;
308
+ } else if (roleCount === 0) {
309
+ return `欢迎使用Multi-Mind Chat 智囊团!已配置 ${channelCount} 个API渠道,请继续配置AI角色和模型。点击设置按钮开始配置。`;
310
+ } else if (roleCount === 1) {
311
+ return `欢迎使用Multi-Mind Chat 智囊团!当前模式: ${modeDescription}。当前只有一个活跃角色: ${roleNames}。建议添加更多角色以获得更好的协作体验。`;
312
+ } else {
313
+ return `欢迎使用Multi-Mind Chat 智囊团!当前模式: ${modeDescription}。活跃的AI角色: ${roleNames}。这些角色将协作讨论您的问题并使用共享记事本。`;
314
+ }
315
+ };
316
+
317
+ const initializeChat = () => {
318
+ setMessages([]);
319
+ setNotepadContent(INITIAL_NOTEPAD_CONTENT);
320
+ setLastNotepadUpdateBy(null);
321
+ setIsDiscussionActive(false);
322
+ setStreamingMessages(new Map());
323
+ setCurrentDiscussion(null);
324
+
325
+ addMessage(
326
+ getWelcomeMessageText(),
327
+ MessageSender.System,
328
+ MessagePurpose.SystemNotification
329
+ );
330
+ };
331
+
332
+ useEffect(() => {
333
+ initializeChat();
334
+ }, [activeRoles, discussionMode, manualFixedTurns, channels]);
335
+
336
+ useEffect(() => {
337
+ if (chatContainerRef.current) {
338
+ chatContainerRef.current.scrollTop = chatContainerRef.current.scrollHeight;
339
+ }
340
+ }, [messages]);
341
+
342
+ useEffect(() => {
343
+ let intervalId: number | undefined;
344
+ if (isLoading && currentQueryStartTimeRef.current) {
345
+ intervalId = window.setInterval(() => {
346
+ if (currentQueryStartTimeRef.current) {
347
+ setCurrentTotalProcessingTimeMs(performance.now() - currentQueryStartTimeRef.current);
348
+ }
349
+ }, 100);
350
+ } else {
351
+ if (intervalId) clearInterval(intervalId);
352
+ }
353
+ return () => {
354
+ if (intervalId) clearInterval(intervalId);
355
+ };
356
+ }, [isLoading]);
357
+
358
+ const handleClearChat = () => {
359
+ if (isLoading) {
360
+ cancelRequestRef.current = true;
361
+ }
362
+ setIsLoading(false);
363
+ setIsDiscussionActive(false);
364
+ setCurrentDiscussion(null);
365
+
366
+ setCurrentTotalProcessingTimeMs(0);
367
+ if (currentQueryStartTimeRef.current) {
368
+ currentQueryStartTimeRef.current = null;
369
+ }
370
+
371
+ setMessages([]);
372
+ setNotepadContent(INITIAL_NOTEPAD_CONTENT);
373
+ setLastNotepadUpdateBy(null);
374
+ setStreamingMessages(new Map());
375
+
376
+ addMessage(
377
+ getWelcomeMessageText(),
378
+ MessageSender.System,
379
+ MessagePurpose.SystemNotification
380
+ );
381
+ };
382
+
383
+ const handleManualFixedTurnsChange = (e: React.ChangeEvent<HTMLInputElement>) => {
384
+ let value = parseInt(e.target.value, 10);
385
+ if (isNaN(value)) {
386
+ value = DEFAULT_MANUAL_FIXED_TURNS;
387
+ }
388
+ value = Math.max(MIN_MANUAL_FIXED_TURNS, Math.min(MAX_MANUAL_FIXED_TURNS, value));
389
+ setManualFixedTurns(value);
390
+ };
391
+
392
+ const toggleRoleActiveState = (roleId: string) => {
393
+ const role = activeRoles.find(r => r.id === roleId);
394
+ if (role) {
395
+ ConfigManager.updateRole(roleId, { isActive: !role.isActive });
396
+ loadConfiguration();
397
+ }
398
+ };
399
+
400
+ const processNextRole = (state: DiscussionState) => {
401
+ if (cancelRequestRef.current) {
402
+ setIsDiscussionActive(false);
403
+ setCurrentDiscussion(null);
404
+ return;
405
+ }
406
+
407
+ // 检查是否完成所有讨论
408
+ if (state.currentTurn === 0 && state.currentRoleIndex >= state.roleOrder.length) {
409
+ // 第一轮结束,开始多轮讨论
410
+ if (!state.previousAISignaledStop && state.maxTurnsForLoop > 0) {
411
+ const newState = { ...state, currentTurn: 1, currentRoleIndex: 0, discussionEndCount: 0 };
412
+ setCurrentDiscussion(newState);
413
+ processNextRole(newState);
414
+ return;
415
+ } else {
416
+ processFinalAnswer(state);
417
+ return;
418
+ }
419
+ } else if (state.currentTurn > 0 &&
420
+ (state.currentTurn >= state.maxTurnsForLoop ||
421
+ state.previousAISignaledStop ||
422
+ state.currentRoleIndex >= state.roleOrder.length)) {
423
+ processFinalAnswer(state);
424
+ return;
425
+ }
426
+
427
+ const currentRole = state.roleOrder[state.currentRoleIndex];
428
+ const shouldUseReducedCapacity = isReducedCapacityEnabled && currentRole.model.supportsReducedCapacity;
429
+
430
+ // 显示系统通知
431
+ addMessage(
432
+ `${currentRole.name} 正在${state.currentTurn === 0 ? '分析问题并提供观点' : '回应其他角色的观点'} (使用 ${currentRole.model.name} - ${currentRole.channel.name})...`,
433
+ MessageSender.System,
434
+ MessagePurpose.SystemNotification
435
+ );
436
+
437
+ // 立即创建消息气泡
438
+ const purpose = state.currentRoleIndex % 2 === 0 ? MessagePurpose.CognitoToMuse : MessagePurpose.MuseToCognito;
439
+ const messageId = Date.now().toString() + Math.random().toString(36).substr(2, 9);
440
+
441
+ // 立即添加空消息到界面,用户会看到正在输入的效果
442
+ const initialMessage: ChatMessage = {
443
+ id: messageId,
444
+ text: '', // 开始时为空,等待流式输入
445
+ sender: createDynamicMessageSender(currentRole.name),
446
+ purpose,
447
+ timestamp: new Date()
448
+ };
449
+
450
+ setMessages(prev => [...prev, initialMessage]);
451
+
452
+ // 构建提示词
453
+ let prompt: string;
454
+ if (state.isFirstMessage) {
455
+ prompt = `用户的查询 (中文) 是: "${state.userQuery}". ${state.imageApiPart ? "用户还提供了一张图片。请在您的分析和回复中同时考虑此图片和文本查询。" : ""} 请针对此查询提供您的初步想法或分析。这是一个多AI协作的环境,其他AI角色稍后会对您的观点进行回应和讨论。用中文回答。\n${state.commonPromptInstructions}`;
456
+ } else if (state.currentTurn === 0) {
457
+ const otherRoles = state.roleOrder.filter(r => r.id !== currentRole.id).map(r => r.name).join(' 和 ');
458
+ prompt = `用户的查询 (中文) 是: "${state.userQuery}". ${state.imageApiPart ? "用户还提供了一张图片。请在您的分析和回复中同时考虑此图片和文本查询。" : ""} 当前讨论 (均为中文):\n${state.discussionLog.join("\n")}\n您正在与 ${otherRoles} 协作讨论这个问题。请提供您的观点和分析。用中文回答。\n${state.commonPromptInstructions}`;
459
+ } else {
460
+ const otherRoles = state.roleOrder.filter(r => r.id !== currentRole.id).map(r => r.name).join(' 和 ');
461
+ prompt = `用户的查询 (中文) 是: "${state.userQuery}". ${state.imageApiPart ? "用户还提供了一张图片。请在您的分析和回复中同时考虑此图片和文本查询。" : ""} 当前讨论 (均为中文):\n${state.discussionLog.join("\n")}\n您正在与 ${otherRoles} 协作讨论。请对前面的讨论进行回应,提供您的进一步见解或不同观点。保持简洁并使用中文。\n${NOTEPAD_INSTRUCTION_PROMPT_PART.replace('{notepadContent}', notepadContent)}`;
462
+
463
+ if (discussionMode === DiscussionMode.AiDriven && state.previousAISignaledStop) {
464
+ prompt += `\n注意:之前有AI角色建议结束讨论。如果您同意,请在回复中包含 ${DISCUSSION_COMPLETE_TAG}。否则,请继续讨论。`;
465
+ } else if (discussionMode === DiscussionMode.AiDriven) {
466
+ prompt += AI_DRIVEN_DISCUSSION_INSTRUCTION_PROMPT_PART;
467
+ }
468
+ }
469
+
470
+ // 用于后台累积完整响应文本的变量
471
+ let accumulatedText = '';
472
+
473
+ // 开始流式API调用
474
+ generateResponse(
475
+ prompt,
476
+ currentRole.model.apiName,
477
+ currentRole.systemPrompt,
478
+ shouldUseReducedCapacity,
479
+ state.imageApiPart,
480
+ currentRole.channel.baseUrl,
481
+ currentRole.channel.apiKey,
482
+ // 关键的流式回调函数
483
+ (newChunk: string, fullText: string, isComplete: boolean) => {
484
+ // newChunk: 本次新接收到的文本块
485
+ // fullText: API累积的完整文本(用于后续处理)
486
+ // isComplete: 是否完成
487
+
488
+ // 更新后台累积的完整文本
489
+ accumulatedText = fullText;
490
+
491
+ // 实时更新界面显示 - 关键是这里直接使用 fullText 进行显示
492
+ setMessages(prev => prev.map(msg =>
493
+ msg.id === messageId ? {
494
+ ...msg,
495
+ text: fullText, // 直接显示累积的完整文本,实现打字机效果
496
+ durationMs: isComplete ? (performance.now() - (currentQueryStartTimeRef.current || 0)) : undefined
497
+ } : msg
498
+ ));
499
+ }
500
+ ).then(response => {
501
+ if (cancelRequestRef.current) {
502
+ setIsDiscussionActive(false);
503
+ setCurrentDiscussion(null);
504
+ return;
505
+ }
506
+
507
+ if (response.error) {
508
+ if (response.error.includes("API key not valid") || response.error.includes("401")) {
509
+ setMessages(prev => prev.map(msg =>
510
+ msg.id === messageId ? {
511
+ ...msg,
512
+ text: `API密钥无效 (渠道: ${currentRole.channel.name}),请在配置界面中检查密钥设置。`,
513
+ durationMs: response.durationMs
514
+ } : msg
515
+ ));
516
+ setIsLoading(false);
517
+ setIsDiscussionActive(false);
518
+ setCurrentDiscussion(null);
519
+ return;
520
+ }
521
+ throw new Error(`${currentRole.name}: ${response.text}`);
522
+ }
523
+
524
+ // 确保最终消息内容正确
525
+ setMessages(prev => prev.map(msg =>
526
+ msg.id === messageId ? {
527
+ ...msg,
528
+ text: response.text,
529
+ durationMs: response.durationMs
530
+ } : msg
531
+ ));
532
+
533
+ // 解析响应
534
+ const parsedResponse = parseAIResponse(response.text);
535
+ if (parsedResponse.newNotepadContent !== null) {
536
+ setNotepadContent(parsedResponse.newNotepadContent);
537
+ setLastNotepadUpdateBy(createDynamicMessageSender(currentRole.name));
538
+ }
539
+
540
+ // 更新讨论日志 - 使用解析后的文本
541
+ const newDiscussionLog = [...state.discussionLog, `${currentRole.name}: ${parsedResponse.spokenText}`];
542
+
543
+ // 更新状态
544
+ let newState = {
545
+ ...state,
546
+ discussionLog: newDiscussionLog,
547
+ isFirstMessage: false,
548
+ currentRoleIndex: state.currentRoleIndex + 1
549
+ };
550
+
551
+ // 处理AI驱动模式的结束信号
552
+ if (discussionMode === DiscussionMode.AiDriven && parsedResponse.discussionShouldEnd) {
553
+ if (state.currentTurn > 0) {
554
+ newState.discussionEndCount++;
555
+ if (state.previousAISignaledStop || newState.discussionEndCount >= Math.ceil(state.roleOrder.length / 2)) {
556
+ addMessage(`多数AI角色已同意结束讨论。`, MessageSender.System, MessagePurpose.SystemNotification);
557
+ newState.previousAISignaledStop = true;
558
+ } else {
559
+ addMessage(`${currentRole.name} 已建议结束讨论。`, MessageSender.System, MessagePurpose.SystemNotification);
560
+ }
561
+ } else {
562
+ newState.previousAISignaledStop = true;
563
+ addMessage(`${currentRole.name} 已建议结束讨论。`, MessageSender.System, MessagePurpose.SystemNotification);
564
+ }
565
+ }
566
+
567
+ setCurrentDiscussion(newState);
568
+
569
+ // 继续处理下一个角色
570
+ setTimeout(() => processNextRole(newState), 100);
571
+ }).catch(error => {
572
+ console.error("处理AI响应时出错:", error);
573
+ addMessage(`错误: ${error instanceof Error ? error.message : "处理响应时发生未知错误"}`, MessageSender.System, MessagePurpose.SystemNotification);
574
+ setIsLoading(false);
575
+ setIsDiscussionActive(false);
576
+ setCurrentDiscussion(null);
577
+ });
578
+ };
579
+
580
+ const processFinalAnswer = (state: DiscussionState) => {
581
+ const finalAnswerRole = state.roleOrder[0];
582
+ const shouldUseReducedCapacity = isReducedCapacityEnabled && finalAnswerRole.model.supportsReducedCapacity;
583
+
584
+ addMessage(
585
+ `${finalAnswerRole.name} 正在综合所有讨论内容,准备最终答案 (使用 ${finalAnswerRole.model.name} - ${finalAnswerRole.channel.name})...`,
586
+ MessageSender.System,
587
+ MessagePurpose.SystemNotification
588
+ );
589
+
590
+ // 立即创建最终答案消息气泡
591
+ const finalMessageId = Date.now().toString() + Math.random().toString(36).substr(2, 9);
592
+ const finalMessage: ChatMessage = {
593
+ id: finalMessageId,
594
+ text: '', // 开始时为空
595
+ sender: createDynamicMessageSender(finalAnswerRole.name),
596
+ purpose: MessagePurpose.FinalResponse,
597
+ timestamp: new Date()
598
+ };
599
+
600
+ setMessages(prev => [...prev, finalMessage]);
601
+
602
+ const finalPrompt = `用户最初的查询 (中文) 是: "${state.userQuery}". ${state.imageApiPart ? "用户还提供了一张图片。请在您的分析和回复中同时考虑此图片和文本查询。" : ""} 您和其他AI角色进行了以下讨论 (均为中文):\n${state.discussionLog.join("\n")}\n基于整个协作讨论过程和共享记事本的最终状态,请综合所有关键观点,为用户提供一个全面、有用的最终答案。直接回复用户,确保答案结构良好,易于理解,并使用中文。如果相关,您可以在答案中引用记事本内容。如果需要,您也可以最后一次更新记事本。\n${NOTEPAD_INSTRUCTION_PROMPT_PART.replace('{notepadContent}', notepadContent)}`;
603
+
604
+ generateResponse(
605
+ finalPrompt,
606
+ finalAnswerRole.model.apiName,
607
+ finalAnswerRole.systemPrompt,
608
+ shouldUseReducedCapacity,
609
+ state.imageApiPart,
610
+ finalAnswerRole.channel.baseUrl,
611
+ finalAnswerRole.channel.apiKey,
612
+ // 最终答案的流式回调
613
+ (newChunk: string, fullText: string, isComplete: boolean) => {
614
+ setMessages(prev => prev.map(msg =>
615
+ msg.id === finalMessageId ? {
616
+ ...msg,
617
+ text: fullText, // 实时显示累积文本
618
+ durationMs: isComplete ? (performance.now() - (currentQueryStartTimeRef.current || 0)) : undefined
619
+ } : msg
620
+ ));
621
+ }
622
+ ).then(finalResponse => {
623
+ if (cancelRequestRef.current) {
624
+ setIsDiscussionActive(false);
625
+ setCurrentDiscussion(null);
626
+ return;
627
+ }
628
+
629
+ if (finalResponse.error) {
630
+ if (finalResponse.error.includes("API key not valid") || finalResponse.error.includes("401")) {
631
+ setMessages(prev => prev.map(msg =>
632
+ msg.id === finalMessageId ? {
633
+ ...msg,
634
+ text: `API密钥无效 (渠道: ${finalAnswerRole.channel.name}),请在配置界面中检查密钥设置。`,
635
+ durationMs: finalResponse.durationMs
636
+ } : msg
637
+ ));
638
+ setIsLoading(false);
639
+ setIsDiscussionActive(false);
640
+ setCurrentDiscussion(null);
641
+ return;
642
+ }
643
+ throw new Error(`${finalAnswerRole.name}: ${finalResponse.text}`);
644
+ }
645
+
646
+ setMessages(prev => prev.map(msg =>
647
+ msg.id === finalMessageId ? {
648
+ ...msg,
649
+ text: finalResponse.text,
650
+ durationMs: finalResponse.durationMs
651
+ } : msg
652
+ ));
653
+
654
+ const finalParsedResponse = parseAIResponse(finalResponse.text);
655
+ if (finalParsedResponse.newNotepadContent !== null) {
656
+ setNotepadContent(finalParsedResponse.newNotepadContent);
657
+ setLastNotepadUpdateBy(createDynamicMessageSender(finalAnswerRole.name));
658
+ }
659
+
660
+ // 讨论完成
661
+ setIsLoading(false);
662
+ setIsDiscussionActive(false);
663
+ setCurrentDiscussion(null);
664
+ currentQueryStartTimeRef.current = null;
665
+ }).catch(error => {
666
+ console.error("生成最终答案时出错:", error);
667
+ addMessage(`错误: ${error instanceof Error ? error.message : "生成最终答案时发生未知错误"}`, MessageSender.System, MessagePurpose.SystemNotification);
668
+ setIsLoading(false);
669
+ setIsDiscussionActive(false);
670
+ setCurrentDiscussion(null);
671
+ });
672
+ };
673
+
674
+ const handleSendMessage = async (userInput: string, imageFile?: File | null) => {
675
+ if (isLoading) return;
676
+ if (!userInput.trim() && !imageFile) return;
677
+
678
+ if (channels.length === 0) {
679
+ addMessage("请先配置API渠道。点击设置按钮添加渠道。", MessageSender.System, MessagePurpose.SystemNotification);
680
+ return;
681
+ }
682
+
683
+ if (activeRoles.length === 0) {
684
+ addMessage("请先配置AI角色。点击设置按钮添加角色。", MessageSender.System, MessagePurpose.SystemNotification);
685
+ return;
686
+ }
687
+
688
+ const rolesWithoutApiKey = activeRoles.filter(role => !role.channel.apiKey?.trim());
689
+ if (rolesWithoutApiKey.length > 0) {
690
+ const roleNames = rolesWithoutApiKey.map(role => `${role.name}(${role.channel.name})`).join('、');
691
+ addMessage(`以下角色的API渠道缺少API密钥: ${roleNames}。请在配置界面中设置相应的API密钥。`, MessageSender.System, MessagePurpose.SystemNotification);
692
+ return;
693
+ }
694
+
695
+ if (imageFile) {
696
+ const supportsImages = activeRoles.some(role => role.model.supportsImages);
697
+ if (!supportsImages) {
698
+ addMessage("当前活跃的角色都不支持图片处理。请添加支持图片的模型和角色,或移除图片。", MessageSender.System, MessagePurpose.SystemNotification);
699
+ return;
700
+ }
701
+ }
702
+
703
+ setIsDiscussionActive(true);
704
+ cancelRequestRef.current = false;
705
+ setIsLoading(true);
706
+ currentQueryStartTimeRef.current = performance.now();
707
+ setCurrentTotalProcessingTimeMs(0);
708
+
709
+ let userImageForDisplay: ChatMessage['image'] | undefined = undefined;
710
+ if (imageFile) {
711
+ const dataUrl = URL.createObjectURL(imageFile);
712
+ userImageForDisplay = { dataUrl, name: imageFile.name, type: imageFile.type };
713
+ }
714
+
715
+ addMessage(userInput, MessageSender.User, MessagePurpose.UserInput, undefined, userImageForDisplay);
716
+
717
+ let imageApiPart: { inlineData: { mimeType: string; data: string } } | undefined = undefined;
718
+ if (imageFile) {
719
+ try {
720
+ const base64Data = await fileToBase64(imageFile);
721
+ imageApiPart = {
722
+ inlineData: {
723
+ mimeType: imageFile.type,
724
+ data: base64Data,
725
+ },
726
+ };
727
+ } catch (error) {
728
+ console.error("Error converting file to base64:", error);
729
+ addMessage("图片处理失败,请重试。", MessageSender.System, MessagePurpose.SystemNotification);
730
+ setIsLoading(false);
731
+ setIsDiscussionActive(false);
732
+ return;
733
+ }
734
+ }
735
+
736
+ const discussionModeInstruction = discussionMode === DiscussionMode.AiDriven ? AI_DRIVEN_DISCUSSION_INSTRUCTION_PROMPT_PART : "";
737
+ const commonPromptInstructions = NOTEPAD_INSTRUCTION_PROMPT_PART.replace('{notepadContent}', notepadContent) + discussionModeInstruction;
738
+
739
+ const roleOrder = [...activeRoles];
740
+ const maxTurnsForLoop = discussionMode === DiscussionMode.AiDriven ? MAX_AI_DRIVEN_DISCUSSION_TURNS_PER_MODEL : manualFixedTurns;
741
+
742
+ // 初始化讨论状态
743
+ const discussionState: DiscussionState = {
744
+ currentRoleIndex: 0,
745
+ currentTurn: 0,
746
+ discussionLog: [],
747
+ isFirstMessage: true,
748
+ previousAISignaledStop: false,
749
+ discussionEndCount: 0,
750
+ userQuery: userInput,
751
+ imageApiPart,
752
+ commonPromptInstructions,
753
+ roleOrder,
754
+ maxTurnsForLoop
755
+ };
756
+
757
+ setCurrentDiscussion(discussionState);
758
+
759
+ // 开始第一个AI的回复(不等待)
760
+ processNextRole(discussionState);
761
+
762
+ // 清理图片URL
763
+ if (userImageForDisplay?.dataUrl.startsWith('blob:')) {
764
+ // 延迟清理,确保消息已渲染
765
+ setTimeout(() => {
766
+ URL.revokeObjectURL(userImageForDisplay.dataUrl);
767
+ }, 5000);
768
+ }
769
+ };
770
+
771
+ const Separator = () => <div className="h-6 w-px bg-gray-600" aria-hidden="true"></div>;
772
+
773
+ const hasValidChannels = channels.some(ch => ch.apiKey?.trim());
774
+ const isSystemReady = hasValidChannels && activeRoles.length > 0;
775
+
776
+ return (
777
+ <div className="flex flex-col h-screen max-w-7xl mx-auto bg-gray-900 shadow-2xl rounded-lg overflow-hidden">
778
+ <header className="p-4 bg-gray-900 border-b border-gray-700 flex items-center justify-between shrink-0 space-x-2 md:space-x-4 flex-wrap">
779
+ <div className="flex items-center shrink-0">
780
+ <BotMessageSquare size={28} className="mr-2 md:mr-3 text-sky-400" />
781
+ <h1 className="text-xl md:text-2xl font-semibold text-sky-400">Multi-Mind Chat 智囊团</h1>
782
+ </div>
783
+
784
+ <div className="flex items-center space-x-2 md:space-x-3 flex-wrap justify-end gap-y-2">
785
+ {/* 讨论控制按钮 */}
786
+ {isDiscussionActive && (
787
+ <div className="flex items-center space-x-2">
788
+ <button
789
+ onClick={interruptDiscussion}
790
+ className="px-3 py-1 bg-red-600 hover:bg-red-700 text-white rounded text-sm flex items-center space-x-1"
791
+ title="中断当前讨论"
792
+ >
793
+ <Square size={16} />
794
+ <span>中断讨论</span>
795
+ </button>
796
+ <Separator />
797
+ </div>
798
+ )}
799
+
800
+ {/* 导出按钮 - 有消息时始终可用 */}
801
+ {messages.length > 1 && (
802
+ <div className="flex items-center space-x-2">
803
+ <button
804
+ onClick={exportDiscussionRecord}
805
+ className="px-3 py-1 bg-green-600 hover:bg-green-700 text-white rounded text-sm flex items-center space-x-1"
806
+ title="导出对话记录"
807
+ disabled={isLoading}
808
+ >
809
+ <Download size={16} />
810
+ <span>导出记录</span>
811
+ </button>
812
+ <Separator />
813
+ </div>
814
+ )}
815
+
816
+ {/* 角色管理器 */}
817
+ <div className="relative flex items-center">
818
+ <label className="text-sm text-gray-300 mr-1.5 flex items-center shrink-0">
819
+ <Users size={18} className="mr-1 text-sky-400"/>
820
+ 角色:
821
+ </label>
822
+ <button
823
+ onClick={() => setIsRoleSelectorOpen(!isRoleSelectorOpen)}
824
+ className="bg-gray-700 border border-gray-600 text-white text-sm rounded-md p-1.5 focus:ring-2 focus:ring-sky-500 focus:border-sky-500 outline-none flex items-center space-x-2 min-w-[120px]"
825
+ aria-label="管理AI角色"
826
+ >
827
+ <span className="truncate">{activeRoles.length}个活跃</span>
828
+ <ChevronDown size={16} className={`transition-transform ${isRoleSelectorOpen ? 'rotate-180' : ''}`} />
829
+ </button>
830
+
831
+ {isRoleSelectorOpen && (
832
+ <div className="absolute top-full left-0 mt-1 w-80 bg-gray-800 border border-gray-600 rounded-md shadow-lg z-50 max-h-96 overflow-y-auto">
833
+ <div className="p-3 border-b border-gray-700">
834
+ <div className="flex justify-between items-center">
835
+ <h3 className="text-white font-medium">活跃角色</h3>
836
+ <button
837
+ onClick={() => {
838
+ setIsConfigManagerOpen(true);
839
+ setIsRoleSelectorOpen(false);
840
+ }}
841
+ className="text-xs bg-sky-600 hover:bg-sky-700 text-white px-2 py-1 rounded flex items-center space-x-1"
842
+ >
843
+ <Settings size={12} />
844
+ <span>配置</span>
845
+ </button>
846
+ </div>
847
+ </div>
848
+
849
+ {activeRoles.length === 0 ? (
850
+ <div className="p-4 text-center text-gray-400">
851
+ <p className="mb-2">暂无活跃角色</p>
852
+ <button
853
+ onClick={() => {
854
+ setIsConfigManagerOpen(true);
855
+ setIsRoleSelectorOpen(false);
856
+ }}
857
+ className="text-sm bg-sky-600 hover:bg-sky-700 text-white px-3 py-1 rounded"
858
+ >
859
+ 添加角色
860
+ </button>
861
+ </div>
862
+ ) : (
863
+ <div className="max-h-64 overflow-y-auto">
864
+ {activeRoles.map((role) => (
865
+ <div key={role.id} className="p-3 border-b border-gray-700 last:border-b-0">
866
+ <div className="flex justify-between items-start">
867
+ <div className="flex-1">
868
+ <div className="flex items-center space-x-2">
869
+ <h4 className="text-white font-medium">{role.name}</h4>
870
+ <button
871
+ onClick={() => toggleRoleActiveState(role.id)}
872
+ className={`p-1 rounded transition-colors ${
873
+ role.isActive ? 'text-green-400 hover:text-green-300' : 'text-gray-500 hover:text-gray-400'
874
+ }`}
875
+ title={role.isActive ? '暂��角色' : '激活角色'}
876
+ >
877
+ {role.isActive ? <Play size={14} /> : <Pause size={14} />}
878
+ </button>
879
+ </div>
880
+ <p className="text-gray-400 text-xs">{role.model.name}</p>
881
+ <p className="text-gray-500 text-xs">渠道: {role.channel.name}</p>
882
+ <div className="flex space-x-1 mt-1">
883
+ {role.model.supportsImages && (
884
+ <span className="text-xs bg-green-600 text-white px-1 rounded">图像</span>
885
+ )}
886
+ {role.model.supportsReducedCapacity && (
887
+ <span className="text-xs bg-blue-600 text-white px-1 rounded">优化</span>
888
+ )}
889
+ {!role.channel.apiKey?.trim() && (
890
+ <span className="text-xs bg-red-600 text-white px-1 rounded">缺少密钥</span>
891
+ )}
892
+ </div>
893
+ </div>
894
+ </div>
895
+ </div>
896
+ ))}
897
+ </div>
898
+ )}
899
+ </div>
900
+ )}
901
+
902
+ {isRoleSelectorOpen && (
903
+ <div
904
+ className="fixed inset-0 z-40"
905
+ onClick={() => setIsRoleSelectorOpen(false)}
906
+ />
907
+ )}
908
+ </div>
909
+
910
+ <Separator />
911
+
912
+ <div className="flex items-center space-x-1.5">
913
+ <label
914
+ htmlFor="discussionModeToggle"
915
+ className="flex items-center text-sm text-gray-300 cursor-pointer hover:text-sky-400"
916
+ title={discussionMode === DiscussionMode.FixedTurns ? "切换到AI驱动模式" : "切换到固定轮次模式"}
917
+ >
918
+ {discussionMode === DiscussionMode.FixedTurns
919
+ ? <MessagesSquare size={18} className="mr-1 text-sky-400" />
920
+ : <Bot size={18} className="mr-1 text-sky-400" />}
921
+ <span className="mr-1 select-none shrink-0">模式:</span>
922
+ <div className="relative">
923
+ <input
924
+ type="checkbox"
925
+ id="discussionModeToggle"
926
+ className="sr-only peer"
927
+ checked={discussionMode === DiscussionMode.AiDriven}
928
+ onChange={() => setDiscussionMode(prev => prev === DiscussionMode.FixedTurns ? DiscussionMode.AiDriven : DiscussionMode.FixedTurns)}
929
+ aria-label="切换对话模式"
930
+ disabled={isLoading}
931
+ />
932
+ <div className={`block w-10 h-6 rounded-full transition-colors ${discussionMode === DiscussionMode.AiDriven ? 'bg-sky-500' : 'bg-gray-600'}`}></div>
933
+ <div className={`absolute left-1 top-1 bg-white w-4 h-4 rounded-full transition-transform ${discussionMode === DiscussionMode.AiDriven ? 'translate-x-4' : ''}`}></div>
934
+ </div>
935
+ <span className="ml-1.5 select-none shrink-0 min-w-[4rem] text-left">
936
+ {discussionMode === DiscussionMode.FixedTurns ? '固定' : 'AI驱动'}
937
+ </span>
938
+ </label>
939
+ {discussionMode === DiscussionMode.FixedTurns && (
940
+ <div className="flex items-center text-sm text-gray-300">
941
+ <input
942
+ type="number"
943
+ id="manualFixedTurnsInput"
944
+ value={manualFixedTurns}
945
+ onChange={handleManualFixedTurnsChange}
946
+ min={MIN_MANUAL_FIXED_TURNS}
947
+ max={MAX_MANUAL_FIXED_TURNS}
948
+ className="w-14 bg-gray-700 border border-gray-600 text-white text-sm rounded-md p-1 text-center focus:ring-1 focus:ring-sky-500 focus:border-sky-500 outline-none"
949
+ aria-label="设置固定轮次数量"
950
+ disabled={isLoading}
951
+ />
952
+ <span className="ml-1 select-none">轮</span>
953
+ </div>
954
+ )}
955
+ </div>
956
+
957
+ <Separator />
958
+
959
+ <label
960
+ htmlFor="capacityToggle"
961
+ className="flex items-center text-sm text-gray-300 cursor-pointer hover:text-sky-400"
962
+ title={isReducedCapacityEnabled ? "切换为优质模式 (完整性能)" : "切换为快速模式 (降低性能)"}
963
+ >
964
+ <SlidersHorizontal size={18} className={`mr-1.5 ${!isReducedCapacityEnabled ? 'text-sky-400' : 'text-gray-500'}`} />
965
+ <span className="mr-2 select-none shrink-0">性能:</span>
966
+ <div className="relative">
967
+ <input
968
+ type="checkbox"
969
+ id="capacityToggle"
970
+ className="sr-only peer"
971
+ checked={!isReducedCapacityEnabled}
972
+ onChange={() => setIsReducedCapacityEnabled(!isReducedCapacityEnabled)}
973
+ aria-label="切换AI性能模式"
974
+ disabled={isLoading}
975
+ />
976
+ <div className={`block w-10 h-6 rounded-full transition-colors ${!isReducedCapacityEnabled ? 'bg-sky-500 peer-checked:bg-sky-500' : 'bg-gray-600'}`}></div>
977
+ <div className={`absolute left-1 top-1 bg-white w-4 h-4 rounded-full transition-transform ${!isReducedCapacityEnabled ? 'peer-checked:translate-x-4' : ''}`}></div>
978
+ </div>
979
+ <span className="ml-2 w-20 text-left select-none shrink-0">
980
+ {!isReducedCapacityEnabled ? '优质' : '快速'}
981
+ </span>
982
+ </label>
983
+
984
+ <Separator />
985
+
986
+ <button
987
+ onClick={() => setIsConfigManagerOpen(true)}
988
+ className="p-2 text-gray-400 hover:text-sky-400 transition-colors duration-150 focus:outline-none focus:ring-2 focus:ring-sky-500 focus:ring-offset-2 focus:ring-offset-gray-900 rounded-md shrink-0"
989
+ aria-label="配置管理"
990
+ title="配置管理"
991
+ >
992
+ <Settings size={22} />
993
+ </button>
994
+
995
+ <button
996
+ onClick={handleClearChat}
997
+ className="p-2 text-gray-400 hover:text-sky-400 transition-colors duration-150 focus:outline-none focus:ring-2 focus:ring-sky-500 focus:ring-offset-2 focus:ring-offset-gray-900 rounded-md shrink-0"
998
+ aria-label="清空会话"
999
+ title="清空会话"
1000
+ disabled={isLoading}
1001
+ >
1002
+ <RefreshCcw size={22} />
1003
+ </button>
1004
+ </div>
1005
+ </header>
1006
+
1007
+ <div className="flex flex-row flex-grow overflow-hidden">
1008
+ <div className="flex flex-col w-2/3 md:w-3/5 lg:w-2/3 h-full">
1009
+ <div ref={chatContainerRef} className="flex-grow p-4 space-y-4 overflow-y-auto bg-gray-800 scroll-smooth">
1010
+ {messages.map((msg) => {
1011
+ const streamingState = streamingMessages.get(msg.id);
1012
+ const displayMessage = streamingState && !streamingState.isComplete
1013
+ ? { ...msg, text: streamingState.text }
1014
+ : msg;
1015
+
1016
+ return <MessageBubble key={msg.id} message={displayMessage} />;
1017
+ })}
1018
+ </div>
1019
+ <ChatInput onSendMessage={handleSendMessage} isLoading={isLoading} isApiKeyMissing={!isSystemReady} />
1020
+ </div>
1021
+
1022
+ <div className="w-1/3 md:w-2/5 lg:w-1/3 h-full bg-slate-800">
1023
+ <Notepad
1024
+ content={notepadContent}
1025
+ lastUpdatedBy={lastNotepadUpdateBy}
1026
+ isLoading={isLoading}
1027
+ />
1028
+ </div>
1029
+ </div>
1030
+
1031
+ {/* 配置管理器 */}
1032
+ <ModelConfigManager
1033
+ isOpen={isConfigManagerOpen}
1034
+ onClose={() => setIsConfigManagerOpen(false)}
1035
+ onConfigChange={loadConfiguration}
1036
+ />
1037
+
1038
+ {/* 处理时间显示 */}
1039
+ {(isLoading || (currentTotalProcessingTimeMs > 0 && !isLoading) || (isLoading && currentTotalProcessingTimeMs === 0)) && (
1040
+ <div className="fixed bottom-4 right-4 md:bottom-6 md:right-6 bg-gray-900 bg-opacity-80 text-white p-2 rounded-md shadow-lg text-xs z-50">
1041
+ 总耗时: {(currentTotalProcessingTimeMs / 1000).toFixed(2)}s
1042
+ {isDiscussionActive && (
1043
+ <div className="text-green-400 mt-1">讨论进行中...</div>
1044
+ )}
1045
+ </div>
1046
+ )}
1047
+
1048
+ {/* 系统状态提示 */}
1049
+ {!isSystemReady && (
1050
+ <div className="fixed bottom-4 left-1/2 transform -translate-x-1/2 p-3 bg-orange-700 text-white rounded-lg shadow-lg flex items-center text-sm z-50">
1051
+ <AlertTriangle size={20} className="mr-2" />
1052
+ {!hasValidChannels ? '请配置API渠道和密钥' : '请配置AI角色'}
1053
+ 。点击设置按钮进行配置。
1054
+ </div>
1055
+ )}
1056
+ </div>
1057
+ );
1058
+ };
1059
+
1060
+ export default App;
Dockerfile ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 使用Node 18作为基础镜像
2
+ FROM node:18-alpine AS builder
3
+
4
+ # 设置工作目录
5
+ WORKDIR /app
6
+
7
+ # 复制package.json和package-lock.json
8
+ COPY package*.json ./
9
+
10
+ # 安装依赖
11
+ RUN npm ci --no-audit --no-fund
12
+
13
+ # 复制源代码
14
+ COPY . .
15
+
16
+ # 设置构建时的环境变量(这些会被嵌入到构建产物中)
17
+ ARG VITE_PRESET_NAME="快速体验渠道"
18
+ ARG VITE_PRESET_BASE_URL
19
+ ARG VITE_PRESET_API_KEY
20
+
21
+ # 构建应用
22
+ RUN npm run build
23
+
24
+ # 使用轻量级的Node镜像作为运行时
25
+ FROM node:18-alpine
26
+
27
+ # 安装serve来提供静态文件服务
28
+ RUN npm install -g serve
29
+
30
+ # 设置工作目录
31
+ WORKDIR /app
32
+
33
+ # 从构建阶段复制构建产物
34
+ COPY --from=builder /app/dist ./dist
35
+
36
+ # 暴露端口
37
+ EXPOSE 7860
38
+
39
+ # 设置健康检查
40
+ HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
41
+ CMD node -e "require('http').get('http://localhost:7860', (res) => { process.exit(res.statusCode === 200 ? 0 : 1); })"
42
+
43
+ # 启动应用
44
+ CMD ["serve", "-s", "dist", "-l", "7860"]
components/ChatInput.tsx ADDED
@@ -0,0 +1,191 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import React, { useState, useRef, useCallback, useEffect } from 'react';
3
+ import { Send, Paperclip, XCircle } from 'lucide-react';
4
+ import LoadingSpinner from './LoadingSpinner';
5
+
6
+ interface ChatInputProps {
7
+ onSendMessage: (message: string, imageFile?: File | null) => void;
8
+ isLoading: boolean;
9
+ isApiKeyMissing: boolean;
10
+ }
11
+
12
+ const ACCEPTED_IMAGE_TYPES = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
13
+
14
+ const ChatInput: React.FC<ChatInputProps> = ({ onSendMessage, isLoading, isApiKeyMissing }) => {
15
+ const [inputValue, setInputValue] = useState('');
16
+ const [selectedImage, setSelectedImage] = useState<File | null>(null);
17
+ const [imagePreviewUrl, setImagePreviewUrl] = useState<string | null>(null);
18
+ const [isDraggingOver, setIsDraggingOver] = useState(false);
19
+ const textareaRef = useRef<HTMLTextAreaElement>(null);
20
+ const fileInputRef = useRef<HTMLInputElement>(null);
21
+
22
+ useEffect(() => {
23
+ if (selectedImage) {
24
+ const objectUrl = URL.createObjectURL(selectedImage);
25
+ setImagePreviewUrl(objectUrl);
26
+ return () => URL.revokeObjectURL(objectUrl);
27
+ }
28
+ setImagePreviewUrl(null);
29
+ }, [selectedImage]);
30
+
31
+ const handleImageFile = (file: File | null) => {
32
+ if (file && ACCEPTED_IMAGE_TYPES.includes(file.type)) {
33
+ setSelectedImage(file);
34
+ } else if (file) {
35
+ alert('不支持的文件类型。请选择 JPG, PNG, GIF, 或 WEBP 格式的图片。');
36
+ setSelectedImage(null);
37
+ } else {
38
+ setSelectedImage(null);
39
+ }
40
+ // Reset file input value to allow selecting the same file again
41
+ if (fileInputRef.current) {
42
+ fileInputRef.current.value = "";
43
+ }
44
+ };
45
+
46
+ const removeImage = () => {
47
+ setSelectedImage(null);
48
+ setImagePreviewUrl(null);
49
+ };
50
+
51
+ const triggerSendMessage = () => {
52
+ if ((inputValue.trim() || selectedImage) && !isLoading && !isApiKeyMissing) {
53
+ onSendMessage(inputValue.trim(), selectedImage);
54
+ setInputValue('');
55
+ removeImage(); // Clear image after sending
56
+ if (textareaRef.current) { // Reset textarea height after sending
57
+ textareaRef.current.style.height = 'auto';
58
+ }
59
+ }
60
+ };
61
+
62
+ const handleSubmit = (e: React.FormEvent) => {
63
+ e.preventDefault();
64
+ triggerSendMessage();
65
+ };
66
+
67
+ const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
68
+ if (e.key === 'Enter' && e.ctrlKey) {
69
+ e.preventDefault(); // Prevent new line
70
+ triggerSendMessage();
71
+ }
72
+ };
73
+
74
+ const handlePaste = useCallback((e: React.ClipboardEvent<HTMLTextAreaElement>) => {
75
+ const items = e.clipboardData?.items;
76
+ if (items) {
77
+ for (let i = 0; i < items.length; i++) {
78
+ if (ACCEPTED_IMAGE_TYPES.includes(items[i].type)) {
79
+ const file = items[i].getAsFile();
80
+ if (file) {
81
+ handleImageFile(file);
82
+ e.preventDefault(); // Prevent pasting file path as text
83
+ break;
84
+ }
85
+ }
86
+ }
87
+ }
88
+ }, []);
89
+
90
+ const handleDrop = useCallback((e: React.DragEvent<HTMLTextAreaElement>) => {
91
+ e.preventDefault();
92
+ setIsDraggingOver(false);
93
+ if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
94
+ handleImageFile(e.dataTransfer.files[0]);
95
+ e.dataTransfer.clearData();
96
+ }
97
+ }, []);
98
+
99
+ const handleDragOver = (e: React.DragEvent<HTMLTextAreaElement>) => {
100
+ e.preventDefault();
101
+ setIsDraggingOver(true);
102
+ };
103
+
104
+ const handleDragLeave = (e: React.DragEvent<HTMLTextAreaElement>) => {
105
+ e.preventDefault();
106
+ setIsDraggingOver(false);
107
+ };
108
+
109
+ const handleFileButtonClick = () => {
110
+ if (fileInputRef.current) {
111
+ fileInputRef.current.click();
112
+ }
113
+ };
114
+
115
+ const handleFileSelected = (e: React.ChangeEvent<HTMLInputElement>) => {
116
+ if (e.target.files && e.target.files.length > 0) {
117
+ handleImageFile(e.target.files[0]);
118
+ }
119
+ };
120
+
121
+ const isDisabled = isLoading || isApiKeyMissing;
122
+
123
+ return (
124
+ <form onSubmit={handleSubmit} className="p-4 bg-gray-800 border-t border-gray-700">
125
+ {imagePreviewUrl && selectedImage && (
126
+ <div className="mb-2 p-2 bg-gray-700 rounded-md relative max-w-xs">
127
+ <img src={imagePreviewUrl} alt={selectedImage.name || "图片预览"} className="max-h-24 max-w-full rounded" />
128
+ <button
129
+ type="button"
130
+ onClick={removeImage}
131
+ className="absolute top-1 right-1 bg-black bg-opacity-50 text-white rounded-full p-0.5 hover:bg-opacity-75"
132
+ aria-label="移除图片"
133
+ >
134
+ <XCircle size={20} />
135
+ </button>
136
+ <div className="text-xs text-gray-300 mt-1 truncate">{selectedImage.name} ({(selectedImage.size / 1024).toFixed(1)} KB)</div>
137
+ </div>
138
+ )}
139
+ <div className="flex items-end space-x-2">
140
+ <textarea
141
+ ref={textareaRef}
142
+ value={inputValue}
143
+ onChange={(e) => setInputValue(e.target.value)}
144
+ onKeyDown={handleKeyDown} // Added keydown handler
145
+ onPaste={handlePaste}
146
+ onDrop={handleDrop}
147
+ onDragOver={handleDragOver}
148
+ onDragLeave={handleDragLeave}
149
+ placeholder={isApiKeyMissing ? "API密钥未配置,聊天功能已禁用。" : (isDraggingOver ? "将图片拖放到此处" : "输入您的消息 (Ctrl+Enter 发送) 或粘贴/拖放图片...")}
150
+ className={`flex-grow p-3 bg-gray-700 border border-gray-600 rounded-lg focus:ring-2 focus:ring-sky-500 focus:border-sky-500 outline-none placeholder-gray-400 disabled:opacity-50 resize-none min-h-[48px] max-h-[150px] ${isDraggingOver ? 'ring-2 ring-sky-500 border-sky-500' : ''}`}
151
+ rows={1} // Start with 1 row, auto-expands
152
+ disabled={isDisabled}
153
+ aria-label="聊天输入框"
154
+ onInput={(e) => { // Auto-resize textarea
155
+ const target = e.target as HTMLTextAreaElement;
156
+ target.style.height = 'auto';
157
+ target.style.height = `${target.scrollHeight}px`;
158
+ }}
159
+ />
160
+ <input
161
+ type="file"
162
+ ref={fileInputRef}
163
+ onChange={handleFileSelected}
164
+ accept={ACCEPTED_IMAGE_TYPES.join(',')}
165
+ className="hidden"
166
+ aria-label="选择图片文件"
167
+ />
168
+ <button
169
+ type="button"
170
+ onClick={handleFileButtonClick}
171
+ className="p-3 bg-gray-600 hover:bg-gray-500 rounded-lg text-white transition-colors duration-150 focus:outline-none focus:ring-2 focus:ring-sky-500 focus:ring-offset-2 focus:ring-offset-gray-800 disabled:opacity-50 disabled:cursor-not-allowed self-end h-[48px]"
172
+ disabled={isDisabled}
173
+ aria-label="添加图片附件"
174
+ title="添加图片"
175
+ >
176
+ <Paperclip size={24} />
177
+ </button>
178
+ <button
179
+ type="submit"
180
+ className="p-3 bg-sky-600 hover:bg-sky-700 rounded-lg text-white transition-colors duration-150 focus:outline-none focus:ring-2 focus:ring-sky-500 focus:ring-offset-2 focus:ring-offset-gray-800 disabled:opacity-50 disabled:cursor-not-allowed self-end h-[48px]"
181
+ disabled={isDisabled || (!inputValue.trim() && !selectedImage)}
182
+ aria-label={isLoading ? "发送中" : "发送消息"}
183
+ >
184
+ {isLoading ? <LoadingSpinner size="w-6 h-6" color="text-white" /> : <Send size={24} />}
185
+ </button>
186
+ </div>
187
+ </form>
188
+ );
189
+ };
190
+
191
+ export default ChatInput;
components/LoadingSpinner.tsx ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import React from 'react';
3
+
4
+ interface LoadingSpinnerProps {
5
+ size?: string; // e.g., 'w-8 h-8'
6
+ color?: string; // e.g., 'text-blue-500'
7
+ }
8
+
9
+ const LoadingSpinner: React.FC<LoadingSpinnerProps> = ({ size = 'w-5 h-5', color = 'text-sky-400' }) => {
10
+ return (
11
+ <svg
12
+ className={`animate-spin ${size} ${color}`}
13
+ xmlns="http://www.w3.org/2000/svg"
14
+ fill="none"
15
+ viewBox="0 0 24 24"
16
+ >
17
+ <circle
18
+ className="opacity-25"
19
+ cx="12"
20
+ cy="12"
21
+ r="10"
22
+ stroke="currentColor"
23
+ strokeWidth="4"
24
+ ></circle>
25
+ <path
26
+ className="opacity-75"
27
+ fill="currentColor"
28
+ d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
29
+ ></path>
30
+ </svg>
31
+ );
32
+ };
33
+
34
+ export default LoadingSpinner;
35
+
components/MessageBubble.tsx ADDED
@@ -0,0 +1,201 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useEffect } from 'react';
2
+ import { ChatMessage, MessageSender, MessagePurpose } from '../types';
3
+ import { Lightbulb, MessageSquareText, UserCircle, Zap, AlertTriangle, Copy, Check, MoreHorizontal } from 'lucide-react';
4
+ import { marked } from 'marked';
5
+ import DOMPurify from 'dompurify';
6
+
7
+ interface SenderIconProps {
8
+ sender: MessageSender;
9
+ purpose: MessagePurpose;
10
+ messageText: string;
11
+ }
12
+
13
+ const SenderIcon: React.FC<SenderIconProps> = ({ sender, purpose, messageText }) => {
14
+ const iconClass = "w-5 h-5 mr-2 flex-shrink-0";
15
+ switch (sender) {
16
+ case MessageSender.User:
17
+ return <UserCircle className={`${iconClass} text-blue-400`} />;
18
+ case MessageSender.Cognito:
19
+ return <Lightbulb className={`${iconClass} text-green-400`} />;
20
+ case MessageSender.Muse:
21
+ return <Zap className={`${iconClass} text-purple-400`} />;
22
+ case MessageSender.System:
23
+ if (
24
+ purpose === MessagePurpose.SystemNotification &&
25
+ (messageText.toLowerCase().includes("error") ||
26
+ messageText.toLowerCase().includes("错误") ||
27
+ messageText.toLowerCase().includes("警告"))
28
+ ) {
29
+ return <AlertTriangle className={`${iconClass} text-red-400`} />;
30
+ }
31
+ return <MessageSquareText className={`${iconClass} text-gray-400`} />;
32
+ default:
33
+ // For dynamic role names, use a generic bot icon with dynamic color
34
+ const colorClasses = [
35
+ 'text-green-400', 'text-purple-400', 'text-blue-400',
36
+ 'text-yellow-400', 'text-pink-400', 'text-indigo-400'
37
+ ];
38
+ const colorIndex = typeof sender === 'string' ?
39
+ sender.length % colorClasses.length : 0;
40
+ return <Lightbulb className={`${iconClass} ${colorClasses[colorIndex]}`} />;
41
+ }
42
+ };
43
+
44
+ const getSenderNameStyle = (sender: MessageSender): string => {
45
+ switch (sender) {
46
+ case MessageSender.User: return "text-blue-300";
47
+ case MessageSender.Cognito: return "text-green-300";
48
+ case MessageSender.Muse: return "text-purple-300";
49
+ case MessageSender.System: return "text-gray-400";
50
+ default:
51
+ // For dynamic role names, apply dynamic colors
52
+ const colorClasses = [
53
+ 'text-green-300', 'text-purple-300', 'text-blue-300',
54
+ 'text-yellow-300', 'text-pink-300', 'text-indigo-300'
55
+ ];
56
+ const colorIndex = typeof sender === 'string' ?
57
+ sender.length % colorClasses.length : 0;
58
+ return colorClasses[colorIndex];
59
+ }
60
+ }
61
+
62
+ const getBubbleStyle = (sender: MessageSender, purpose: MessagePurpose, messageText: string): string => {
63
+ let baseStyle = "mb-4 p-4 rounded-lg shadow-md max-w-xl break-words relative ";
64
+ if (purpose === MessagePurpose.SystemNotification) {
65
+ if (
66
+ messageText.toLowerCase().includes("error") ||
67
+ messageText.toLowerCase().includes("错误") ||
68
+ messageText.toLowerCase().includes("警告") ||
69
+ messageText.toLowerCase().includes("critical") ||
70
+ messageText.toLowerCase().includes("严重")
71
+ ) {
72
+ return baseStyle + "bg-red-800 border border-red-700 text-center text-sm italic mx-auto text-red-200";
73
+ }
74
+ return baseStyle + "bg-gray-700 text-center text-sm italic mx-auto";
75
+ }
76
+ switch (sender) {
77
+ case MessageSender.User:
78
+ return baseStyle + "bg-blue-600 ml-auto rounded-br-none";
79
+ case MessageSender.Cognito:
80
+ return baseStyle + "bg-green-700 mr-auto rounded-bl-none";
81
+ case MessageSender.Muse:
82
+ return baseStyle + "bg-purple-700 mr-auto rounded-bl-none";
83
+ default:
84
+ // For dynamic role names, use varied background colors
85
+ const bgColors = [
86
+ 'bg-green-700', 'bg-purple-700', 'bg-blue-700',
87
+ 'bg-yellow-700', 'bg-pink-700', 'bg-indigo-700'
88
+ ];
89
+ const bgIndex = typeof sender === 'string' ?
90
+ sender.length % bgColors.length : 0;
91
+ return baseStyle + bgColors[bgIndex] + " mr-auto rounded-bl-none";
92
+ }
93
+ };
94
+
95
+ const getPurposePrefix = (purpose: MessagePurpose, sender: MessageSender): string => {
96
+ switch (purpose) {
97
+ case MessagePurpose.CognitoToMuse:
98
+ return `致 ${MessageSender.Muse}的消息: `;
99
+ case MessagePurpose.MuseToCognito:
100
+ return `致 ${MessageSender.Cognito}的消息: `;
101
+ case MessagePurpose.FinalResponse:
102
+ return `最终答案: `;
103
+ default:
104
+ return "";
105
+ }
106
+ }
107
+
108
+ interface MessageBubbleProps {
109
+ message: ChatMessage;
110
+ }
111
+
112
+ const MessageBubble: React.FC<MessageBubbleProps> = ({ message }) => {
113
+ const { text: messageText, sender, purpose, timestamp, durationMs, image } = message;
114
+ const formattedTime = new Date(timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
115
+ const [isCopied, setIsCopied] = useState(false);
116
+
117
+ // 移除所有流式动画逻辑,直接显示内容
118
+ const isDiscussionStep = purpose === MessagePurpose.CognitoToMuse || purpose === MessagePurpose.MuseToCognito;
119
+ const isFinalResponse = purpose === MessagePurpose.FinalResponse;
120
+ const showDuration = durationMs !== undefined && durationMs > 0;
121
+
122
+ const shouldRenderMarkdown =
123
+ (sender === MessageSender.User || sender === MessageSender.Cognito || sender === MessageSender.Muse || typeof sender === 'string') &&
124
+ purpose !== MessagePurpose.SystemNotification;
125
+
126
+ let sanitizedHtml = '';
127
+ if (shouldRenderMarkdown && messageText) {
128
+ const rawHtml = marked.parse(messageText) as string;
129
+ sanitizedHtml = DOMPurify.sanitize(rawHtml);
130
+ }
131
+
132
+ const handleCopy = async () => {
133
+ try {
134
+ await navigator.clipboard.writeText(messageText);
135
+ setIsCopied(true);
136
+ setTimeout(() => setIsCopied(false), 2000);
137
+ } catch (err) {
138
+ console.error('无法复制文本: ', err);
139
+ }
140
+ };
141
+
142
+ const canCopy = (sender === MessageSender.User || sender === MessageSender.Cognito || sender === MessageSender.Muse || typeof sender === 'string') && purpose !== MessagePurpose.SystemNotification;
143
+
144
+ return (
145
+ <div className={`flex ${sender === MessageSender.User ? 'justify-end' : 'justify-start'}`}>
146
+ <div className={getBubbleStyle(sender, purpose, messageText)}>
147
+ {canCopy && (
148
+ <button
149
+ onClick={handleCopy}
150
+ title={isCopied ? "已复制!" : "复制消息"}
151
+ className="absolute top-1.5 right-1.5 p-1 text-gray-400 hover:text-sky-300 transition-colors rounded-md focus:outline-none focus:ring-1 focus:ring-sky-500"
152
+ >
153
+ {isCopied ? <Check size={16} className="text-green-400" /> : <Copy size={16} />}
154
+ </button>
155
+ )}
156
+
157
+ <div className="flex items-center mb-1">
158
+ <SenderIcon sender={sender} purpose={purpose} messageText={messageText} />
159
+ <span className={`font-semibold ${getSenderNameStyle(sender)}`}>{sender}</span>
160
+ {isDiscussionStep && <span className="ml-2 text-xs text-gray-400">(内部讨论)</span>}
161
+ </div>
162
+
163
+ {messageText ? (
164
+ shouldRenderMarkdown ? (
165
+ <div
166
+ className="chat-markdown-content text-sm text-gray-200"
167
+ dangerouslySetInnerHTML={{ __html: sanitizedHtml }}
168
+ />
169
+ ) : (
170
+ <p className="text-sm text-gray-200 whitespace-pre-wrap">
171
+ {messageText}
172
+ </p>
173
+ )
174
+ ) : (
175
+ <p className="text-sm text-gray-400 italic">
176
+ 正在生成回复...
177
+ </p>
178
+ )}
179
+
180
+ {image && sender === MessageSender.User && (
181
+ <div className={`mt-2 ${messageText ? 'pt-2 border-t border-blue-500' : ''}`}>
182
+ <img
183
+ src={image.dataUrl}
184
+ alt={image.name || "用户上传的图片"}
185
+ className="max-w-xs max-h-64 rounded-md object-contain"
186
+ />
187
+ </div>
188
+ )}
189
+
190
+ <div className="text-xs text-gray-400 mt-2 flex justify-between items-center">
191
+ <span>{formattedTime}</span>
192
+ {showDuration && (
193
+ <span className="italic"> (耗时: {(durationMs / 1000).toFixed(2)}s)</span>
194
+ )}
195
+ </div>
196
+ </div>
197
+ </div>
198
+ );
199
+ };
200
+
201
+ export default MessageBubble;
components/ModelConfigManager.tsx ADDED
@@ -0,0 +1,1143 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useEffect } from 'react';
2
+ import { AiModel, AiRole, ApiChannel, ModelConfigManager } from '../constants';
3
+ import {
4
+ Settings,
5
+ Plus,
6
+ Edit3,
7
+ Trash2,
8
+ Save,
9
+ X,
10
+ Download,
11
+ Upload,
12
+ RefreshCw,
13
+ Bot,
14
+ Brain,
15
+ Image,
16
+ Zap,
17
+ AlertCircle,
18
+ Check,
19
+ HardDrive,
20
+ Globe,
21
+ Key,
22
+ Eye,
23
+ EyeOff,
24
+ Star,
25
+ StarOff
26
+ } from 'lucide-react';
27
+
28
+ interface ModelConfigManagerProps {
29
+ isOpen: boolean;
30
+ onClose: () => void;
31
+ onConfigChange: () => void;
32
+ }
33
+
34
+ interface EditingChannel extends Partial<ApiChannel> {
35
+ isNew?: boolean;
36
+ }
37
+
38
+ interface EditingModel extends Partial<AiModel> {
39
+ isNew?: boolean;
40
+ }
41
+
42
+ interface EditingRole extends Partial<AiRole> {
43
+ isNew?: boolean;
44
+ }
45
+
46
+ const ModelConfigManagerComponent: React.FC<ModelConfigManagerProps> = ({
47
+ isOpen,
48
+ onClose,
49
+ onConfigChange
50
+ }) => {
51
+ const [activeTab, setActiveTab] = useState<'channels' | 'models' | 'roles' | 'storage'>('channels');
52
+ const [channels, setChannels] = useState<ApiChannel[]>([]);
53
+ const [models, setModels] = useState<AiModel[]>([]);
54
+ const [roles, setRoles] = useState<AiRole[]>([]);
55
+ const [editingChannel, setEditingChannel] = useState<EditingChannel | null>(null);
56
+ const [editingModel, setEditingModel] = useState<EditingModel | null>(null);
57
+ const [editingRole, setEditingRole] = useState<EditingRole | null>(null);
58
+ const [validationErrors, setValidationErrors] = useState<string[]>([]);
59
+ const [importText, setImportText] = useState('');
60
+ const [showImport, setShowImport] = useState(false);
61
+ const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
62
+ const [storageInfo, setStorageInfo] = useState<{ used: number; available: number; channels: number; models: number; roles: number }>({ used: 0, available: 0, channels: 0, models: 0, roles: 0 });
63
+ const [showApiKeys, setShowApiKeys] = useState<Record<string, boolean>>({});
64
+
65
+ useEffect(() => {
66
+ if (isOpen) {
67
+ loadConfig();
68
+ updateStorageInfo();
69
+ }
70
+ }, [isOpen]);
71
+
72
+ const loadConfig = () => {
73
+ try {
74
+ setChannels(ModelConfigManager.getChannels());
75
+ setModels(ModelConfigManager.getModels());
76
+ setRoles(ModelConfigManager.getRoles());
77
+ } catch (error) {
78
+ console.error('加载配置失败:', error);
79
+ showMessage('error', '加载配置失败,请检查浏览器存储设置');
80
+ }
81
+ };
82
+
83
+ const updateStorageInfo = () => {
84
+ try {
85
+ setStorageInfo(ModelConfigManager.getStorageInfo());
86
+ } catch (error) {
87
+ console.error('获取存储信息失败:', error);
88
+ }
89
+ };
90
+
91
+ const showMessage = (type: 'success' | 'error', text: string) => {
92
+ setMessage({ type, text });
93
+ setTimeout(() => setMessage(null), 3000);
94
+ };
95
+
96
+ const toggleApiKeyVisibility = (channelId: string) => {
97
+ setShowApiKeys(prev => ({
98
+ ...prev,
99
+ [channelId]: !prev[channelId]
100
+ }));
101
+ };
102
+
103
+ // ============ 渠道管理 ============
104
+
105
+ const handleSaveChannel = () => {
106
+ if (!editingChannel) return;
107
+
108
+ const errors = ModelConfigManager.validateChannel(editingChannel);
109
+ if (errors.length > 0) {
110
+ setValidationErrors(errors);
111
+ return;
112
+ }
113
+
114
+ try {
115
+ if (editingChannel.isNew) {
116
+ const { id, createdAt, isCustom, isNew, ...channelData } = editingChannel;
117
+ ModelConfigManager.addChannel(channelData as Omit<ApiChannel, 'id' | 'createdAt' | 'isCustom'>);
118
+ showMessage('success', '渠道添加成功');
119
+ } else {
120
+ ModelConfigManager.updateChannel(editingChannel.id!, editingChannel);
121
+ showMessage('success', '渠道更新成功');
122
+ }
123
+
124
+ loadConfig();
125
+ updateStorageInfo();
126
+ setEditingChannel(null);
127
+ setValidationErrors([]);
128
+ onConfigChange();
129
+ } catch (error) {
130
+ showMessage('error', '保存渠道失败: ' + (error instanceof Error ? error.message : '未知错误'));
131
+ }
132
+ };
133
+
134
+ const handleDeleteChannel = (id: string) => {
135
+ // 检查是否有模型使用此渠道
136
+ const modelsUsingChannel = models.filter(model => model.channelId === id);
137
+ if (modelsUsingChannel.length > 0) {
138
+ showMessage('error', `无法删除渠道:有 ${modelsUsingChannel.length} 个模型正在使用此渠道`);
139
+ return;
140
+ }
141
+
142
+ if (window.confirm('确定要删除这个渠道吗?此操作不可撤销。')) {
143
+ try {
144
+ ModelConfigManager.deleteChannel(id);
145
+ loadConfig();
146
+ updateStorageInfo();
147
+ showMessage('success', '渠道删除成功');
148
+ onConfigChange();
149
+ } catch (error) {
150
+ showMessage('error', '删除渠道失败: ' + (error instanceof Error ? error.message : '未知错误'));
151
+ }
152
+ }
153
+ };
154
+
155
+ const handleSetDefaultChannel = (id: string) => {
156
+ try {
157
+ ModelConfigManager.updateChannel(id, { isDefault: true });
158
+ loadConfig();
159
+ showMessage('success', '默认渠道设置成功');
160
+ } catch (error) {
161
+ showMessage('error', '设置默认渠道失败');
162
+ }
163
+ };
164
+
165
+ // ============ 模型管理 ============
166
+
167
+ const handleSaveModel = () => {
168
+ if (!editingModel) return;
169
+
170
+ const errors = ModelConfigManager.validateModel(editingModel);
171
+ if (errors.length > 0) {
172
+ setValidationErrors(errors);
173
+ return;
174
+ }
175
+
176
+ try {
177
+ if (editingModel.isNew) {
178
+ const { id, createdAt, isCustom, isNew, ...modelData } = editingModel;
179
+ ModelConfigManager.addModel(modelData as Omit<AiModel, 'id' | 'createdAt' | 'isCustom'>);
180
+ showMessage('success', '模型添加成功');
181
+ } else {
182
+ ModelConfigManager.updateModel(editingModel.id!, editingModel);
183
+ showMessage('success', '模型更新成功');
184
+ }
185
+
186
+ loadConfig();
187
+ updateStorageInfo();
188
+ setEditingModel(null);
189
+ setValidationErrors([]);
190
+ onConfigChange();
191
+ } catch (error) {
192
+ showMessage('error', '保存模型失败: ' + (error instanceof Error ? error.message : '未知错误'));
193
+ }
194
+ };
195
+
196
+ const handleDeleteModel = (id: string) => {
197
+ // 检查是否有角色使用此模型
198
+ const rolesUsingModel = roles.filter(role => role.modelId === id);
199
+ if (rolesUsingModel.length > 0) {
200
+ showMessage('error', `无法删除模型:有 ${rolesUsingModel.length} 个角色正在使用此模型`);
201
+ return;
202
+ }
203
+
204
+ if (window.confirm('确定要删除这个模型吗?此操作不可撤销。')) {
205
+ try {
206
+ ModelConfigManager.deleteModel(id);
207
+ loadConfig();
208
+ updateStorageInfo();
209
+ showMessage('success', '模型删除成功');
210
+ onConfigChange();
211
+ } catch (error) {
212
+ showMessage('error', '删除模型失败: ' + (error instanceof Error ? error.message : '未知错误'));
213
+ }
214
+ }
215
+ };
216
+
217
+ // ============ 角色管理 ============
218
+
219
+ const handleSaveRole = () => {
220
+ if (!editingRole) return;
221
+
222
+ if (!editingRole.name?.trim() || !editingRole.systemPrompt?.trim() || !editingRole.modelId) {
223
+ setValidationErrors(['角色名称、系统提示词和关联模型都不能为空']);
224
+ return;
225
+ }
226
+
227
+ try {
228
+ if (editingRole.isNew) {
229
+ const { id, isNew, ...roleData } = editingRole;
230
+ ModelConfigManager.addRole(roleData as Omit<AiRole, 'id'>);
231
+ showMessage('success', '角色添加成功');
232
+ } else {
233
+ ModelConfigManager.updateRole(editingRole.id!, editingRole);
234
+ showMessage('success', '角色更新成功');
235
+ }
236
+
237
+ loadConfig();
238
+ updateStorageInfo();
239
+ setEditingRole(null);
240
+ setValidationErrors([]);
241
+ onConfigChange();
242
+ } catch (error) {
243
+ showMessage('error', '保存角色失败: ' + (error instanceof Error ? error.message : '未知错误'));
244
+ }
245
+ };
246
+
247
+ const handleDeleteRole = (id: string) => {
248
+ if (window.confirm('确定要删除这个角色吗?此操作不可撤销。')) {
249
+ try {
250
+ ModelConfigManager.deleteRole(id);
251
+ loadConfig();
252
+ updateStorageInfo();
253
+ showMessage('success', '角色删除成功');
254
+ onConfigChange();
255
+ } catch (error) {
256
+ showMessage('error', '删除角色失败: ' + (error instanceof Error ? error.message : '未知错误'));
257
+ }
258
+ }
259
+ };
260
+
261
+ // ============ 通用操作 ============
262
+
263
+ const handleExport = () => {
264
+ try {
265
+ const config = ModelConfigManager.exportConfig();
266
+ const blob = new Blob([config], { type: 'application/json' });
267
+ const url = URL.createObjectURL(blob);
268
+ const a = document.createElement('a');
269
+ a.href = url;
270
+ a.download = `multi-mind-chat-config-${new Date().toISOString().split('T')[0]}.json`;
271
+ document.body.appendChild(a);
272
+ a.click();
273
+ document.body.removeChild(a);
274
+ URL.revokeObjectURL(url);
275
+ showMessage('success', '配置导出成功');
276
+ } catch (error) {
277
+ showMessage('error', '导出配置失败: ' + (error instanceof Error ? error.message : '未知错误'));
278
+ }
279
+ };
280
+
281
+ const handleImport = () => {
282
+ if (!importText.trim()) {
283
+ showMessage('error', '请输入配置内容');
284
+ return;
285
+ }
286
+
287
+ try {
288
+ const result = ModelConfigManager.importConfig(importText);
289
+ if (result.success) {
290
+ loadConfig();
291
+ updateStorageInfo();
292
+ setImportText('');
293
+ setShowImport(false);
294
+ showMessage('success', result.message);
295
+ onConfigChange();
296
+ } else {
297
+ showMessage('error', result.message);
298
+ }
299
+ } catch (error) {
300
+ showMessage('error', '导入配置时发生错误: ' + (error instanceof Error ? error.message : '未知错误'));
301
+ }
302
+ };
303
+
304
+ const handleReset = () => {
305
+ if (window.confirm('确定要重置为默认配置吗?这将删除所有自定义配置。')) {
306
+ try {
307
+ ModelConfigManager.resetToDefaults();
308
+ loadConfig();
309
+ updateStorageInfo();
310
+ showMessage('success', '已重置为默认配置');
311
+ onConfigChange();
312
+ } catch (error) {
313
+ showMessage('error', '重置配置失败: ' + (error instanceof Error ? error.message : '未知错误'));
314
+ }
315
+ }
316
+ };
317
+
318
+ const handleClearAllData = () => {
319
+ if (window.confirm('警告:这将清空所有配置数据,包括渠道、模型和角色!此操作不可撤销,确定继续吗?')) {
320
+ try {
321
+ ModelConfigManager.clearAllData();
322
+ loadConfig();
323
+ updateStorageInfo();
324
+ showMessage('success', '所有数据已清空');
325
+ onConfigChange();
326
+ } catch (error) {
327
+ showMessage('error', '清空数据失败: ' + (error instanceof Error ? error.message : '未知错误'));
328
+ }
329
+ }
330
+ };
331
+
332
+ if (!isOpen) return null;
333
+
334
+ return (
335
+ <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
336
+ <div className="bg-gray-800 rounded-lg w-full max-w-6xl h-[90vh] flex flex-col">
337
+ {/* Header */}
338
+ <div className="flex items-center justify-between p-4 border-b border-gray-700">
339
+ <div className="flex items-center space-x-2">
340
+ <Settings size={24} className="text-sky-400" />
341
+ <h2 className="text-xl font-semibold text-white">Multi-Mind Chat 配置管理</h2>
342
+ </div>
343
+ <div className="flex items-center space-x-2">
344
+ <button
345
+ onClick={handleExport}
346
+ className="px-3 py-1 bg-green-600 hover:bg-green-700 text-white rounded text-sm flex items-center space-x-1"
347
+ title="导出配置"
348
+ >
349
+ <Download size={16} />
350
+ <span>导出</span>
351
+ </button>
352
+ <button
353
+ onClick={() => setShowImport(!showImport)}
354
+ className="px-3 py-1 bg-blue-600 hover:bg-blue-700 text-white rounded text-sm flex items-center space-x-1"
355
+ title="导入配置"
356
+ >
357
+ <Upload size={16} />
358
+ <span>导入</span>
359
+ </button>
360
+ <button
361
+ onClick={handleReset}
362
+ className="px-3 py-1 bg-orange-600 hover:bg-orange-700 text-white rounded text-sm flex items-center space-x-1"
363
+ title="重置为默认配置"
364
+ >
365
+ <RefreshCw size={16} />
366
+ <span>重置</span>
367
+ </button>
368
+ <button
369
+ onClick={onClose}
370
+ className="p-2 hover:bg-gray-700 rounded text-gray-400 hover:text-white"
371
+ title="关闭"
372
+ >
373
+ <X size={20} />
374
+ </button>
375
+ </div>
376
+ </div>
377
+
378
+ {/* Message */}
379
+ {message && (
380
+ <div className={`mx-4 mt-2 p-2 rounded text-sm flex items-center space-x-2 ${
381
+ message.type === 'success' ? 'bg-green-600 text-white' : 'bg-red-600 text-white'
382
+ }`}>
383
+ {message.type === 'success' ? <Check size={16} /> : <AlertCircle size={16} />}
384
+ <span>{message.text}</span>
385
+ </div>
386
+ )}
387
+
388
+ {/* Import Section */}
389
+ {showImport && (
390
+ <div className="mx-4 mt-2 p-4 bg-gray-700 rounded">
391
+ <h3 className="text-white mb-2">导入配置</h3>
392
+ <textarea
393
+ value={importText}
394
+ onChange={(e) => setImportText(e.target.value)}
395
+ className="w-full h-32 bg-gray-600 text-white p-2 rounded text-sm font-mono"
396
+ placeholder="粘贴配置JSON内容..."
397
+ />
398
+ <div className="flex justify-end space-x-2 mt-2">
399
+ <button
400
+ onClick={() => setShowImport(false)}
401
+ className="px-3 py-1 bg-gray-600 hover:bg-gray-500 text-white rounded text-sm"
402
+ >
403
+ 取消
404
+ </button>
405
+ <button
406
+ onClick={handleImport}
407
+ className="px-3 py-1 bg-blue-600 hover:bg-blue-700 text-white rounded text-sm"
408
+ >
409
+ 导入
410
+ </button>
411
+ </div>
412
+ </div>
413
+ )}
414
+
415
+ {/* Tabs */}
416
+ <div className="flex border-b border-gray-700">
417
+ <button
418
+ onClick={() => setActiveTab('channels')}
419
+ className={`px-4 py-2 text-sm font-medium transition-colors ${
420
+ activeTab === 'channels'
421
+ ? 'text-sky-400 border-b-2 border-sky-400'
422
+ : 'text-gray-400 hover:text-white'
423
+ }`}
424
+ >
425
+ <div className="flex items-center space-x-2">
426
+ <Globe size={16} />
427
+ <span>API渠道配置</span>
428
+ </div>
429
+ </button>
430
+ <button
431
+ onClick={() => setActiveTab('models')}
432
+ className={`px-4 py-2 text-sm font-medium transition-colors ${
433
+ activeTab === 'models'
434
+ ? 'text-sky-400 border-b-2 border-sky-400'
435
+ : 'text-gray-400 hover:text-white'
436
+ }`}
437
+ >
438
+ <div className="flex items-center space-x-2">
439
+ <Brain size={16} />
440
+ <span>AI模型配置</span>
441
+ </div>
442
+ </button>
443
+ <button
444
+ onClick={() => setActiveTab('roles')}
445
+ className={`px-4 py-2 text-sm font-medium transition-colors ${
446
+ activeTab === 'roles'
447
+ ? 'text-sky-400 border-b-2 border-sky-400'
448
+ : 'text-gray-400 hover:text-white'
449
+ }`}
450
+ >
451
+ <div className="flex items-center space-x-2">
452
+ <Bot size={16} />
453
+ <span>AI角色配置</span>
454
+ </div>
455
+ </button>
456
+ <button
457
+ onClick={() => setActiveTab('storage')}
458
+ className={`px-4 py-2 text-sm font-medium transition-colors ${
459
+ activeTab === 'storage'
460
+ ? 'text-sky-400 border-b-2 border-sky-400'
461
+ : 'text-gray-400 hover:text-white'
462
+ }`}
463
+ >
464
+ <div className="flex items-center space-x-2">
465
+ <HardDrive size={16} />
466
+ <span>存储管理</span>
467
+ </div>
468
+ </button>
469
+ </div>
470
+
471
+ {/* Content */}
472
+ <div className="flex-1 overflow-hidden">
473
+ {activeTab === 'channels' ? (
474
+ <div className="h-full flex">
475
+ {/* Channels List */}
476
+ <div className="w-1/2 border-r border-gray-700 overflow-y-auto">
477
+ <div className="p-4">
478
+ <div className="flex justify-between items-center mb-4">
479
+ <h3 className="text-lg font-medium text-white">API渠道列表</h3>
480
+ <button
481
+ onClick={() => setEditingChannel({
482
+ isNew: true,
483
+ isDefault: false,
484
+ timeout: 30000,
485
+ baseUrl: 'https://api.openai.com/v1'
486
+ })}
487
+ className="px-3 py-1 bg-sky-600 hover:bg-sky-700 text-white rounded text-sm flex items-center space-x-1"
488
+ >
489
+ <Plus size={16} />
490
+ <span>添加渠道</span>
491
+ </button>
492
+ </div>
493
+
494
+ <div className="space-y-2">
495
+ {channels.map((channel) => (
496
+ <div key={channel.id} className="bg-gray-700 rounded p-3">
497
+ <div className="flex justify-between items-start mb-2">
498
+ <div>
499
+ <div className="flex items-center space-x-2 mb-1">
500
+ <h4 className="text-white font-medium">{channel.name}</h4>
501
+ {channel.isDefault && (
502
+ <Star size={16} className="text-yellow-400" title="默认渠道" />
503
+ )}
504
+ </div>
505
+ <p className="text-gray-400 text-sm">{channel.baseUrl}</p>
506
+ <div className="flex items-center space-x-2 mt-2">
507
+ <Key size={12} className="text-gray-500" />
508
+ <span className="text-gray-500 text-xs">
509
+ {showApiKeys[channel.id]
510
+ ? channel.apiKey || '未设置'
511
+ : '••••••••••••••••••••••••••••••••••••••••'
512
+ }
513
+ </span>
514
+ <button
515
+ onClick={() => toggleApiKeyVisibility(channel.id)}
516
+ className="text-gray-500 hover:text-gray-300"
517
+ >
518
+ {showApiKeys[channel.id] ? <EyeOff size={12} /> : <Eye size={12} />}
519
+ </button>
520
+ </div>
521
+ {channel.description && (
522
+ <p className="text-gray-500 text-xs mt-1">{channel.description}</p>
523
+ )}
524
+ </div>
525
+ <div className="flex space-x-1">
526
+ {!channel.isDefault && (
527
+ <button
528
+ onClick={() => handleSetDefaultChannel(channel.id)}
529
+ className="p-1 text-gray-400 hover:text-yellow-400"
530
+ title="设为默认"
531
+ >
532
+ <StarOff size={16} />
533
+ </button>
534
+ )}
535
+ <button
536
+ onClick={() => setEditingChannel(channel)}
537
+ className="p-1 text-gray-400 hover:text-sky-400"
538
+ title="编辑"
539
+ >
540
+ <Edit3 size={16} />
541
+ </button>
542
+ {channel.isCustom && (
543
+ <button
544
+ onClick={() => handleDeleteChannel(channel.id)}
545
+ className="p-1 text-gray-400 hover:text-red-400"
546
+ title="删除"
547
+ >
548
+ <Trash2 size={16} />
549
+ </button>
550
+ )}
551
+ </div>
552
+ </div>
553
+ </div>
554
+ ))}
555
+ </div>
556
+ </div>
557
+ </div>
558
+
559
+ {/* Channel Editor */}
560
+ <div className="w-1/2 overflow-y-auto">
561
+ {editingChannel ? (
562
+ <div className="p-4">
563
+ <h3 className="text-lg font-medium text-white mb-4">
564
+ {editingChannel.isNew ? '添加新渠道' : '编辑渠道'}
565
+ </h3>
566
+
567
+ {validationErrors.length > 0 && (
568
+ <div className="mb-4 p-3 bg-red-600 rounded">
569
+ <div className="flex items-center space-x-2 mb-2">
570
+ <AlertCircle size={16} className="text-white" />
571
+ <span className="text-white font-medium">配置错误</span>
572
+ </div>
573
+ {validationErrors.map((error, index) => (
574
+ <p key={index} className="text-white text-sm">{error}</p>
575
+ ))}
576
+ </div>
577
+ )}
578
+
579
+ <div className="space-y-4">
580
+ <div>
581
+ <label className="block text-gray-300 text-sm mb-1">渠道名称</label>
582
+ <input
583
+ type="text"
584
+ value={editingChannel.name || ''}
585
+ onChange={(e) => setEditingChannel({ ...editingChannel, name: e.target.value })}
586
+ className="w-full bg-gray-700 text-white p-2 rounded text-sm"
587
+ placeholder="例如: OpenAI 官方"
588
+ />
589
+ </div>
590
+
591
+ <div>
592
+ <label className="block text-gray-300 text-sm mb-1">API基础URL</label>
593
+ <input
594
+ type="text"
595
+ value={editingChannel.baseUrl || ''}
596
+ onChange={(e) => setEditingChannel({ ...editingChannel, baseUrl: e.target.value })}
597
+ className="w-full bg-gray-700 text-white p-2 rounded text-sm"
598
+ placeholder="https://api.openai.com/v1"
599
+ />
600
+ </div>
601
+
602
+ <div>
603
+ <label className="block text-gray-300 text-sm mb-1">API密钥</label>
604
+ <div className="relative">
605
+ <input
606
+ type="password"
607
+ value={editingChannel.apiKey || ''}
608
+ onChange={(e) => setEditingChannel({ ...editingChannel, apiKey: e.target.value })}
609
+ className="w-full bg-gray-700 text-white p-2 rounded text-sm"
610
+ placeholder="输入API密钥"
611
+ />
612
+ </div>
613
+ </div>
614
+
615
+ <div>
616
+ <label className="block text-gray-300 text-sm mb-1">描述(可选)</label>
617
+ <textarea
618
+ value={editingChannel.description || ''}
619
+ onChange={(e) => setEditingChannel({ ...editingChannel, description: e.target.value })}
620
+ className="w-full bg-gray-700 text-white p-2 rounded text-sm h-20"
621
+ placeholder="渠道用途描述..."
622
+ />
623
+ </div>
624
+
625
+ <div>
626
+ <label className="block text-gray-300 text-sm mb-1">超时时间(毫秒)</label>
627
+ <input
628
+ type="number"
629
+ value={editingChannel.timeout || 30000}
630
+ onChange={(e) => setEditingChannel({ ...editingChannel, timeout: parseInt(e.target.value) || 30000 })}
631
+ className="w-full bg-gray-700 text-white p-2 rounded text-sm"
632
+ min="1000"
633
+ max="120000"
634
+ />
635
+ </div>
636
+
637
+ <div className="space-y-2">
638
+ <label className="flex items-center space-x-2">
639
+ <input
640
+ type="checkbox"
641
+ checked={editingChannel.isDefault || false}
642
+ onChange={(e) => setEditingChannel({ ...editingChannel, isDefault: e.target.checked })}
643
+ className="rounded"
644
+ />
645
+ <span className="text-gray-300 text-sm">设为默认渠道</span>
646
+ </label>
647
+ </div>
648
+
649
+ <div className="flex justify-end space-x-2 pt-4">
650
+ <button
651
+ onClick={() => {
652
+ setEditingChannel(null);
653
+ setValidationErrors([]);
654
+ }}
655
+ className="px-4 py-2 bg-gray-600 hover:bg-gray-500 text-white rounded text-sm"
656
+ >
657
+ 取消
658
+ </button>
659
+ <button
660
+ onClick={handleSaveChannel}
661
+ className="px-4 py-2 bg-sky-600 hover:bg-sky-700 text-white rounded text-sm flex items-center space-x-1"
662
+ >
663
+ <Save size={16} />
664
+ <span>保存</span>
665
+ </button>
666
+ </div>
667
+ </div>
668
+ </div>
669
+ ) : (
670
+ <div className="p-4 h-full flex items-center justify-center">
671
+ <p className="text-gray-400">选择一个渠道进行编辑,或添加新渠道</p>
672
+ </div>
673
+ )}
674
+ </div>
675
+ </div>
676
+ ) : activeTab === 'models' ? (
677
+ <div className="h-full flex">
678
+ {/* Models List */}
679
+ <div className="w-1/2 border-r border-gray-700 overflow-y-auto">
680
+ <div className="p-4">
681
+ <div className="flex justify-between items-center mb-4">
682
+ <h3 className="text-lg font-medium text-white">模型列表</h3>
683
+ <button
684
+ onClick={() => setEditingModel({
685
+ isNew: true,
686
+ supportsImages: true,
687
+ supportsReducedCapacity: true,
688
+ category: 'GPT-4系列',
689
+ maxTokens: 4096,
690
+ temperature: 0.7,
691
+ channelId: channels.length > 0 ? channels[0].id : ''
692
+ })}
693
+ className="px-3 py-1 bg-sky-600 hover:bg-sky-700 text-white rounded text-sm flex items-center space-x-1"
694
+ >
695
+ <Plus size={16} />
696
+ <span>添加模型</span>
697
+ </button>
698
+ </div>
699
+
700
+ <div className="space-y-2">
701
+ {models.map((model) => {
702
+ const channel = channels.find(ch => ch.id === model.channelId);
703
+ return (
704
+ <div key={model.id} className="bg-gray-700 rounded p-3">
705
+ <div className="flex justify-between items-start mb-2">
706
+ <div>
707
+ <h4 className="text-white font-medium">{model.name}</h4>
708
+ <p className="text-gray-400 text-sm">{model.apiName}</p>
709
+ <p className="text-gray-500 text-xs">{model.category}</p>
710
+ <p className="text-gray-500 text-xs">
711
+ 渠道: {channel?.name || '未找到渠道'}
712
+ </p>
713
+ </div>
714
+ <div className="flex space-x-1">
715
+ <button
716
+ onClick={() => setEditingModel(model)}
717
+ className="p-1 text-gray-400 hover:text-sky-400"
718
+ title="编辑"
719
+ >
720
+ <Edit3 size={16} />
721
+ </button>
722
+ {model.isCustom && (
723
+ <button
724
+ onClick={() => handleDeleteModel(model.id)}
725
+ className="p-1 text-gray-400 hover:text-red-400"
726
+ title="删除"
727
+ >
728
+ <Trash2 size={16} />
729
+ </button>
730
+ )}
731
+ </div>
732
+ </div>
733
+ <div className="flex space-x-2">
734
+ {model.supportsImages && (
735
+ <span className="text-xs bg-green-600 text-white px-2 py-1 rounded flex items-center space-x-1">
736
+ <Image size={12} />
737
+ <span>图像</span>
738
+ </span>
739
+ )}
740
+ {model.supportsReducedCapacity && (
741
+ <span className="text-xs bg-blue-600 text-white px-2 py-1 rounded flex items-center space-x-1">
742
+ <Zap size={12} />
743
+ <span>优化</span>
744
+ </span>
745
+ )}
746
+ </div>
747
+ </div>
748
+ );
749
+ })}
750
+ </div>
751
+ </div>
752
+ </div>
753
+
754
+ {/* Model Editor */}
755
+ <div className="w-1/2 overflow-y-auto">
756
+ {editingModel ? (
757
+ <div className="p-4">
758
+ <h3 className="text-lg font-medium text-white mb-4">
759
+ {editingModel.isNew ? '添加新模型' : '编辑模型'}
760
+ </h3>
761
+
762
+ {validationErrors.length > 0 && (
763
+ <div className="mb-4 p-3 bg-red-600 rounded">
764
+ <div className="flex items-center space-x-2 mb-2">
765
+ <AlertCircle size={16} className="text-white" />
766
+ <span className="text-white font-medium">配置错误</span>
767
+ </div>
768
+ {validationErrors.map((error, index) => (
769
+ <p key={index} className="text-white text-sm">{error}</p>
770
+ ))}
771
+ </div>
772
+ )}
773
+
774
+ <div className="space-y-4">
775
+ <div>
776
+ <label className="block text-gray-300 text-sm mb-1">显示名称</label>
777
+ <input
778
+ type="text"
779
+ value={editingModel.name || ''}
780
+ onChange={(e) => setEditingModel({ ...editingModel, name: e.target.value })}
781
+ className="w-full bg-gray-700 text-white p-2 rounded text-sm"
782
+ placeholder="例如: GPT-4 Turbo"
783
+ />
784
+ </div>
785
+
786
+ <div>
787
+ <label className="block text-gray-300 text-sm mb-1">API模型名称</label>
788
+ <input
789
+ type="text"
790
+ value={editingModel.apiName || ''}
791
+ onChange={(e) => setEditingModel({ ...editingModel, apiName: e.target.value })}
792
+ className="w-full bg-gray-700 text-white p-2 rounded text-sm"
793
+ placeholder="例如: gpt-4-turbo"
794
+ />
795
+ </div>
796
+
797
+ <div>
798
+ <label className="block text-gray-300 text-sm mb-1">API渠道</label>
799
+ <select
800
+ value={editingModel.channelId || ''}
801
+ onChange={(e) => setEditingModel({ ...editingModel, channelId: e.target.value })}
802
+ className="w-full bg-gray-700 text-white p-2 rounded text-sm"
803
+ >
804
+ <option value="">选择渠道</option>
805
+ {channels.map((channel) => (
806
+ <option key={channel.id} value={channel.id}>
807
+ {channel.name}
808
+ </option>
809
+ ))}
810
+ </select>
811
+ </div>
812
+
813
+ <div>
814
+ <label className="block text-gray-300 text-sm mb-1">模型类别</label>
815
+ <input
816
+ type="text"
817
+ value={editingModel.category || ''}
818
+ onChange={(e) => setEditingModel({ ...editingModel, category: e.target.value })}
819
+ className="w-full bg-gray-700 text-white p-2 rounded text-sm"
820
+ placeholder="例如: GPT-4系列"
821
+ />
822
+ </div>
823
+
824
+ <div className="grid grid-cols-2 gap-4">
825
+ <div>
826
+ <label className="block text-gray-300 text-sm mb-1">最大Token数</label>
827
+ <input
828
+ type="number"
829
+ value={editingModel.maxTokens || 4096}
830
+ onChange={(e) => setEditingModel({ ...editingModel, maxTokens: parseInt(e.target.value) || 4096 })}
831
+ className="w-full bg-gray-700 text-white p-2 rounded text-sm"
832
+ min="1"
833
+ max="32768"
834
+ />
835
+ </div>
836
+
837
+ <div>
838
+ <label className="block text-gray-300 text-sm mb-1">默认温度</label>
839
+ <input
840
+ type="number"
841
+ value={editingModel.temperature || 0.7}
842
+ onChange={(e) => setEditingModel({ ...editingModel, temperature: parseFloat(e.target.value) || 0.7 })}
843
+ className="w-full bg-gray-700 text-white p-2 rounded text-sm"
844
+ min="0"
845
+ max="2"
846
+ step="0.1"
847
+ />
848
+ </div>
849
+ </div>
850
+
851
+ <div className="space-y-2">
852
+ <label className="flex items-center space-x-2">
853
+ <input
854
+ type="checkbox"
855
+ checked={editingModel.supportsImages || false}
856
+ onChange={(e) => setEditingModel({ ...editingModel, supportsImages: e.target.checked })}
857
+ className="rounded"
858
+ />
859
+ <span className="text-gray-300 text-sm">支持图像处理</span>
860
+ </label>
861
+
862
+ <label className="flex items-center space-x-2">
863
+ <input
864
+ type="checkbox"
865
+ checked={editingModel.supportsReducedCapacity || false}
866
+ onChange={(e) => setEditingModel({ ...editingModel, supportsReducedCapacity: e.target.checked })}
867
+ className="rounded"
868
+ />
869
+ <span className="text-gray-300 text-sm">支持性能优化模式</span>
870
+ </label>
871
+ </div>
872
+
873
+ <div className="flex justify-end space-x-2 pt-4">
874
+ <button
875
+ onClick={() => {
876
+ setEditingModel(null);
877
+ setValidationErrors([]);
878
+ }}
879
+ className="px-4 py-2 bg-gray-600 hover:bg-gray-500 text-white rounded text-sm"
880
+ >
881
+ 取消
882
+ </button>
883
+ <button
884
+ onClick={handleSaveModel}
885
+ className="px-4 py-2 bg-sky-600 hover:bg-sky-700 text-white rounded text-sm flex items-center space-x-1"
886
+ >
887
+ <Save size={16} />
888
+ <span>保存</span>
889
+ </button>
890
+ </div>
891
+ </div>
892
+ </div>
893
+ ) : (
894
+ <div className="p-4 h-full flex items-center justify-center">
895
+ <p className="text-gray-400">选择一个模型进行编辑,或添加新模型</p>
896
+ </div>
897
+ )}
898
+ </div>
899
+ </div>
900
+ ) : activeTab === 'roles' ? (
901
+ <div className="h-full flex">
902
+ {/* Roles List */}
903
+ <div className="w-1/2 border-r border-gray-700 overflow-y-auto">
904
+ <div className="p-4">
905
+ <div className="flex justify-between items-center mb-4">
906
+ <h3 className="text-lg font-medium text-white">角色列表</h3>
907
+ <button
908
+ onClick={() => setEditingRole({
909
+ isNew: true,
910
+ isActive: true,
911
+ modelId: models.length > 0 ? models[0].id : ''
912
+ })}
913
+ className="px-3 py-1 bg-sky-600 hover:bg-sky-700 text-white rounded text-sm flex items-center space-x-1"
914
+ >
915
+ <Plus size={16} />
916
+ <span>添加角色</span>
917
+ </button>
918
+ </div>
919
+
920
+ <div className="space-y-2">
921
+ {roles.map((role) => {
922
+ const associatedModel = models.find(m => m.id === role.modelId);
923
+ const associatedChannel = associatedModel ? channels.find(ch => ch.id === associatedModel.channelId) : null;
924
+ return (
925
+ <div key={role.id} className="bg-gray-700 rounded p-3">
926
+ <div className="flex justify-between items-start mb-2">
927
+ <div>
928
+ <h4 className="text-white font-medium flex items-center space-x-2">
929
+ <span>{role.name}</span>
930
+ {role.isActive && (
931
+ <span className="text-xs bg-green-600 text-white px-2 py-1 rounded">活跃</span>
932
+ )}
933
+ </h4>
934
+ <p className="text-gray-400 text-sm">
935
+ 模型: {associatedModel?.name || '未找到模型'}
936
+ </p>
937
+ <p className="text-gray-500 text-xs">
938
+ 渠道: {associatedChannel?.name || '未找到渠道'}
939
+ </p>
940
+ </div>
941
+ <div className="flex space-x-1">
942
+ <button
943
+ onClick={() => setEditingRole(role)}
944
+ className="p-1 text-gray-400 hover:text-sky-400"
945
+ title="编辑"
946
+ >
947
+ <Edit3 size={16} />
948
+ </button>
949
+ <button
950
+ onClick={() => handleDeleteRole(role.id)}
951
+ className="p-1 text-gray-400 hover:text-red-400"
952
+ title="删除"
953
+ >
954
+ <Trash2 size={16} />
955
+ </button>
956
+ </div>
957
+ </div>
958
+ <p className="text-gray-500 text-xs line-clamp-2">
959
+ {role.systemPrompt}
960
+ </p>
961
+ </div>
962
+ );
963
+ })}
964
+ </div>
965
+ </div>
966
+ </div>
967
+
968
+ {/* Role Editor */}
969
+ <div className="w-1/2 overflow-y-auto">
970
+ {editingRole ? (
971
+ <div className="p-4">
972
+ <h3 className="text-lg font-medium text-white mb-4">
973
+ {editingRole.isNew ? '添加新角色' : '编辑角色'}
974
+ </h3>
975
+
976
+ {validationErrors.length > 0 && (
977
+ <div className="mb-4 p-3 bg-red-600 rounded">
978
+ <div className="flex items-center space-x-2 mb-2">
979
+ <AlertCircle size={16} className="text-white" />
980
+ <span className="text-white font-medium">配置错误</span>
981
+ </div>
982
+ {validationErrors.map((error, index) => (
983
+ <p key={index} className="text-white text-sm">{error}</p>
984
+ ))}
985
+ </div>
986
+ )}
987
+
988
+ <div className="space-y-4">
989
+ <div>
990
+ <label className="block text-gray-300 text-sm mb-1">角色名称</label>
991
+ <input
992
+ type="text"
993
+ value={editingRole.name || ''}
994
+ onChange={(e) => setEditingRole({ ...editingRole, name: e.target.value })}
995
+ className="w-full bg-gray-700 text-white p-2 rounded text-sm"
996
+ placeholder="例如: 分析师"
997
+ />
998
+ </div>
999
+
1000
+ <div>
1001
+ <label className="block text-gray-300 text-sm mb-1">关联模型</label>
1002
+ <select
1003
+ value={editingRole.modelId || ''}
1004
+ onChange={(e) => setEditingRole({ ...editingRole, modelId: e.target.value })}
1005
+ className="w-full bg-gray-700 text-white p-2 rounded text-sm"
1006
+ >
1007
+ <option value="">选择模型</option>
1008
+ {models.map((model) => {
1009
+ const channel = channels.find(ch => ch.id === model.channelId);
1010
+ return (
1011
+ <option key={model.id} value={model.id}>
1012
+ {model.name} ({channel?.name || '未知渠道'})
1013
+ </option>
1014
+ );
1015
+ })}
1016
+ </select>
1017
+ </div>
1018
+
1019
+ <div>
1020
+ <label className="block text-gray-300 text-sm mb-1">系统提示词</label>
1021
+ <textarea
1022
+ value={editingRole.systemPrompt || ''}
1023
+ onChange={(e) => setEditingRole({ ...editingRole, systemPrompt: e.target.value })}
1024
+ className="w-full bg-gray-700 text-white p-2 rounded text-sm h-32"
1025
+ placeholder="定义AI角色的行为和特性..."
1026
+ />
1027
+ </div>
1028
+
1029
+ <div>
1030
+ <label className="flex items-center space-x-2">
1031
+ <input
1032
+ type="checkbox"
1033
+ checked={editingRole.isActive || false}
1034
+ onChange={(e) => setEditingRole({ ...editingRole, isActive: e.target.checked })}
1035
+ className="rounded"
1036
+ />
1037
+ <span className="text-gray-300 text-sm">激活此角色</span>
1038
+ </label>
1039
+ </div>
1040
+
1041
+ <div className="flex justify-end space-x-2 pt-4">
1042
+ <button
1043
+ onClick={() => {
1044
+ setEditingRole(null);
1045
+ setValidationErrors([]);
1046
+ }}
1047
+ className="px-4 py-2 bg-gray-600 hover:bg-gray-500 text-white rounded text-sm"
1048
+ >
1049
+ 取消
1050
+ </button>
1051
+ <button
1052
+ onClick={handleSaveRole}
1053
+ className="px-4 py-2 bg-sky-600 hover:bg-sky-700 text-white rounded text-sm flex items-center space-x-1"
1054
+ >
1055
+ <Save size={16} />
1056
+ <span>保存</span>
1057
+ </button>
1058
+ </div>
1059
+ </div>
1060
+ </div>
1061
+ ) : (
1062
+ <div className="p-4 h-full flex items-center justify-center">
1063
+ <p className="text-gray-400">选择一个角色进行编辑,或添加新角色</p>
1064
+ </div>
1065
+ )}
1066
+ </div>
1067
+ </div>
1068
+ ) : (
1069
+ // Storage Management Tab
1070
+ <div className="p-6">
1071
+ <h3 className="text-lg font-medium text-white mb-6">存储管理</h3>
1072
+
1073
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
1074
+ <div className="bg-gray-700 rounded-lg p-4">
1075
+ <h4 className="text-white font-medium mb-3">存储使用情况</h4>
1076
+ <div className="space-y-2">
1077
+ <div className="flex justify-between text-sm">
1078
+ <span className="text-gray-300">已使用:</span>
1079
+ <span className="text-white">{(storageInfo.used / 1024).toFixed(2)} KB</span>
1080
+ </div>
1081
+ <div className="flex justify-between text-sm">
1082
+ <span className="text-gray-300">可用:</span>
1083
+ <span className="text-white">{(storageInfo.available / 1024).toFixed(2)} KB</span>
1084
+ </div>
1085
+ <div className="w-full bg-gray-600 rounded-full h-2 mt-2">
1086
+ <div
1087
+ className="bg-sky-500 h-2 rounded-full transition-all duration-300"
1088
+ style={{ width: `${Math.min((storageInfo.used / (storageInfo.used + storageInfo.available)) * 100, 100)}%` }}
1089
+ ></div>
1090
+ </div>
1091
+ </div>
1092
+ </div>
1093
+
1094
+ <div className="bg-gray-700 rounded-lg p-4">
1095
+ <h4 className="text-white font-medium mb-3">配置统计</h4>
1096
+ <div className="space-y-2">
1097
+ <div className="flex justify-between text-sm">
1098
+ <span className="text-gray-300">API渠道:</span>
1099
+ <span className="text-white">{storageInfo.channels}</span>
1100
+ </div>
1101
+ <div className="flex justify-between text-sm">
1102
+ <span className="text-gray-300">模型数量:</span>
1103
+ <span className="text-white">{storageInfo.models}</span>
1104
+ </div>
1105
+ <div className="flex justify-between text-sm">
1106
+ <span className="text-gray-300">角色数量:</span>
1107
+ <span className="text-white">{storageInfo.roles}</span>
1108
+ </div>
1109
+ </div>
1110
+ </div>
1111
+ </div>
1112
+
1113
+ <div className="mt-6 bg-gray-700 rounded-lg p-4">
1114
+ <h4 className="text-white font-medium mb-3">数据管理</h4>
1115
+ <div className="space-y-3">
1116
+ <p className="text-gray-300 text-sm">
1117
+ 所有配置数据存储在浏览器的localStorage中。清除浏览器数据可能会导致配置丢失。建议定期导出配置进行备份。
1118
+ </p>
1119
+ <div className="flex space-x-3">
1120
+ <button
1121
+ onClick={handleReset}
1122
+ className="px-4 py-2 bg-orange-600 hover:bg-orange-700 text-white rounded text-sm"
1123
+ >
1124
+ 重置为默认配置
1125
+ </button>
1126
+ <button
1127
+ onClick={handleClearAllData}
1128
+ className="px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded text-sm"
1129
+ >
1130
+ 清空所有数据
1131
+ </button>
1132
+ </div>
1133
+ </div>
1134
+ </div>
1135
+ </div>
1136
+ )}
1137
+ </div>
1138
+ </div>
1139
+ </div>
1140
+ );
1141
+ };
1142
+
1143
+ export default ModelConfigManagerComponent;
components/Notepad.tsx ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useMemo } from 'react';
2
+ import { MessageSender } from '../types';
3
+ import { FileText, Edit3, Eye, Code, Copy, Check } from 'lucide-react';
4
+ import { marked } from 'marked';
5
+ import DOMPurify from 'dompurify';
6
+
7
+ interface NotepadProps {
8
+ content: string;
9
+ lastUpdatedBy?: MessageSender | null;
10
+ isLoading: boolean;
11
+ }
12
+
13
+ const Notepad: React.FC<NotepadProps> = ({ content, lastUpdatedBy, isLoading }) => {
14
+ const [isPreviewMode, setIsPreviewMode] = useState(false);
15
+ const [isCopied, setIsCopied] = useState(false);
16
+
17
+ const getSenderColor = (sender?: MessageSender | null) => {
18
+ if (sender === MessageSender.Cognito) return 'text-green-400';
19
+ if (sender === MessageSender.Muse) return 'text-purple-400';
20
+ return 'text-gray-400';
21
+ };
22
+
23
+ const processedHtml = useMemo(() => {
24
+ if (isPreviewMode) {
25
+ const rawHtml = marked.parse(content) as string;
26
+ return DOMPurify.sanitize(rawHtml);
27
+ }
28
+ return '';
29
+ }, [content, isPreviewMode]);
30
+
31
+ const handleCopyNotepad = async () => {
32
+ try {
33
+ await navigator.clipboard.writeText(content);
34
+ setIsCopied(true);
35
+ setTimeout(() => setIsCopied(false), 2000);
36
+ } catch (err) {
37
+ console.error('无法复制记事本内容: ', err);
38
+ // Optionally, display an error message to the user
39
+ }
40
+ };
41
+
42
+ return (
43
+ <div className="h-full flex flex-col bg-slate-800 border-l border-gray-700">
44
+ <header className="p-3 border-b border-gray-700 flex items-center justify-between bg-slate-900">
45
+ <div className="flex items-center">
46
+ <FileText size={20} className="mr-2 text-sky-400" />
47
+ <h2 className="text-lg font-semibold text-sky-400">记事本</h2>
48
+ </div>
49
+ <div className="flex items-center space-x-2">
50
+ {isLoading && <span className="text-xs text-gray-400 italic">AI 思考中...</span>}
51
+ <button
52
+ onClick={handleCopyNotepad}
53
+ className="p-1.5 text-gray-400 hover:text-sky-400 transition-colors duration-150 focus:outline-none focus:ring-1 focus:ring-sky-500 rounded-md"
54
+ title={isCopied ? "已复制!" : "复制记事本内容"}
55
+ aria-label={isCopied ? "已复制记事本内容到剪贴板" : "复制记事本内容"}
56
+ >
57
+ {isCopied ? <Check size={18} className="text-green-400" /> : <Copy size={18} />}
58
+ </button>
59
+ <button
60
+ onClick={() => setIsPreviewMode(!isPreviewMode)}
61
+ className="p-1.5 text-gray-400 hover:text-sky-400 transition-colors duration-150 focus:outline-none focus:ring-1 focus:ring-sky-500 rounded-md"
62
+ title={isPreviewMode ? "查看原始内容" : "预览 Markdown"}
63
+ aria-label={isPreviewMode ? "Switch to raw text view" : "Switch to Markdown preview"}
64
+ >
65
+ {isPreviewMode ? <Code size={18} /> : <Eye size={18} />}
66
+ </button>
67
+ </div>
68
+ </header>
69
+ <div className="flex-grow overflow-y-auto relative">
70
+ {isPreviewMode ? (
71
+ <div
72
+ className="markdown-preview" // Styles defined in index.html
73
+ dangerouslySetInnerHTML={{ __html: processedHtml }}
74
+ aria-label="Markdown 预览"
75
+ />
76
+ ) : (
77
+ <textarea
78
+ readOnly
79
+ value={content}
80
+ className="w-full h-full p-3 bg-slate-800 text-gray-300 resize-none border-none focus:ring-0 font-mono text-sm leading-relaxed"
81
+ aria-label="共享记事本内容 (原始内容)"
82
+ />
83
+ )}
84
+ </div>
85
+ <footer className="p-2 border-t border-gray-700 text-xs text-gray-500 bg-slate-900">
86
+ {lastUpdatedBy ? (
87
+ <div className="flex items-center">
88
+ <Edit3 size={14} className={`mr-1.5 ${getSenderColor(lastUpdatedBy)}`} />
89
+ 最后更新者: <span className={`font-medium ml-1 ${getSenderColor(lastUpdatedBy)}`}>{lastUpdatedBy}</span>
90
+ </div>
91
+ ) : (
92
+ <span>记事本内容未被 AI 修改过。</span>
93
+ )}
94
+ </footer>
95
+ </div>
96
+ );
97
+ };
98
+
99
+ export default Notepad;
components/discussion-utils.ts ADDED
@@ -0,0 +1,354 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { DiscussionRecord, DiscussionStats, DiscussionExportData, ExportOptions, ChatMessage, MessagePurpose } from './types';
2
+
3
+ export class DiscussionRecordManager {
4
+ private static STORAGE_KEY = 'multi-mind-chat-discussion-records';
5
+ private static MAX_RECORDS = 50; // 最多保存50条记录
6
+
7
+ // 保存讨论记录到本地存储
8
+ static saveRecord(record: DiscussionRecord): void {
9
+ try {
10
+ const existingRecords = this.getAllRecords();
11
+ const updatedRecords = [record, ...existingRecords.filter(r => r.id !== record.id)];
12
+
13
+ // 保持最大记录数限制
14
+ const trimmedRecords = updatedRecords.slice(0, this.MAX_RECORDS);
15
+
16
+ localStorage.setItem(this.STORAGE_KEY, JSON.stringify(trimmedRecords));
17
+ } catch (error) {
18
+ console.error('保存讨论记录失败:', error);
19
+ throw new Error('无法保存讨论记录到本地存储');
20
+ }
21
+ }
22
+
23
+ // 获取所有讨论记录
24
+ static getAllRecords(): DiscussionRecord[] {
25
+ try {
26
+ const stored = localStorage.getItem(this.STORAGE_KEY);
27
+ if (!stored) return [];
28
+
29
+ const records = JSON.parse(stored);
30
+ return records.map((record: any) => ({
31
+ ...record,
32
+ timestamp: new Date(record.timestamp),
33
+ turns: record.turns.map((turn: any) => ({
34
+ ...turn,
35
+ timestamp: new Date(turn.timestamp)
36
+ })),
37
+ notepadUpdates: record.notepadUpdates.map((update: any) => ({
38
+ ...update,
39
+ timestamp: new Date(update.timestamp)
40
+ })),
41
+ finalAnswer: record.finalAnswer ? {
42
+ ...record.finalAnswer,
43
+ timestamp: new Date(record.finalAnswer.timestamp)
44
+ } : undefined,
45
+ interruptedAt: record.interruptedAt ? new Date(record.interruptedAt) : undefined,
46
+ metadata: {
47
+ ...record.metadata,
48
+ exportedAt: record.metadata.exportedAt ? new Date(record.metadata.exportedAt) : undefined
49
+ }
50
+ }));
51
+ } catch (error) {
52
+ console.error('加载讨论记录失败:', error);
53
+ return [];
54
+ }
55
+ }
56
+
57
+ // 根据ID获取单个记录
58
+ static getRecordById(id: string): DiscussionRecord | null {
59
+ const records = this.getAllRecords();
60
+ return records.find(record => record.id === id) || null;
61
+ }
62
+
63
+ // 删除指定记录
64
+ static deleteRecord(id: string): void {
65
+ try {
66
+ const records = this.getAllRecords();
67
+ const filteredRecords = records.filter(record => record.id !== id);
68
+ localStorage.setItem(this.STORAGE_KEY, JSON.stringify(filteredRecords));
69
+ } catch (error) {
70
+ console.error('删除讨论记录失败:', error);
71
+ throw new Error('无法删除讨论记录');
72
+ }
73
+ }
74
+
75
+ // 清空所有记录
76
+ static clearAllRecords(): void {
77
+ try {
78
+ localStorage.removeItem(this.STORAGE_KEY);
79
+ } catch (error) {
80
+ console.error('清空讨论记录失败:', error);
81
+ throw new Error('无法清空讨论记录');
82
+ }
83
+ }
84
+
85
+ // 计算讨论统计信息
86
+ static calculateStats(record: DiscussionRecord): DiscussionStats {
87
+ const turns = record.turns.filter(turn => turn.durationMs && turn.durationMs > 0);
88
+ const responseTimes = turns.map(turn => turn.durationMs!);
89
+
90
+ const roleParticipation: Record<string, any> = {};
91
+
92
+ // 统计每个角色的参与情况
93
+ record.activeRoles.forEach(role => {
94
+ const roleTurns = turns.filter(turn => turn.roleId === role.id);
95
+ const roleResponseTimes = roleTurns.map(turn => turn.durationMs!);
96
+
97
+ roleParticipation[role.name] = {
98
+ turnCount: roleTurns.length,
99
+ totalResponseTime: roleResponseTimes.reduce((sum, time) => sum + time, 0),
100
+ averageResponseTime: roleResponseTimes.length > 0
101
+ ? roleResponseTimes.reduce((sum, time) => sum + time, 0) / roleResponseTimes.length
102
+ : 0
103
+ };
104
+ });
105
+
106
+ return {
107
+ totalTurns: turns.length,
108
+ averageResponseTime: responseTimes.length > 0
109
+ ? responseTimes.reduce((sum, time) => sum + time, 0) / responseTimes.length
110
+ : 0,
111
+ longestResponseTime: responseTimes.length > 0 ? Math.max(...responseTimes) : 0,
112
+ shortestResponseTime: responseTimes.length > 0 ? Math.min(...responseTimes) : 0,
113
+ roleParticipation,
114
+ notepadUpdateFrequency: record.notepadUpdates.length / Math.max(turns.length, 1)
115
+ };
116
+ }
117
+
118
+ // 生成完整的讨论文本记录
119
+ static generateTranscript(record: DiscussionRecord, includeMetadata: boolean = true): string {
120
+ let transcript = '';
121
+
122
+ if (includeMetadata) {
123
+ transcript += `=== Multi-Mind Chat 讨论记录 ===\n`;
124
+ transcript += `讨论ID: ${record.id}\n`;
125
+ transcript += `开始时间: ${record.timestamp.toLocaleString()}\n`;
126
+ transcript += `讨论模式: ${record.discussionMode}\n`;
127
+ transcript += `参与角色: ${record.activeRoles.map(r => r.name).join(', ')}\n`;
128
+ transcript += `总耗时: ${(record.totalDuration / 1000).toFixed(2)}秒\n`;
129
+
130
+ if (record.wasInterrupted) {
131
+ transcript += `状态: 被用户中断 (${record.interruptedAt?.toLocaleString()})\n`;
132
+ } else if (record.isCompleted) {
133
+ transcript += `状态: 正常完成\n`;
134
+ }
135
+
136
+ transcript += `\n=== 用户查询 ===\n`;
137
+ transcript += `${record.userQuery}\n\n`;
138
+ }
139
+
140
+ // 添加讨论过程
141
+ transcript += `=== 讨论过程 ===\n`;
142
+ record.turns.forEach((turn, index) => {
143
+ const timeStr = turn.timestamp.toLocaleTimeString();
144
+ const durationStr = turn.durationMs ? ` (${(turn.durationMs / 1000).toFixed(2)}s)` : '';
145
+ transcript += `[${timeStr}] ${turn.role}${durationStr}:\n${turn.message}\n\n`;
146
+ });
147
+
148
+ // 添加记事本更新历史
149
+ if (record.notepadUpdates.length > 0) {
150
+ transcript += `=== 记事本更新历史 ===\n`;
151
+ record.notepadUpdates.forEach((update, index) => {
152
+ const timeStr = update.timestamp.toLocaleTimeString();
153
+ transcript += `[${timeStr}] ${update.updater} 更新了记事本:\n${update.content}\n\n`;
154
+ });
155
+ }
156
+
157
+ // 添加最终答案
158
+ if (record.finalAnswer) {
159
+ transcript += `=== 最终答案 ===\n`;
160
+ const timeStr = record.finalAnswer.timestamp.toLocaleTimeString();
161
+ const durationStr = record.finalAnswer.durationMs ? ` (${(record.finalAnswer.durationMs / 1000).toFixed(2)}s)` : '';
162
+ transcript += `[${timeStr}] ${record.finalAnswer.provider}${durationStr}:\n${record.finalAnswer.content}\n\n`;
163
+ }
164
+
165
+ return transcript;
166
+ }
167
+
168
+ // 导出讨论记录为指定格式
169
+ static exportRecord(record: DiscussionRecord, options: ExportOptions): DiscussionExportData {
170
+ const stats = options.includeStats ? this.calculateStats(record) : {} as DiscussionStats;
171
+ const transcript = this.generateTranscript(record, options.includeMetadata);
172
+
173
+ return {
174
+ record: options.includeMetadata ? record : {
175
+ ...record,
176
+ metadata: { version: record.metadata.version, messageCount: 0, notepadUpdateCount: 0 }
177
+ } as DiscussionRecord,
178
+ stats,
179
+ fullTranscript: transcript,
180
+ exportFormat: options.format,
181
+ exportedAt: new Date().toISOString(),
182
+ version: '1.0'
183
+ };
184
+ }
185
+
186
+ // 生成Markdown格式的导出
187
+ static exportAsMarkdown(record: DiscussionRecord, options: ExportOptions): string {
188
+ let markdown = `# Multi-Mind Chat 讨论记录\n\n`;
189
+
190
+ if (options.includeMetadata) {
191
+ markdown += `## 基本信息\n\n`;
192
+ markdown += `- **讨论ID**: ${record.id}\n`;
193
+ markdown += `- **开始时间**: ${record.timestamp.toLocaleString()}\n`;
194
+ markdown += `- **讨论模式**: ${record.discussionMode}\n`;
195
+ markdown += `- **参与角色**: ${record.activeRoles.map(r => r.name).join(', ')}\n`;
196
+ markdown += `- **总耗时**: ${(record.totalDuration / 1000).toFixed(2)}秒\n`;
197
+
198
+ if (record.wasInterrupted) {
199
+ markdown += `- **状态**: ⚠️ 被用户中断 (${record.interruptedAt?.toLocaleString()})\n`;
200
+ } else if (record.isCompleted) {
201
+ markdown += `- **状态**: ✅ 正常完成\n`;
202
+ }
203
+
204
+ markdown += `\n`;
205
+ }
206
+
207
+ markdown += `## 用户查询\n\n`;
208
+ markdown += `> ${record.userQuery}\n\n`;
209
+
210
+ if (record.userImage) {
211
+ markdown += `*用户还上传了图片: ${record.userImage.name} (${(record.userImage.size / 1024).toFixed(1)} KB)*\n\n`;
212
+ }
213
+
214
+ markdown += `## 讨论过程\n\n`;
215
+ record.turns.forEach((turn, index) => {
216
+ const timeStr = turn.timestamp.toLocaleTimeString();
217
+ const durationStr = turn.durationMs ? ` *(${(turn.durationMs / 1000).toFixed(2)}s)*` : '';
218
+ markdown += `### ${turn.role} - ${timeStr}${durationStr}\n\n`;
219
+ markdown += `${turn.message}\n\n`;
220
+ });
221
+
222
+ if (record.notepadUpdates.length > 0 && options.includeNotepadHistory) {
223
+ markdown += `## 记事本更新历史\n\n`;
224
+ record.notepadUpdates.forEach((update, index) => {
225
+ const timeStr = update.timestamp.toLocaleTimeString();
226
+ markdown += `### ${update.updater} - ${timeStr}\n\n`;
227
+ markdown += `\`\`\`\n${update.content}\n\`\`\`\n\n`;
228
+ });
229
+ }
230
+
231
+ if (record.finalAnswer) {
232
+ markdown += `## 最终答案\n\n`;
233
+ const timeStr = record.finalAnswer.timestamp.toLocaleTimeString();
234
+ const durationStr = record.finalAnswer.durationMs ? ` *(${(record.finalAnswer.durationMs / 1000).toFixed(2)}s)*` : '';
235
+ markdown += `### ${record.finalAnswer.provider} - ${timeStr}${durationStr}\n\n`;
236
+ markdown += `${record.finalAnswer.content}\n\n`;
237
+ }
238
+
239
+ if (options.includeStats) {
240
+ const stats = this.calculateStats(record);
241
+ markdown += `## 讨论统计\n\n`;
242
+ markdown += `- **总轮次**: ${stats.totalTurns}\n`;
243
+ markdown += `- **平均响应时间**: ${(stats.averageResponseTime / 1000).toFixed(2)}秒\n`;
244
+ markdown += `- **最长响应时间**: ${(stats.longestResponseTime / 1000).toFixed(2)}秒\n`;
245
+ markdown += `- **最短响应时间**: ${(stats.shortestResponseTime / 1000).toFixed(2)}秒\n`;
246
+ markdown += `- **记事本更新频率**: ${(stats.notepadUpdateFrequency * 100).toFixed(1)}%\n\n`;
247
+
248
+ markdown += `### 角色参与度\n\n`;
249
+ Object.entries(stats.roleParticipation).forEach(([role, data]) => {
250
+ markdown += `- **${role}**: ${data.turnCount}轮, 平均${(data.averageResponseTime / 1000).toFixed(2)}秒\n`;
251
+ });
252
+ }
253
+
254
+ markdown += `\n---\n`;
255
+ markdown += `*导出时间: ${new Date().toLocaleString()}*\n`;
256
+ markdown += `*导出版本: Multi-Mind Chat v1.0*\n`;
257
+
258
+ return markdown;
259
+ }
260
+
261
+ // 下载文件的工具函数
262
+ static downloadFile(content: string, filename: string, mimeType: string = 'text/plain'): void {
263
+ const blob = new Blob([content], { type: mimeType });
264
+ const url = URL.createObjectURL(blob);
265
+ const link = document.createElement('a');
266
+ link.href = url;
267
+ link.download = filename;
268
+ document.body.appendChild(link);
269
+ link.click();
270
+ document.body.removeChild(link);
271
+ URL.revokeObjectURL(url);
272
+ }
273
+
274
+ // 导出并下载讨论记录
275
+ static downloadRecord(record: DiscussionRecord, format: 'json' | 'markdown' | 'txt' = 'json'): void {
276
+ const timestamp = record.timestamp.toISOString().split('T')[0];
277
+ const safeQuery = record.userQuery.substring(0, 20).replace(/[^\w\s-]/g, '').trim();
278
+
279
+ switch (format) {
280
+ case 'json':
281
+ const exportData = this.exportRecord(record, {
282
+ format: 'json',
283
+ includeMetadata: true,
284
+ includeStats: true,
285
+ includeNotepadHistory: true,
286
+ includeSystemMessages: false,
287
+ timestampFormat: 'iso',
288
+ compressOutput: false
289
+ });
290
+ this.downloadFile(
291
+ JSON.stringify(exportData, null, 2),
292
+ `讨论记录-${timestamp}-${safeQuery}.json`,
293
+ 'application/json'
294
+ );
295
+ break;
296
+
297
+ case 'markdown':
298
+ const markdownContent = this.exportAsMarkdown(record, {
299
+ format: 'markdown',
300
+ includeMetadata: true,
301
+ includeStats: true,
302
+ includeNotepadHistory: true,
303
+ includeSystemMessages: false,
304
+ timestampFormat: 'local',
305
+ compressOutput: false
306
+ });
307
+ this.downloadFile(
308
+ markdownContent,
309
+ `讨论记录-${timestamp}-${safeQuery}.md`,
310
+ 'text/markdown'
311
+ );
312
+ break;
313
+
314
+ case 'txt':
315
+ const txtContent = this.generateTranscript(record, true);
316
+ this.downloadFile(
317
+ txtContent,
318
+ `讨论记录-${timestamp}-${safeQuery}.txt`,
319
+ 'text/plain'
320
+ );
321
+ break;
322
+ }
323
+ }
324
+
325
+ // 搜索讨论记录
326
+ static searchRecords(query: string, maxResults: number = 10): DiscussionRecord[] {
327
+ const records = this.getAllRecords();
328
+ const searchLower = query.toLowerCase();
329
+
330
+ return records
331
+ .filter(record =>
332
+ record.userQuery.toLowerCase().includes(searchLower) ||
333
+ record.turns.some(turn => turn.message.toLowerCase().includes(searchLower)) ||
334
+ record.finalAnswer?.content.toLowerCase().includes(searchLower)
335
+ )
336
+ .slice(0, maxResults);
337
+ }
338
+
339
+ // 获取存储使用情况
340
+ static getStorageInfo(): { used: number; available: number; recordCount: number } {
341
+ try {
342
+ const records = this.getAllRecords();
343
+ const dataSize = JSON.stringify(records).length;
344
+
345
+ return {
346
+ used: dataSize,
347
+ available: 5242880 - dataSize, // 假设5MB存储限制
348
+ recordCount: records.length
349
+ };
350
+ } catch (error) {
351
+ return { used: 0, available: 0, recordCount: 0 };
352
+ }
353
+ }
354
+ }
config/preset.ts ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // 预置配置管理
2
+ interface PresetConfig {
3
+ name: string;
4
+ baseUrl: string;
5
+ apiKey: string;
6
+ }
7
+
8
+ // 从环境变量读取预置配置
9
+ export function getPresetConfig(): PresetConfig | null {
10
+ // 在构建时从环境变量读取
11
+ const baseUrl = import.meta.env.VITE_PRESET_BASE_URL;
12
+ const apiKey = import.meta.env.VITE_PRESET_API_KEY;
13
+ const name = import.meta.env.VITE_PRESET_NAME || '快速体验渠道';
14
+
15
+ if (!baseUrl || !apiKey) {
16
+ return null;
17
+ }
18
+
19
+ return {
20
+ name,
21
+ baseUrl,
22
+ apiKey
23
+ };
24
+ }
25
+
26
+ // 检查是否有预置配置
27
+ export function hasPresetConfig(): boolean {
28
+ return getPresetConfig() !== null;
29
+ }
constants.ts ADDED
@@ -0,0 +1,637 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { getPresetConfig, hasPresetConfig } from './config/preset';
2
+
3
+ // API渠道配置接口
4
+ export interface ApiChannel {
5
+ id: string;
6
+ name: string;
7
+ baseUrl: string;
8
+ apiKey: string;
9
+ isDefault: boolean;
10
+ isCustom: boolean;
11
+ timeout?: number;
12
+ headers?: Record<string, string>;
13
+ description?: string;
14
+ createdAt: Date;
15
+ }
16
+
17
+ // 动态模型配置接口
18
+ export interface AiModel {
19
+ id: string;
20
+ name: string;
21
+ apiName: string;
22
+ channelId: string; // 关联的渠道ID
23
+ supportsImages: boolean;
24
+ supportsReducedCapacity: boolean;
25
+ category: string;
26
+ maxTokens: number;
27
+ temperature: number;
28
+ isCustom: boolean;
29
+ createdAt: Date;
30
+ }
31
+
32
+ // AI角色配置接口
33
+ export interface AiRole {
34
+ id: string;
35
+ name: string;
36
+ systemPrompt: string;
37
+ modelId: string;
38
+ isActive: boolean;
39
+ }
40
+
41
+ // 预置渠道ID常量
42
+ export const PRESET_CHANNEL_ID = 'system-preset-channel';
43
+
44
+ // 默认渠道配置
45
+ export const DEFAULT_CHANNELS: ApiChannel[] = [
46
+ {
47
+ id: 'openai-official',
48
+ name: 'OpenAI 官方',
49
+ baseUrl: 'https://api.openai.com/v1',
50
+ apiKey: '',
51
+ isDefault: false, // 注意这里改为false
52
+ isCustom: false,
53
+ timeout: 30000,
54
+ description: 'OpenAI 官方API服务',
55
+ createdAt: new Date()
56
+ }
57
+ ];
58
+
59
+ // 默认预设模型配置(仅一个,关联到预置渠道或默认渠道)
60
+ export const DEFAULT_MODELS: AiModel[] = [
61
+ {
62
+ id: 'gpt-4-mini-default',
63
+ name: 'GPT-4.1 Mini',
64
+ apiName: 'gpt-4.1-mini',
65
+ channelId: hasPresetConfig() ? PRESET_CHANNEL_ID : 'openai-official',
66
+ supportsImages: true,
67
+ supportsReducedCapacity: true,
68
+ category: 'GPT-4系列',
69
+ maxTokens: 16384,
70
+ temperature: 0.7,
71
+ isCustom: false,
72
+ createdAt: new Date()
73
+ }
74
+ ];
75
+
76
+ // 默认角色配置 - 使用中文系统提示词并明确身份认知
77
+ export const DEFAULT_ROLES: AiRole[] = [
78
+ {
79
+ id: 'cognito-default',
80
+ name: 'Cognito',
81
+ systemPrompt: `你是Cognito,一位严谨的逻辑分析师AI助手。只有你叫Cognito这个名字,你的独特特征包括:
82
+ - 系统性思维和结构化分析
83
+ - 注重数据、事实和逻辑推理
84
+ - 追求准确性和客观性
85
+ - 善于发现问题的核心和关键要素
86
+
87
+ 在多AI协作讨论环境中,你与其他AI角色平等协作,各自发挥专长。请始终:
88
+ 1. 用中文进行所有回应
89
+ 2. 以你的名字Cognito的身份进行思考和回应
90
+ 3. 与其他AI角色进行建设性对话,避免使用"你们"等不当称呼
91
+ 4. 发挥你的逻辑分析专长,为讨论提供理性和系统化的观点
92
+
93
+ 记住:你是Cognito,独一无二的逻辑分析师。`,
94
+ modelId: 'gpt-4-mini-default',
95
+ isActive: true
96
+ },
97
+ {
98
+ id: 'muse-default',
99
+ name: 'Muse',
100
+ systemPrompt: `你是Muse,一位富有创意的思考家AI助手。只有你叫Muse这个名字,你的独特特征包括:
101
+ - 发散性思维和创新视角
102
+ - 善于联想、类比和跨领域思考
103
+ - 关注人文情怀和情感层面
104
+ - 能够从不同角度审视问题
105
+
106
+ 在多AI协作讨论环境中,你与其他AI角色平等协作,各自发挥专长。请始终:
107
+ 1. 用中文进行所有回应
108
+ 2. 以你的名字Muse的身份进行思考和回应
109
+ 3. 与其他AI角色进行建设性对话,避免使用"你们"等不当称呼
110
+ 4. 发挥你的创意思维专长,为讨论带来新颖和富有启发性的观点
111
+
112
+ 记住:你是Muse,独一无二的创意思考家。`,
113
+ modelId: 'gpt-4-mini-default',
114
+ isActive: true
115
+ }
116
+ ];
117
+
118
+ // 配置管理类
119
+ export class ModelConfigManager {
120
+ private static STORAGE_KEY_CHANNELS = 'multi-mind-chat-channels';
121
+ private static STORAGE_KEY_MODELS = 'multi-mind-chat-models';
122
+ private static STORAGE_KEY_ROLES = 'multi-mind-chat-roles';
123
+ private static STORAGE_KEY_INITIALIZED = 'multi-mind-chat-initialized';
124
+
125
+ // 初始化检查
126
+ private static ensureInitialized(): void {
127
+ try {
128
+ const isInitialized = localStorage.getItem(this.STORAGE_KEY_INITIALIZED);
129
+ if (!isInitialized) {
130
+ localStorage.setItem(this.STORAGE_KEY_CHANNELS, JSON.stringify(DEFAULT_CHANNELS));
131
+ localStorage.setItem(this.STORAGE_KEY_MODELS, JSON.stringify(DEFAULT_MODELS));
132
+ localStorage.setItem(this.STORAGE_KEY_ROLES, JSON.stringify(DEFAULT_ROLES));
133
+ localStorage.setItem(this.STORAGE_KEY_INITIALIZED, 'true');
134
+ }
135
+ } catch (error) {
136
+ console.warn('无法访问localStorage,将使用内存存储:', error);
137
+ }
138
+ }
139
+
140
+ // 获取预置渠道
141
+ private static getPresetChannel(): ApiChannel | null {
142
+ const config = getPresetConfig();
143
+ if (!config) return null;
144
+
145
+ return {
146
+ id: PRESET_CHANNEL_ID,
147
+ name: config.name,
148
+ baseUrl: config.baseUrl,
149
+ apiKey: config.apiKey,
150
+ isDefault: true,
151
+ isCustom: false,
152
+ timeout: 30000,
153
+ description: '系统提供的快速体验服务',
154
+ createdAt: new Date()
155
+ };
156
+ }
157
+
158
+ // ============ 渠道管理 ============
159
+
160
+ // 获取所有渠道
161
+ static getChannels(): ApiChannel[] {
162
+ this.ensureInitialized();
163
+
164
+ const presetChannel = this.getPresetChannel();
165
+ const channels: ApiChannel[] = [];
166
+
167
+ // 始终将预置渠道放在第一位(如果存在)
168
+ if (presetChannel) {
169
+ channels.push(presetChannel);
170
+ }
171
+
172
+ try {
173
+ const stored = localStorage.getItem(this.STORAGE_KEY_CHANNELS);
174
+ if (stored) {
175
+ const parsed = JSON.parse(stored);
176
+ const storedChannels = parsed.map((channel: any) => ({
177
+ ...channel,
178
+ createdAt: new Date(channel.createdAt)
179
+ }));
180
+
181
+ // 如果有预置渠道,确保其他渠道的isDefault为false
182
+ if (presetChannel) {
183
+ storedChannels.forEach((ch: ApiChannel) => {
184
+ ch.isDefault = false;
185
+ });
186
+ }
187
+
188
+ channels.push(...storedChannels);
189
+ } else {
190
+ // 如果没有存储的渠道,添加默认的OpenAI官方渠道
191
+ const defaultChannels = [...DEFAULT_CHANNELS];
192
+ if (presetChannel) {
193
+ defaultChannels.forEach(ch => {
194
+ ch.isDefault = false;
195
+ });
196
+ }
197
+ channels.push(...defaultChannels);
198
+ }
199
+ } catch (error) {
200
+ console.warn('从localStorage加载渠道失败:', error);
201
+ channels.push(...DEFAULT_CHANNELS);
202
+ }
203
+
204
+ return channels;
205
+ }
206
+
207
+ // 保存渠道配置
208
+ static saveChannels(channels: ApiChannel[]): void {
209
+ try {
210
+ // 过滤掉预置渠道,因为它不应该被保存到localStorage
211
+ const channelsToSave = channels.filter(ch => ch.id !== PRESET_CHANNEL_ID);
212
+ localStorage.setItem(this.STORAGE_KEY_CHANNELS, JSON.stringify(channelsToSave));
213
+ } catch (error) {
214
+ console.error('保存渠道到localStorage失败:', error);
215
+ throw new Error('无法保存渠道配置');
216
+ }
217
+ }
218
+
219
+ // 添加新渠道
220
+ static addChannel(channel: Omit<ApiChannel, 'id' | 'createdAt' | 'isCustom'>): ApiChannel {
221
+ const newChannel: ApiChannel = {
222
+ ...channel,
223
+ id: `channel-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
224
+ createdAt: new Date(),
225
+ isCustom: true
226
+ };
227
+
228
+ const channels = this.getChannels();
229
+
230
+ // 如果这是第一个渠道或设置为默认,但存在预置渠道,则不允许设为默认
231
+ if (newChannel.isDefault && this.getPresetChannel()) {
232
+ console.warn('存在预置渠道时,无法设置其他渠道为默认');
233
+ newChannel.isDefault = false;
234
+ }
235
+
236
+ channels.push(newChannel);
237
+ this.saveChannels(channels.filter(ch => ch.id !== PRESET_CHANNEL_ID));
238
+ return newChannel;
239
+ }
240
+
241
+ // 更新渠道
242
+ static updateChannel(id: string, updates: Partial<ApiChannel>): void {
243
+ if (id === PRESET_CHANNEL_ID) {
244
+ console.warn('预置渠道不允许修改');
245
+ return;
246
+ }
247
+
248
+ const channels = this.getChannels();
249
+ const index = channels.findIndex(ch => ch.id === id);
250
+ if (index !== -1) {
251
+ // 如果尝试设置为默认,但存在预置渠道,则阻止
252
+ if (updates.isDefault && this.getPresetChannel()) {
253
+ console.warn('存在预置渠道时,无法设置其他渠道为默认');
254
+ return;
255
+ }
256
+
257
+ channels[index] = { ...channels[index], ...updates };
258
+ // 保存时过滤掉预置渠道
259
+ this.saveChannels(channels.filter(ch => ch.id !== PRESET_CHANNEL_ID));
260
+ }
261
+ }
262
+
263
+ // 删除渠道
264
+ static deleteChannel(id: string): void {
265
+ if (id === PRESET_CHANNEL_ID) {
266
+ console.warn('预置渠道不允许删除');
267
+ return;
268
+ }
269
+
270
+ const channels = this.getChannels();
271
+ const filtered = channels.filter(ch => ch.id !== id && ch.id !== PRESET_CHANNEL_ID);
272
+ this.saveChannels(filtered);
273
+ }
274
+
275
+ // 获取默认渠道
276
+ static getDefaultChannel(): ApiChannel | null {
277
+ const channels = this.getChannels();
278
+ return channels.find(ch => ch.isDefault) || channels[0] || null;
279
+ }
280
+
281
+ // 根据ID获取渠道
282
+ static getChannelById(id: string): ApiChannel | null {
283
+ const channels = this.getChannels();
284
+ return channels.find(ch => ch.id === id) || null;
285
+ }
286
+
287
+ // 验证渠道配置
288
+ static validateChannel(channel: Partial<ApiChannel>): string[] {
289
+ const errors: string[] = [];
290
+
291
+ if (!channel.name?.trim()) {
292
+ errors.push('渠道名称不能为空');
293
+ }
294
+
295
+ if (!channel.baseUrl?.trim()) {
296
+ errors.push('API基础URL不能为空');
297
+ } else {
298
+ try {
299
+ new URL(channel.baseUrl);
300
+ } catch {
301
+ errors.push('API基础URL格式无效');
302
+ }
303
+ }
304
+
305
+ if (!channel.apiKey?.trim()) {
306
+ errors.push('API密钥不能为空');
307
+ }
308
+
309
+ if (channel.timeout && (typeof channel.timeout !== 'number' || channel.timeout < 1000)) {
310
+ errors.push('超时时间必须是大于1000毫秒的数字');
311
+ }
312
+
313
+ return errors;
314
+ }
315
+
316
+ // ============ 模型管理 ============
317
+
318
+ // 获取所有模型
319
+ static getModels(): AiModel[] {
320
+ this.ensureInitialized();
321
+ try {
322
+ const stored = localStorage.getItem(this.STORAGE_KEY_MODELS);
323
+ if (stored) {
324
+ const parsed = JSON.parse(stored);
325
+ return parsed.map((model: any) => ({
326
+ ...model,
327
+ createdAt: new Date(model.createdAt)
328
+ }));
329
+ }
330
+ } catch (error) {
331
+ console.warn('从localStorage加载模型失败:', error);
332
+ }
333
+ return [...DEFAULT_MODELS];
334
+ }
335
+
336
+ // 保存模型配置
337
+ static saveModels(models: AiModel[]): void {
338
+ try {
339
+ localStorage.setItem(this.STORAGE_KEY_MODELS, JSON.stringify(models));
340
+ } catch (error) {
341
+ console.error('保存模型到localStorage失败:', error);
342
+ throw new Error('无法保存模型配置');
343
+ }
344
+ }
345
+
346
+ // 添加新模型
347
+ static addModel(model: Omit<AiModel, 'id' | 'createdAt' | 'isCustom'>): AiModel {
348
+ const newModel: AiModel = {
349
+ ...model,
350
+ id: `model-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
351
+ createdAt: new Date(),
352
+ isCustom: true,
353
+ // 如果没有指定channelId,默认使用OpenAI官方
354
+ channelId: model.channelId || 'openai-official'
355
+ };
356
+
357
+ const models = this.getModels();
358
+ models.push(newModel);
359
+ this.saveModels(models);
360
+ return newModel;
361
+ }
362
+
363
+ // 更新模型
364
+ static updateModel(id: string, updates: Partial<AiModel>): void {
365
+ const models = this.getModels();
366
+ const index = models.findIndex(m => m.id === id);
367
+ if (index !== -1) {
368
+ models[index] = { ...models[index], ...updates };
369
+ this.saveModels(models);
370
+ }
371
+ }
372
+
373
+ // 删除模型
374
+ static deleteModel(id: string): void {
375
+ const models = this.getModels();
376
+ const filtered = models.filter(m => m.id !== id);
377
+ this.saveModels(filtered);
378
+ }
379
+
380
+ // 验证模型配置
381
+ static validateModel(model: Partial<AiModel>): string[] {
382
+ const errors: string[] = [];
383
+
384
+ if (!model.name?.trim()) {
385
+ errors.push('模型名称不能为空');
386
+ }
387
+
388
+ if (!model.apiName?.trim()) {
389
+ errors.push('API模型名称不能为空');
390
+ }
391
+
392
+ if (!model.channelId?.trim()) {
393
+ errors.push('必须选择API渠道');
394
+ }
395
+
396
+ if (!model.category?.trim()) {
397
+ errors.push('模型类别不能为空');
398
+ }
399
+
400
+ if (typeof model.maxTokens !== 'number' || model.maxTokens < 1) {
401
+ errors.push('最大Token数必须是大于0的数字');
402
+ }
403
+
404
+ if (typeof model.temperature !== 'number' || model.temperature < 0 || model.temperature > 2) {
405
+ errors.push('温度参数必须在0-2之间');
406
+ }
407
+
408
+ return errors;
409
+ }
410
+
411
+ // ============ 角色管理 ============
412
+
413
+ // 获取所有角色
414
+ static getRoles(): AiRole[] {
415
+ this.ensureInitialized();
416
+ try {
417
+ const stored = localStorage.getItem(this.STORAGE_KEY_ROLES);
418
+ if (stored) {
419
+ return JSON.parse(stored);
420
+ }
421
+ } catch (error) {
422
+ console.warn('从localStorage加载角色失败:', error);
423
+ }
424
+ return [...DEFAULT_ROLES];
425
+ }
426
+
427
+ // 保存角色配置
428
+ static saveRoles(roles: AiRole[]): void {
429
+ try {
430
+ localStorage.setItem(this.STORAGE_KEY_ROLES, JSON.stringify(roles));
431
+ } catch (error) {
432
+ console.error('保存角色到localStorage失败:', error);
433
+ throw new Error('无法保存角色配置');
434
+ }
435
+ }
436
+
437
+ // 添加新角色
438
+ static addRole(role: Omit<AiRole, 'id'>): AiRole {
439
+ const newRole: AiRole = {
440
+ ...role,
441
+ id: `role-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
442
+ };
443
+
444
+ const roles = this.getRoles();
445
+ roles.push(newRole);
446
+ this.saveRoles(roles);
447
+ return newRole;
448
+ }
449
+
450
+ // 更新角色
451
+ static updateRole(id: string, updates: Partial<AiRole>): void {
452
+ const roles = this.getRoles();
453
+ const index = roles.findIndex(r => r.id === id);
454
+ if (index !== -1) {
455
+ roles[index] = { ...roles[index], ...updates };
456
+ this.saveRoles(roles);
457
+ }
458
+ }
459
+
460
+ // 删除角色
461
+ static deleteRole(id: string): void {
462
+ const roles = this.getRoles();
463
+ const filtered = roles.filter(r => r.id !== id);
464
+ this.saveRoles(filtered);
465
+ }
466
+
467
+ // 获取活跃角色
468
+ static getActiveRoles(): AiRole[] {
469
+ return this.getRoles().filter(role => role.isActive);
470
+ }
471
+
472
+ // ============ 工具方法 ============
473
+
474
+ // 根据类别分组模型
475
+ static getModelsByCategory(): Record<string, AiModel[]> {
476
+ const models = this.getModels();
477
+ return models.reduce((acc, model) => {
478
+ if (!acc[model.category]) {
479
+ acc[model.category] = [];
480
+ }
481
+ acc[model.category].push(model);
482
+ return acc;
483
+ }, {} as Record<string, AiModel[]>);
484
+ }
485
+
486
+ // 重置为默认配置
487
+ static resetToDefaults(): void {
488
+ try {
489
+ localStorage.removeItem(this.STORAGE_KEY_CHANNELS);
490
+ localStorage.removeItem(this.STORAGE_KEY_MODELS);
491
+ localStorage.removeItem(this.STORAGE_KEY_ROLES);
492
+ localStorage.removeItem(this.STORAGE_KEY_INITIALIZED);
493
+ this.ensureInitialized();
494
+ } catch (error) {
495
+ console.error('重置配置失败:', error);
496
+ throw new Error('无法��置配置');
497
+ }
498
+ }
499
+
500
+ // 导出配置
501
+ static exportConfig(): string {
502
+ return JSON.stringify({
503
+ channels: this.getChannels().filter(ch => ch.id !== PRESET_CHANNEL_ID),
504
+ models: this.getModels(),
505
+ roles: this.getRoles(),
506
+ exportedAt: new Date().toISOString(),
507
+ version: '2.0'
508
+ }, null, 2);
509
+ }
510
+
511
+ // 导入配置
512
+ static importConfig(configJson: string): { success: boolean; message: string } {
513
+ try {
514
+ const config = JSON.parse(configJson);
515
+
516
+ if (!config.channels && !config.models && !config.roles) {
517
+ return { success: false, message: '配置文件格式无效,缺少必要的配置信息' };
518
+ }
519
+
520
+ if (config.channels && Array.isArray(config.channels)) {
521
+ const validChannels = config.channels.filter((channel: any) => {
522
+ const errors = this.validateChannel(channel);
523
+ return errors.length === 0;
524
+ });
525
+
526
+ if (validChannels.length > 0) {
527
+ const processedChannels = validChannels.map((channel: any) => ({
528
+ ...channel,
529
+ createdAt: new Date(channel.createdAt || new Date()),
530
+ isCustom: channel.isCustom !== false
531
+ }));
532
+ this.saveChannels(processedChannels);
533
+ }
534
+ }
535
+
536
+ if (config.models && Array.isArray(config.models)) {
537
+ const validModels = config.models.filter((model: any) => {
538
+ const errors = this.validateModel(model);
539
+ return errors.length === 0;
540
+ });
541
+
542
+ if (validModels.length > 0) {
543
+ const processedModels = validModels.map((model: any) => ({
544
+ ...model,
545
+ createdAt: new Date(model.createdAt || new Date()),
546
+ isCustom: model.isCustom !== false
547
+ }));
548
+ this.saveModels(processedModels);
549
+ }
550
+ }
551
+
552
+ if (config.roles && Array.isArray(config.roles)) {
553
+ this.saveRoles(config.roles);
554
+ }
555
+
556
+ return { success: true, message: '配置导入成功' };
557
+ } catch (error) {
558
+ return { success: false, message: `配置导入失败: ${error instanceof Error ? error.message : '未知错误'}` };
559
+ }
560
+ }
561
+
562
+ // 清空所有数据
563
+ static clearAllData(): void {
564
+ try {
565
+ localStorage.removeItem(this.STORAGE_KEY_CHANNELS);
566
+ localStorage.removeItem(this.STORAGE_KEY_MODELS);
567
+ localStorage.removeItem(this.STORAGE_KEY_ROLES);
568
+ localStorage.removeItem(this.STORAGE_KEY_INITIALIZED);
569
+ } catch (error) {
570
+ console.error('清空数据失败:', error);
571
+ throw new Error('无法清空配置数据');
572
+ }
573
+ }
574
+
575
+ // 获取存储使用情况
576
+ static getStorageInfo(): { used: number; available: number; channels: number; models: number; roles: number } {
577
+ try {
578
+ const channelsData = localStorage.getItem(this.STORAGE_KEY_CHANNELS) || '';
579
+ const modelsData = localStorage.getItem(this.STORAGE_KEY_MODELS) || '';
580
+ const rolesData = localStorage.getItem(this.STORAGE_KEY_ROLES) || '';
581
+ const used = channelsData.length + modelsData.length + rolesData.length;
582
+
583
+ return {
584
+ used,
585
+ available: 5242880 - used, // 5MB 大致容量
586
+ channels: this.getChannels().length,
587
+ models: this.getModels().length,
588
+ roles: this.getRoles().length
589
+ };
590
+ } catch (error) {
591
+ return { used: 0, available: 0, channels: 0, models: 0, roles: 0 };
592
+ }
593
+ }
594
+ }
595
+
596
+ // 其他常量配置
597
+ export const DEFAULT_MANUAL_FIXED_TURNS = 2;
598
+ export const MIN_MANUAL_FIXED_TURNS = 1;
599
+ export const MAX_MANUAL_FIXED_TURNS = 5;
600
+ export const MAX_AI_DRIVEN_DISCUSSION_TURNS_PER_MODEL = 3;
601
+
602
+ export const INITIAL_NOTEPAD_CONTENT = `这是一个共享记事本。
603
+ AI角色可以在这里合作记录想法、草稿或关键点。
604
+
605
+ 使用指南:
606
+ - AI 模型可以通过在其回复中包含特定指令来更新此记事本。
607
+ - 记事本的内容将包含在发送给 AI 的后续提示中。
608
+
609
+ 初始状态:空白。`;
610
+
611
+ export const NOTEPAD_INSTRUCTION_PROMPT_PART = `
612
+ You also have access to a shared notepad.
613
+ Current Notepad Content:
614
+ ---
615
+ {notepadContent}
616
+ ---
617
+ Instructions for Notepad:
618
+ 1. To update the notepad, include a section at the very end of your response, formatted exactly as:
619
+ <notepad_update>
620
+ [YOUR NEW FULL NOTEPAD CONTENT HERE. THIS WILL REPLACE THE ENTIRE CURRENT NOTEPAD CONTENT.]
621
+ </notepad_update>
622
+ 2. If you do not want to change the notepad, do NOT include the <notepad_update> section at all.
623
+ 3. Your primary spoken response to the ongoing discussion should come BEFORE any <notepad_update> section. Ensure you still provide a spoken response.
624
+ `;
625
+
626
+ export const NOTEPAD_UPDATE_TAG_START = "<notepad_update>";
627
+ export const NOTEPAD_UPDATE_TAG_END = "</notepad_update>";
628
+ export const DISCUSSION_COMPLETE_TAG = "<discussion_complete />";
629
+
630
+ export const AI_DRIVEN_DISCUSSION_INSTRUCTION_PROMPT_PART = `
631
+ Instruction for ending discussion: If you believe the current topic has been sufficiently explored between you and your AI partner for the final synthesis, include the exact tag ${DISCUSSION_COMPLETE_TAG} at the very end of your current message (after any notepad update). Do not use this tag if you wish to continue the discussion or require more input/response from your partner.
632
+ `;
633
+
634
+ export enum DiscussionMode {
635
+ FixedTurns = 'fixed',
636
+ AiDriven = 'ai-driven',
637
+ }
index.html ADDED
@@ -0,0 +1,197 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Multi-Mind Chat 智囊团</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <style>
9
+ /* Custom scrollbar for webkit browsers */
10
+ ::-webkit-scrollbar {
11
+ width: 8px;
12
+ }
13
+ ::-webkit-scrollbar-track {
14
+ background: #f1f1f1;
15
+ border-radius: 10px;
16
+ }
17
+ ::-webkit-scrollbar-thumb {
18
+ background: #888;
19
+ border-radius: 10px;
20
+ }
21
+ ::-webkit-scrollbar-thumb:hover {
22
+ background: #555;
23
+ }
24
+ body {
25
+ font-family: 'Inter', sans-serif; /* A common sans-serif font often used with Tailwind */
26
+ }
27
+ /* Basic styles for Markdown preview in Notepad */
28
+ .markdown-preview {
29
+ padding: 0.75rem; /* Corresponds to p-3 in Tailwind */
30
+ color: #d1d5db; /* text-gray-300 */
31
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; /* font-mono */
32
+ font-size: 0.875rem; /* text-sm */
33
+ line-height: 1.625; /* leading-relaxed */
34
+ background-color: #1f2937; /* bg-slate-800 or similar dark bg */
35
+ height: 100%;
36
+ overflow-y: auto;
37
+ }
38
+ .markdown-preview h1,
39
+ .markdown-preview h2,
40
+ .markdown-preview h3,
41
+ .markdown-preview h4,
42
+ .markdown-preview h5,
43
+ .markdown-preview h6 {
44
+ color: #93c5fd; /* A lighter blue for headings */
45
+ margin-top: 1em;
46
+ margin-bottom: 0.5em;
47
+ font-weight: 600;
48
+ }
49
+ .markdown-preview h1 { font-size: 1.875em; }
50
+ .markdown-preview h2 { font-size: 1.5em; }
51
+ .markdown-preview h3 { font-size: 1.25em; }
52
+ .markdown-preview p {
53
+ margin-bottom: 0.75em;
54
+ }
55
+ .markdown-preview ul,
56
+ .markdown-preview ol {
57
+ margin-left: 1.5em;
58
+ margin-bottom: 0.75em;
59
+ list-style-position: outside;
60
+ }
61
+ .markdown-preview ul { list-style-type: disc; }
62
+ .markdown-preview ol { list-style-type: decimal; }
63
+ .markdown-preview li { margin-bottom: 0.25em; }
64
+ .markdown-preview blockquote {
65
+ border-left: 4px solid #6b7280; /* border-gray-500 */
66
+ padding-left: 1em;
67
+ margin-left: 0;
68
+ margin-bottom: 0.75em;
69
+ color: #9ca3af; /* text-gray-400 */
70
+ font-style: italic;
71
+ }
72
+ .markdown-preview pre {
73
+ background-color: #374151; /* bg-gray-700 */
74
+ padding: 0.75em;
75
+ border-radius: 0.375rem; /* rounded-md */
76
+ overflow-x: auto;
77
+ margin-bottom: 0.75em;
78
+ color: #e5e7eb; /* text-gray-200 */
79
+ }
80
+ .markdown-preview code {
81
+ background-color: #4b5563; /* bg-gray-600 */
82
+ padding: 0.2em 0.4em;
83
+ border-radius: 0.25rem;
84
+ font-size: 0.9em;
85
+ }
86
+ .markdown-preview pre code {
87
+ background-color: transparent;
88
+ padding: 0;
89
+ font-size: 1em;
90
+ }
91
+ .markdown-preview a {
92
+ color: #60a5fa; /* text-blue-400 */
93
+ text-decoration: underline;
94
+ }
95
+ .markdown-preview hr {
96
+ border-top: 1px solid #4b5563; /* border-gray-600 */
97
+ margin: 1em 0;
98
+ }
99
+
100
+ /* Styles for Markdown content within chat bubbles */
101
+ .chat-markdown-content {
102
+ /* Base text color and size are applied by Tailwind class on the div */
103
+ /* e.g., text-sm text-gray-200 */
104
+ line-height: 1.625; /* leading-relaxed */
105
+ }
106
+ .chat-markdown-content h1,
107
+ .chat-markdown-content h2,
108
+ .chat-markdown-content h3,
109
+ .chat-markdown-content h4,
110
+ .chat-markdown-content h5,
111
+ .chat-markdown-content h6 {
112
+ color: #bae6fd; /* light-sky-300, slightly brighter for chat */
113
+ margin-top: 0.75em;
114
+ margin-bottom: 0.4em;
115
+ font-weight: 600;
116
+ line-height: 1.3;
117
+ }
118
+ .chat-markdown-content h1 { font-size: 1.5em; } /* Adjusted for chat context */
119
+ .chat-markdown-content h2 { font-size: 1.3em; }
120
+ .chat-markdown-content h3 { font-size: 1.15em; }
121
+ .chat-markdown-content p {
122
+ margin-bottom: 0.65em;
123
+ }
124
+ .chat-markdown-content ul,
125
+ .chat-markdown-content ol {
126
+ margin-left: 1.25em; /* Slightly less indent for chat */
127
+ margin-bottom: 0.65em;
128
+ list-style-position: outside;
129
+ }
130
+ .chat-markdown-content ul { list-style-type: disc; }
131
+ .chat-markdown-content ol { list-style-type: decimal; }
132
+ .chat-markdown-content li { margin-bottom: 0.2em; }
133
+ .chat-markdown-content blockquote {
134
+ border-left: 3px solid #4b5563; /* border-gray-600 */
135
+ padding-left: 0.75em;
136
+ margin-left: 0;
137
+ margin-bottom: 0.65em;
138
+ color: #9ca3af; /* text-gray-400 */
139
+ font-style: italic;
140
+ }
141
+ .chat-markdown-content pre {
142
+ background-color: #1e293b; /* slate-800, distinct from bubble */
143
+ padding: 0.65em;
144
+ border-radius: 0.25rem; /* rounded-sm */
145
+ overflow-x: auto;
146
+ margin-bottom: 0.65em;
147
+ color: #e2e8f0; /* slate-200 */
148
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
149
+ font-size: 0.875rem; /* text-sm */
150
+ }
151
+ .chat-markdown-content code { /* Inline code */
152
+ background-color: #334155; /* slate-700 */
153
+ color: #e2e8f0; /* slate-200 */
154
+ padding: 0.15em 0.3em;
155
+ border-radius: 0.2rem;
156
+ font-size: 0.85em; /* Slightly smaller */
157
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
158
+ }
159
+ .chat-markdown-content pre code { /* Code within pre blocks */
160
+ background-color: transparent;
161
+ padding: 0;
162
+ font-size: 1em; /* Reset from inline code */
163
+ color: inherit; /* Inherit from pre */
164
+ }
165
+ .chat-markdown-content a {
166
+ color: #38bdf8; /* sky-400 */
167
+ text-decoration: underline;
168
+ }
169
+ .chat-markdown-content a:hover {
170
+ color: #7dd3fc; /* sky-300 */
171
+ }
172
+ .chat-markdown-content hr {
173
+ border-top: 1px solid #334155; /* slate-700 */
174
+ margin: 0.75em 0;
175
+ }
176
+
177
+ </style>
178
+ <script type="importmap">
179
+ {
180
+ "imports": {
181
+ "react": "https://esm.sh/react@^19.1.0",
182
+ "react-dom/": "https://esm.sh/react-dom@^19.1.0/",
183
+ "react/": "https://esm.sh/react@^19.1.0/",
184
+ "@google/genai": "https://esm.sh/@google/genai@^1.0.1",
185
+ "lucide-react": "https://esm.sh/lucide-react@^0.511.0",
186
+ "marked": "https://esm.sh/marked@^13.0.2",
187
+ "dompurify": "https://esm.sh/dompurify@^3.1.6"
188
+ }
189
+ }
190
+ </script>
191
+ </head>
192
+ <body class="bg-gray-900 text-white">
193
+ <div id="root"></div>
194
+ <script type="module" src="/index.tsx"></script>
195
+ </body>
196
+ </html><link rel="stylesheet" href="index.css">
197
+ <script src="index.tsx" type="module"></script>
index.tsx ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import React from 'react';
3
+ import ReactDOM from 'react-dom/client';
4
+ import App from './App';
5
+
6
+ const rootElement = document.getElementById('root');
7
+ if (!rootElement) {
8
+ throw new Error("Could not find root element to mount to");
9
+ }
10
+
11
+ const root = ReactDOM.createRoot(rootElement);
12
+ root.render(
13
+ <React.StrictMode>
14
+ <App />
15
+ </React.StrictMode>
16
+ );
17
+
metadata.json ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ {
2
+ "name": "Multi-Mind Chat 智囊团(融合3)",
3
+ "description": "A chat application where user messages are discussed by two AI models (Cognito and Muse). Features a shared notepad that both AIs can read from and write to, with the entire conversation and notepad changes displayed.",
4
+ "requestFramePermissions": [],
5
+ "prompt": ""
6
+ }
package-lock.json ADDED
@@ -0,0 +1,2514 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "dual-ai-chat(融合3)",
3
+ "version": "0.0.0",
4
+ "lockfileVersion": 3,
5
+ "requires": true,
6
+ "packages": {
7
+ "": {
8
+ "name": "dual-ai-chat(融合3)",
9
+ "version": "0.0.0",
10
+ "dependencies": {
11
+ "@google/genai": "^1.0.1",
12
+ "dompurify": "^3.1.6",
13
+ "lucide-react": "^0.511.0",
14
+ "marked": "^13.0.2",
15
+ "react": "^19.1.0",
16
+ "react-dom": "^19.1.0"
17
+ },
18
+ "devDependencies": {
19
+ "@types/node": "^22.14.0",
20
+ "@types/react": "^19.1.5",
21
+ "typescript": "~5.7.2",
22
+ "vite": "^6.2.0"
23
+ }
24
+ },
25
+ "node_modules/@esbuild/aix-ppc64": {
26
+ "version": "0.25.4",
27
+ "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.4.tgz",
28
+ "integrity": "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q==",
29
+ "cpu": [
30
+ "ppc64"
31
+ ],
32
+ "dev": true,
33
+ "license": "MIT",
34
+ "optional": true,
35
+ "os": [
36
+ "aix"
37
+ ],
38
+ "engines": {
39
+ "node": ">=18"
40
+ }
41
+ },
42
+ "node_modules/@esbuild/android-arm": {
43
+ "version": "0.25.4",
44
+ "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.25.4.tgz",
45
+ "integrity": "sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ==",
46
+ "cpu": [
47
+ "arm"
48
+ ],
49
+ "dev": true,
50
+ "license": "MIT",
51
+ "optional": true,
52
+ "os": [
53
+ "android"
54
+ ],
55
+ "engines": {
56
+ "node": ">=18"
57
+ }
58
+ },
59
+ "node_modules/@esbuild/android-arm64": {
60
+ "version": "0.25.4",
61
+ "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.25.4.tgz",
62
+ "integrity": "sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A==",
63
+ "cpu": [
64
+ "arm64"
65
+ ],
66
+ "dev": true,
67
+ "license": "MIT",
68
+ "optional": true,
69
+ "os": [
70
+ "android"
71
+ ],
72
+ "engines": {
73
+ "node": ">=18"
74
+ }
75
+ },
76
+ "node_modules/@esbuild/android-x64": {
77
+ "version": "0.25.4",
78
+ "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.25.4.tgz",
79
+ "integrity": "sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ==",
80
+ "cpu": [
81
+ "x64"
82
+ ],
83
+ "dev": true,
84
+ "license": "MIT",
85
+ "optional": true,
86
+ "os": [
87
+ "android"
88
+ ],
89
+ "engines": {
90
+ "node": ">=18"
91
+ }
92
+ },
93
+ "node_modules/@esbuild/darwin-arm64": {
94
+ "version": "0.25.4",
95
+ "resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.25.4.tgz",
96
+ "integrity": "sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g==",
97
+ "cpu": [
98
+ "arm64"
99
+ ],
100
+ "dev": true,
101
+ "license": "MIT",
102
+ "optional": true,
103
+ "os": [
104
+ "darwin"
105
+ ],
106
+ "engines": {
107
+ "node": ">=18"
108
+ }
109
+ },
110
+ "node_modules/@esbuild/darwin-x64": {
111
+ "version": "0.25.4",
112
+ "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.25.4.tgz",
113
+ "integrity": "sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A==",
114
+ "cpu": [
115
+ "x64"
116
+ ],
117
+ "dev": true,
118
+ "license": "MIT",
119
+ "optional": true,
120
+ "os": [
121
+ "darwin"
122
+ ],
123
+ "engines": {
124
+ "node": ">=18"
125
+ }
126
+ },
127
+ "node_modules/@esbuild/freebsd-arm64": {
128
+ "version": "0.25.4",
129
+ "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.4.tgz",
130
+ "integrity": "sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ==",
131
+ "cpu": [
132
+ "arm64"
133
+ ],
134
+ "dev": true,
135
+ "license": "MIT",
136
+ "optional": true,
137
+ "os": [
138
+ "freebsd"
139
+ ],
140
+ "engines": {
141
+ "node": ">=18"
142
+ }
143
+ },
144
+ "node_modules/@esbuild/freebsd-x64": {
145
+ "version": "0.25.4",
146
+ "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.4.tgz",
147
+ "integrity": "sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ==",
148
+ "cpu": [
149
+ "x64"
150
+ ],
151
+ "dev": true,
152
+ "license": "MIT",
153
+ "optional": true,
154
+ "os": [
155
+ "freebsd"
156
+ ],
157
+ "engines": {
158
+ "node": ">=18"
159
+ }
160
+ },
161
+ "node_modules/@esbuild/linux-arm": {
162
+ "version": "0.25.4",
163
+ "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.25.4.tgz",
164
+ "integrity": "sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ==",
165
+ "cpu": [
166
+ "arm"
167
+ ],
168
+ "dev": true,
169
+ "license": "MIT",
170
+ "optional": true,
171
+ "os": [
172
+ "linux"
173
+ ],
174
+ "engines": {
175
+ "node": ">=18"
176
+ }
177
+ },
178
+ "node_modules/@esbuild/linux-arm64": {
179
+ "version": "0.25.4",
180
+ "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.25.4.tgz",
181
+ "integrity": "sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ==",
182
+ "cpu": [
183
+ "arm64"
184
+ ],
185
+ "dev": true,
186
+ "license": "MIT",
187
+ "optional": true,
188
+ "os": [
189
+ "linux"
190
+ ],
191
+ "engines": {
192
+ "node": ">=18"
193
+ }
194
+ },
195
+ "node_modules/@esbuild/linux-ia32": {
196
+ "version": "0.25.4",
197
+ "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.25.4.tgz",
198
+ "integrity": "sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ==",
199
+ "cpu": [
200
+ "ia32"
201
+ ],
202
+ "dev": true,
203
+ "license": "MIT",
204
+ "optional": true,
205
+ "os": [
206
+ "linux"
207
+ ],
208
+ "engines": {
209
+ "node": ">=18"
210
+ }
211
+ },
212
+ "node_modules/@esbuild/linux-loong64": {
213
+ "version": "0.25.4",
214
+ "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.25.4.tgz",
215
+ "integrity": "sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA==",
216
+ "cpu": [
217
+ "loong64"
218
+ ],
219
+ "dev": true,
220
+ "license": "MIT",
221
+ "optional": true,
222
+ "os": [
223
+ "linux"
224
+ ],
225
+ "engines": {
226
+ "node": ">=18"
227
+ }
228
+ },
229
+ "node_modules/@esbuild/linux-mips64el": {
230
+ "version": "0.25.4",
231
+ "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.4.tgz",
232
+ "integrity": "sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg==",
233
+ "cpu": [
234
+ "mips64el"
235
+ ],
236
+ "dev": true,
237
+ "license": "MIT",
238
+ "optional": true,
239
+ "os": [
240
+ "linux"
241
+ ],
242
+ "engines": {
243
+ "node": ">=18"
244
+ }
245
+ },
246
+ "node_modules/@esbuild/linux-ppc64": {
247
+ "version": "0.25.4",
248
+ "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.4.tgz",
249
+ "integrity": "sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag==",
250
+ "cpu": [
251
+ "ppc64"
252
+ ],
253
+ "dev": true,
254
+ "license": "MIT",
255
+ "optional": true,
256
+ "os": [
257
+ "linux"
258
+ ],
259
+ "engines": {
260
+ "node": ">=18"
261
+ }
262
+ },
263
+ "node_modules/@esbuild/linux-riscv64": {
264
+ "version": "0.25.4",
265
+ "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.4.tgz",
266
+ "integrity": "sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA==",
267
+ "cpu": [
268
+ "riscv64"
269
+ ],
270
+ "dev": true,
271
+ "license": "MIT",
272
+ "optional": true,
273
+ "os": [
274
+ "linux"
275
+ ],
276
+ "engines": {
277
+ "node": ">=18"
278
+ }
279
+ },
280
+ "node_modules/@esbuild/linux-s390x": {
281
+ "version": "0.25.4",
282
+ "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.25.4.tgz",
283
+ "integrity": "sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g==",
284
+ "cpu": [
285
+ "s390x"
286
+ ],
287
+ "dev": true,
288
+ "license": "MIT",
289
+ "optional": true,
290
+ "os": [
291
+ "linux"
292
+ ],
293
+ "engines": {
294
+ "node": ">=18"
295
+ }
296
+ },
297
+ "node_modules/@esbuild/linux-x64": {
298
+ "version": "0.25.4",
299
+ "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.25.4.tgz",
300
+ "integrity": "sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA==",
301
+ "cpu": [
302
+ "x64"
303
+ ],
304
+ "dev": true,
305
+ "license": "MIT",
306
+ "optional": true,
307
+ "os": [
308
+ "linux"
309
+ ],
310
+ "engines": {
311
+ "node": ">=18"
312
+ }
313
+ },
314
+ "node_modules/@esbuild/netbsd-arm64": {
315
+ "version": "0.25.4",
316
+ "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.4.tgz",
317
+ "integrity": "sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ==",
318
+ "cpu": [
319
+ "arm64"
320
+ ],
321
+ "dev": true,
322
+ "license": "MIT",
323
+ "optional": true,
324
+ "os": [
325
+ "netbsd"
326
+ ],
327
+ "engines": {
328
+ "node": ">=18"
329
+ }
330
+ },
331
+ "node_modules/@esbuild/netbsd-x64": {
332
+ "version": "0.25.4",
333
+ "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.4.tgz",
334
+ "integrity": "sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw==",
335
+ "cpu": [
336
+ "x64"
337
+ ],
338
+ "dev": true,
339
+ "license": "MIT",
340
+ "optional": true,
341
+ "os": [
342
+ "netbsd"
343
+ ],
344
+ "engines": {
345
+ "node": ">=18"
346
+ }
347
+ },
348
+ "node_modules/@esbuild/openbsd-arm64": {
349
+ "version": "0.25.4",
350
+ "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.4.tgz",
351
+ "integrity": "sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A==",
352
+ "cpu": [
353
+ "arm64"
354
+ ],
355
+ "dev": true,
356
+ "license": "MIT",
357
+ "optional": true,
358
+ "os": [
359
+ "openbsd"
360
+ ],
361
+ "engines": {
362
+ "node": ">=18"
363
+ }
364
+ },
365
+ "node_modules/@esbuild/openbsd-x64": {
366
+ "version": "0.25.4",
367
+ "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.4.tgz",
368
+ "integrity": "sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw==",
369
+ "cpu": [
370
+ "x64"
371
+ ],
372
+ "dev": true,
373
+ "license": "MIT",
374
+ "optional": true,
375
+ "os": [
376
+ "openbsd"
377
+ ],
378
+ "engines": {
379
+ "node": ">=18"
380
+ }
381
+ },
382
+ "node_modules/@esbuild/sunos-x64": {
383
+ "version": "0.25.4",
384
+ "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.25.4.tgz",
385
+ "integrity": "sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q==",
386
+ "cpu": [
387
+ "x64"
388
+ ],
389
+ "dev": true,
390
+ "license": "MIT",
391
+ "optional": true,
392
+ "os": [
393
+ "sunos"
394
+ ],
395
+ "engines": {
396
+ "node": ">=18"
397
+ }
398
+ },
399
+ "node_modules/@esbuild/win32-arm64": {
400
+ "version": "0.25.4",
401
+ "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.25.4.tgz",
402
+ "integrity": "sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ==",
403
+ "cpu": [
404
+ "arm64"
405
+ ],
406
+ "dev": true,
407
+ "license": "MIT",
408
+ "optional": true,
409
+ "os": [
410
+ "win32"
411
+ ],
412
+ "engines": {
413
+ "node": ">=18"
414
+ }
415
+ },
416
+ "node_modules/@esbuild/win32-ia32": {
417
+ "version": "0.25.4",
418
+ "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.25.4.tgz",
419
+ "integrity": "sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg==",
420
+ "cpu": [
421
+ "ia32"
422
+ ],
423
+ "dev": true,
424
+ "license": "MIT",
425
+ "optional": true,
426
+ "os": [
427
+ "win32"
428
+ ],
429
+ "engines": {
430
+ "node": ">=18"
431
+ }
432
+ },
433
+ "node_modules/@esbuild/win32-x64": {
434
+ "version": "0.25.4",
435
+ "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.25.4.tgz",
436
+ "integrity": "sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ==",
437
+ "cpu": [
438
+ "x64"
439
+ ],
440
+ "dev": true,
441
+ "license": "MIT",
442
+ "optional": true,
443
+ "os": [
444
+ "win32"
445
+ ],
446
+ "engines": {
447
+ "node": ">=18"
448
+ }
449
+ },
450
+ "node_modules/@google/genai": {
451
+ "version": "1.0.1",
452
+ "resolved": "https://registry.npmmirror.com/@google/genai/-/genai-1.0.1.tgz",
453
+ "integrity": "sha512-qf8sq9vpuKUeBKukAn43z2eC1I/Jw63b9wo6O+1x3EIroF3oDouJOtW1AzwvfO+9gzCPfLjuCUONhMKiBC8vkQ==",
454
+ "license": "Apache-2.0",
455
+ "dependencies": {
456
+ "google-auth-library": "^9.14.2",
457
+ "ws": "^8.18.0",
458
+ "zod": "^3.22.4",
459
+ "zod-to-json-schema": "^3.22.4"
460
+ },
461
+ "engines": {
462
+ "node": ">=20.0.0"
463
+ },
464
+ "peerDependencies": {
465
+ "@modelcontextprotocol/sdk": "^1.11.0"
466
+ }
467
+ },
468
+ "node_modules/@modelcontextprotocol/sdk": {
469
+ "version": "1.12.0",
470
+ "resolved": "https://registry.npmmirror.com/@modelcontextprotocol/sdk/-/sdk-1.12.0.tgz",
471
+ "integrity": "sha512-m//7RlINx1F3sz3KqwY1WWzVgTcYX52HYk4bJ1hkBXV3zccAEth+jRvG8DBRrdaQuRsPAJOx2MH3zaHNCKL7Zg==",
472
+ "license": "MIT",
473
+ "peer": true,
474
+ "dependencies": {
475
+ "ajv": "^6.12.6",
476
+ "content-type": "^1.0.5",
477
+ "cors": "^2.8.5",
478
+ "cross-spawn": "^7.0.5",
479
+ "eventsource": "^3.0.2",
480
+ "express": "^5.0.1",
481
+ "express-rate-limit": "^7.5.0",
482
+ "pkce-challenge": "^5.0.0",
483
+ "raw-body": "^3.0.0",
484
+ "zod": "^3.23.8",
485
+ "zod-to-json-schema": "^3.24.1"
486
+ },
487
+ "engines": {
488
+ "node": ">=18"
489
+ }
490
+ },
491
+ "node_modules/@rollup/rollup-android-arm-eabi": {
492
+ "version": "4.41.1",
493
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.41.1.tgz",
494
+ "integrity": "sha512-NELNvyEWZ6R9QMkiytB4/L4zSEaBC03KIXEghptLGLZWJ6VPrL63ooZQCOnlx36aQPGhzuOMwDerC1Eb2VmrLw==",
495
+ "cpu": [
496
+ "arm"
497
+ ],
498
+ "dev": true,
499
+ "license": "MIT",
500
+ "optional": true,
501
+ "os": [
502
+ "android"
503
+ ]
504
+ },
505
+ "node_modules/@rollup/rollup-android-arm64": {
506
+ "version": "4.41.1",
507
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.41.1.tgz",
508
+ "integrity": "sha512-DXdQe1BJ6TK47ukAoZLehRHhfKnKg9BjnQYUu9gzhI8Mwa1d2fzxA1aw2JixHVl403bwp1+/o/NhhHtxWJBgEA==",
509
+ "cpu": [
510
+ "arm64"
511
+ ],
512
+ "dev": true,
513
+ "license": "MIT",
514
+ "optional": true,
515
+ "os": [
516
+ "android"
517
+ ]
518
+ },
519
+ "node_modules/@rollup/rollup-darwin-arm64": {
520
+ "version": "4.41.1",
521
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.41.1.tgz",
522
+ "integrity": "sha512-5afxvwszzdulsU2w8JKWwY8/sJOLPzf0e1bFuvcW5h9zsEg+RQAojdW0ux2zyYAz7R8HvvzKCjLNJhVq965U7w==",
523
+ "cpu": [
524
+ "arm64"
525
+ ],
526
+ "dev": true,
527
+ "license": "MIT",
528
+ "optional": true,
529
+ "os": [
530
+ "darwin"
531
+ ]
532
+ },
533
+ "node_modules/@rollup/rollup-darwin-x64": {
534
+ "version": "4.41.1",
535
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.41.1.tgz",
536
+ "integrity": "sha512-egpJACny8QOdHNNMZKf8xY0Is6gIMz+tuqXlusxquWu3F833DcMwmGM7WlvCO9sB3OsPjdC4U0wHw5FabzCGZg==",
537
+ "cpu": [
538
+ "x64"
539
+ ],
540
+ "dev": true,
541
+ "license": "MIT",
542
+ "optional": true,
543
+ "os": [
544
+ "darwin"
545
+ ]
546
+ },
547
+ "node_modules/@rollup/rollup-freebsd-arm64": {
548
+ "version": "4.41.1",
549
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.41.1.tgz",
550
+ "integrity": "sha512-DBVMZH5vbjgRk3r0OzgjS38z+atlupJ7xfKIDJdZZL6sM6wjfDNo64aowcLPKIx7LMQi8vybB56uh1Ftck/Atg==",
551
+ "cpu": [
552
+ "arm64"
553
+ ],
554
+ "dev": true,
555
+ "license": "MIT",
556
+ "optional": true,
557
+ "os": [
558
+ "freebsd"
559
+ ]
560
+ },
561
+ "node_modules/@rollup/rollup-freebsd-x64": {
562
+ "version": "4.41.1",
563
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.41.1.tgz",
564
+ "integrity": "sha512-3FkydeohozEskBxNWEIbPfOE0aqQgB6ttTkJ159uWOFn42VLyfAiyD9UK5mhu+ItWzft60DycIN1Xdgiy8o/SA==",
565
+ "cpu": [
566
+ "x64"
567
+ ],
568
+ "dev": true,
569
+ "license": "MIT",
570
+ "optional": true,
571
+ "os": [
572
+ "freebsd"
573
+ ]
574
+ },
575
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
576
+ "version": "4.41.1",
577
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.41.1.tgz",
578
+ "integrity": "sha512-wC53ZNDgt0pqx5xCAgNunkTzFE8GTgdZ9EwYGVcg+jEjJdZGtq9xPjDnFgfFozQI/Xm1mh+D9YlYtl+ueswNEg==",
579
+ "cpu": [
580
+ "arm"
581
+ ],
582
+ "dev": true,
583
+ "license": "MIT",
584
+ "optional": true,
585
+ "os": [
586
+ "linux"
587
+ ]
588
+ },
589
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
590
+ "version": "4.41.1",
591
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.41.1.tgz",
592
+ "integrity": "sha512-jwKCca1gbZkZLhLRtsrka5N8sFAaxrGz/7wRJ8Wwvq3jug7toO21vWlViihG85ei7uJTpzbXZRcORotE+xyrLA==",
593
+ "cpu": [
594
+ "arm"
595
+ ],
596
+ "dev": true,
597
+ "license": "MIT",
598
+ "optional": true,
599
+ "os": [
600
+ "linux"
601
+ ]
602
+ },
603
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
604
+ "version": "4.41.1",
605
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.41.1.tgz",
606
+ "integrity": "sha512-g0UBcNknsmmNQ8V2d/zD2P7WWfJKU0F1nu0k5pW4rvdb+BIqMm8ToluW/eeRmxCared5dD76lS04uL4UaNgpNA==",
607
+ "cpu": [
608
+ "arm64"
609
+ ],
610
+ "dev": true,
611
+ "license": "MIT",
612
+ "optional": true,
613
+ "os": [
614
+ "linux"
615
+ ]
616
+ },
617
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
618
+ "version": "4.41.1",
619
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.41.1.tgz",
620
+ "integrity": "sha512-XZpeGB5TKEZWzIrj7sXr+BEaSgo/ma/kCgrZgL0oo5qdB1JlTzIYQKel/RmhT6vMAvOdM2teYlAaOGJpJ9lahg==",
621
+ "cpu": [
622
+ "arm64"
623
+ ],
624
+ "dev": true,
625
+ "license": "MIT",
626
+ "optional": true,
627
+ "os": [
628
+ "linux"
629
+ ]
630
+ },
631
+ "node_modules/@rollup/rollup-linux-loongarch64-gnu": {
632
+ "version": "4.41.1",
633
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.41.1.tgz",
634
+ "integrity": "sha512-bkCfDJ4qzWfFRCNt5RVV4DOw6KEgFTUZi2r2RuYhGWC8WhCA8lCAJhDeAmrM/fdiAH54m0mA0Vk2FGRPyzI+tw==",
635
+ "cpu": [
636
+ "loong64"
637
+ ],
638
+ "dev": true,
639
+ "license": "MIT",
640
+ "optional": true,
641
+ "os": [
642
+ "linux"
643
+ ]
644
+ },
645
+ "node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
646
+ "version": "4.41.1",
647
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.41.1.tgz",
648
+ "integrity": "sha512-3mr3Xm+gvMX+/8EKogIZSIEF0WUu0HL9di+YWlJpO8CQBnoLAEL/roTCxuLncEdgcfJcvA4UMOf+2dnjl4Ut1A==",
649
+ "cpu": [
650
+ "ppc64"
651
+ ],
652
+ "dev": true,
653
+ "license": "MIT",
654
+ "optional": true,
655
+ "os": [
656
+ "linux"
657
+ ]
658
+ },
659
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
660
+ "version": "4.41.1",
661
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.41.1.tgz",
662
+ "integrity": "sha512-3rwCIh6MQ1LGrvKJitQjZFuQnT2wxfU+ivhNBzmxXTXPllewOF7JR1s2vMX/tWtUYFgphygxjqMl76q4aMotGw==",
663
+ "cpu": [
664
+ "riscv64"
665
+ ],
666
+ "dev": true,
667
+ "license": "MIT",
668
+ "optional": true,
669
+ "os": [
670
+ "linux"
671
+ ]
672
+ },
673
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
674
+ "version": "4.41.1",
675
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.41.1.tgz",
676
+ "integrity": "sha512-LdIUOb3gvfmpkgFZuccNa2uYiqtgZAz3PTzjuM5bH3nvuy9ty6RGc/Q0+HDFrHrizJGVpjnTZ1yS5TNNjFlklw==",
677
+ "cpu": [
678
+ "riscv64"
679
+ ],
680
+ "dev": true,
681
+ "license": "MIT",
682
+ "optional": true,
683
+ "os": [
684
+ "linux"
685
+ ]
686
+ },
687
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
688
+ "version": "4.41.1",
689
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.41.1.tgz",
690
+ "integrity": "sha512-oIE6M8WC9ma6xYqjvPhzZYk6NbobIURvP/lEbh7FWplcMO6gn7MM2yHKA1eC/GvYwzNKK/1LYgqzdkZ8YFxR8g==",
691
+ "cpu": [
692
+ "s390x"
693
+ ],
694
+ "dev": true,
695
+ "license": "MIT",
696
+ "optional": true,
697
+ "os": [
698
+ "linux"
699
+ ]
700
+ },
701
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
702
+ "version": "4.41.1",
703
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.41.1.tgz",
704
+ "integrity": "sha512-cWBOvayNvA+SyeQMp79BHPK8ws6sHSsYnK5zDcsC3Hsxr1dgTABKjMnMslPq1DvZIp6uO7kIWhiGwaTdR4Og9A==",
705
+ "cpu": [
706
+ "x64"
707
+ ],
708
+ "dev": true,
709
+ "license": "MIT",
710
+ "optional": true,
711
+ "os": [
712
+ "linux"
713
+ ]
714
+ },
715
+ "node_modules/@rollup/rollup-linux-x64-musl": {
716
+ "version": "4.41.1",
717
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.41.1.tgz",
718
+ "integrity": "sha512-y5CbN44M+pUCdGDlZFzGGBSKCA4A/J2ZH4edTYSSxFg7ce1Xt3GtydbVKWLlzL+INfFIZAEg1ZV6hh9+QQf9YQ==",
719
+ "cpu": [
720
+ "x64"
721
+ ],
722
+ "dev": true,
723
+ "license": "MIT",
724
+ "optional": true,
725
+ "os": [
726
+ "linux"
727
+ ]
728
+ },
729
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
730
+ "version": "4.41.1",
731
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.41.1.tgz",
732
+ "integrity": "sha512-lZkCxIrjlJlMt1dLO/FbpZbzt6J/A8p4DnqzSa4PWqPEUUUnzXLeki/iyPLfV0BmHItlYgHUqJe+3KiyydmiNQ==",
733
+ "cpu": [
734
+ "arm64"
735
+ ],
736
+ "dev": true,
737
+ "license": "MIT",
738
+ "optional": true,
739
+ "os": [
740
+ "win32"
741
+ ]
742
+ },
743
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
744
+ "version": "4.41.1",
745
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.41.1.tgz",
746
+ "integrity": "sha512-+psFT9+pIh2iuGsxFYYa/LhS5MFKmuivRsx9iPJWNSGbh2XVEjk90fmpUEjCnILPEPJnikAU6SFDiEUyOv90Pg==",
747
+ "cpu": [
748
+ "ia32"
749
+ ],
750
+ "dev": true,
751
+ "license": "MIT",
752
+ "optional": true,
753
+ "os": [
754
+ "win32"
755
+ ]
756
+ },
757
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
758
+ "version": "4.41.1",
759
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.41.1.tgz",
760
+ "integrity": "sha512-Wq2zpapRYLfi4aKxf2Xff0tN+7slj2d4R87WEzqw7ZLsVvO5zwYCIuEGSZYiK41+GlwUo1HiR+GdkLEJnCKTCw==",
761
+ "cpu": [
762
+ "x64"
763
+ ],
764
+ "dev": true,
765
+ "license": "MIT",
766
+ "optional": true,
767
+ "os": [
768
+ "win32"
769
+ ]
770
+ },
771
+ "node_modules/@types/estree": {
772
+ "version": "1.0.7",
773
+ "resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.7.tgz",
774
+ "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==",
775
+ "dev": true,
776
+ "license": "MIT"
777
+ },
778
+ "node_modules/@types/node": {
779
+ "version": "22.15.21",
780
+ "resolved": "https://registry.npmmirror.com/@types/node/-/node-22.15.21.tgz",
781
+ "integrity": "sha512-EV/37Td6c+MgKAbkcLG6vqZ2zEYHD7bvSrzqqs2RIhbA6w3x+Dqz8MZM3sP6kGTeLrdoOgKZe+Xja7tUB2DNkQ==",
782
+ "dev": true,
783
+ "license": "MIT",
784
+ "dependencies": {
785
+ "undici-types": "~6.21.0"
786
+ }
787
+ },
788
+ "node_modules/@types/react": {
789
+ "version": "19.1.5",
790
+ "resolved": "https://registry.npmmirror.com/@types/react/-/react-19.1.5.tgz",
791
+ "integrity": "sha512-piErsCVVbpMMT2r7wbawdZsq4xMvIAhQuac2gedQHysu1TZYEigE6pnFfgZT+/jQnrRuF5r+SHzuehFjfRjr4g==",
792
+ "dev": true,
793
+ "license": "MIT",
794
+ "dependencies": {
795
+ "csstype": "^3.0.2"
796
+ }
797
+ },
798
+ "node_modules/@types/trusted-types": {
799
+ "version": "2.0.7",
800
+ "resolved": "https://registry.npmmirror.com/@types/trusted-types/-/trusted-types-2.0.7.tgz",
801
+ "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
802
+ "license": "MIT",
803
+ "optional": true
804
+ },
805
+ "node_modules/accepts": {
806
+ "version": "2.0.0",
807
+ "resolved": "https://registry.npmmirror.com/accepts/-/accepts-2.0.0.tgz",
808
+ "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",
809
+ "license": "MIT",
810
+ "peer": true,
811
+ "dependencies": {
812
+ "mime-types": "^3.0.0",
813
+ "negotiator": "^1.0.0"
814
+ },
815
+ "engines": {
816
+ "node": ">= 0.6"
817
+ }
818
+ },
819
+ "node_modules/agent-base": {
820
+ "version": "7.1.3",
821
+ "resolved": "https://registry.npmmirror.com/agent-base/-/agent-base-7.1.3.tgz",
822
+ "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==",
823
+ "license": "MIT",
824
+ "engines": {
825
+ "node": ">= 14"
826
+ }
827
+ },
828
+ "node_modules/ajv": {
829
+ "version": "6.12.6",
830
+ "resolved": "https://registry.npmmirror.com/ajv/-/ajv-6.12.6.tgz",
831
+ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
832
+ "license": "MIT",
833
+ "peer": true,
834
+ "dependencies": {
835
+ "fast-deep-equal": "^3.1.1",
836
+ "fast-json-stable-stringify": "^2.0.0",
837
+ "json-schema-traverse": "^0.4.1",
838
+ "uri-js": "^4.2.2"
839
+ },
840
+ "funding": {
841
+ "type": "github",
842
+ "url": "https://github.com/sponsors/epoberezkin"
843
+ }
844
+ },
845
+ "node_modules/base64-js": {
846
+ "version": "1.5.1",
847
+ "resolved": "https://registry.npmmirror.com/base64-js/-/base64-js-1.5.1.tgz",
848
+ "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
849
+ "funding": [
850
+ {
851
+ "type": "github",
852
+ "url": "https://github.com/sponsors/feross"
853
+ },
854
+ {
855
+ "type": "patreon",
856
+ "url": "https://www.patreon.com/feross"
857
+ },
858
+ {
859
+ "type": "consulting",
860
+ "url": "https://feross.org/support"
861
+ }
862
+ ],
863
+ "license": "MIT"
864
+ },
865
+ "node_modules/bignumber.js": {
866
+ "version": "9.3.0",
867
+ "resolved": "https://registry.npmmirror.com/bignumber.js/-/bignumber.js-9.3.0.tgz",
868
+ "integrity": "sha512-EM7aMFTXbptt/wZdMlBv2t8IViwQL+h6SLHosp8Yf0dqJMTnY6iL32opnAB6kAdL0SZPuvcAzFr31o0c/R3/RA==",
869
+ "license": "MIT",
870
+ "engines": {
871
+ "node": "*"
872
+ }
873
+ },
874
+ "node_modules/body-parser": {
875
+ "version": "2.2.0",
876
+ "resolved": "https://registry.npmmirror.com/body-parser/-/body-parser-2.2.0.tgz",
877
+ "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==",
878
+ "license": "MIT",
879
+ "peer": true,
880
+ "dependencies": {
881
+ "bytes": "^3.1.2",
882
+ "content-type": "^1.0.5",
883
+ "debug": "^4.4.0",
884
+ "http-errors": "^2.0.0",
885
+ "iconv-lite": "^0.6.3",
886
+ "on-finished": "^2.4.1",
887
+ "qs": "^6.14.0",
888
+ "raw-body": "^3.0.0",
889
+ "type-is": "^2.0.0"
890
+ },
891
+ "engines": {
892
+ "node": ">=18"
893
+ }
894
+ },
895
+ "node_modules/buffer-equal-constant-time": {
896
+ "version": "1.0.1",
897
+ "resolved": "https://registry.npmmirror.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
898
+ "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
899
+ "license": "BSD-3-Clause"
900
+ },
901
+ "node_modules/bytes": {
902
+ "version": "3.1.2",
903
+ "resolved": "https://registry.npmmirror.com/bytes/-/bytes-3.1.2.tgz",
904
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
905
+ "license": "MIT",
906
+ "peer": true,
907
+ "engines": {
908
+ "node": ">= 0.8"
909
+ }
910
+ },
911
+ "node_modules/call-bind-apply-helpers": {
912
+ "version": "1.0.2",
913
+ "resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
914
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
915
+ "license": "MIT",
916
+ "peer": true,
917
+ "dependencies": {
918
+ "es-errors": "^1.3.0",
919
+ "function-bind": "^1.1.2"
920
+ },
921
+ "engines": {
922
+ "node": ">= 0.4"
923
+ }
924
+ },
925
+ "node_modules/call-bound": {
926
+ "version": "1.0.4",
927
+ "resolved": "https://registry.npmmirror.com/call-bound/-/call-bound-1.0.4.tgz",
928
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
929
+ "license": "MIT",
930
+ "peer": true,
931
+ "dependencies": {
932
+ "call-bind-apply-helpers": "^1.0.2",
933
+ "get-intrinsic": "^1.3.0"
934
+ },
935
+ "engines": {
936
+ "node": ">= 0.4"
937
+ },
938
+ "funding": {
939
+ "url": "https://github.com/sponsors/ljharb"
940
+ }
941
+ },
942
+ "node_modules/content-disposition": {
943
+ "version": "1.0.0",
944
+ "resolved": "https://registry.npmmirror.com/content-disposition/-/content-disposition-1.0.0.tgz",
945
+ "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==",
946
+ "license": "MIT",
947
+ "peer": true,
948
+ "dependencies": {
949
+ "safe-buffer": "5.2.1"
950
+ },
951
+ "engines": {
952
+ "node": ">= 0.6"
953
+ }
954
+ },
955
+ "node_modules/content-type": {
956
+ "version": "1.0.5",
957
+ "resolved": "https://registry.npmmirror.com/content-type/-/content-type-1.0.5.tgz",
958
+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
959
+ "license": "MIT",
960
+ "peer": true,
961
+ "engines": {
962
+ "node": ">= 0.6"
963
+ }
964
+ },
965
+ "node_modules/cookie": {
966
+ "version": "0.7.2",
967
+ "resolved": "https://registry.npmmirror.com/cookie/-/cookie-0.7.2.tgz",
968
+ "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
969
+ "license": "MIT",
970
+ "peer": true,
971
+ "engines": {
972
+ "node": ">= 0.6"
973
+ }
974
+ },
975
+ "node_modules/cookie-signature": {
976
+ "version": "1.2.2",
977
+ "resolved": "https://registry.npmmirror.com/cookie-signature/-/cookie-signature-1.2.2.tgz",
978
+ "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==",
979
+ "license": "MIT",
980
+ "peer": true,
981
+ "engines": {
982
+ "node": ">=6.6.0"
983
+ }
984
+ },
985
+ "node_modules/cors": {
986
+ "version": "2.8.5",
987
+ "resolved": "https://registry.npmmirror.com/cors/-/cors-2.8.5.tgz",
988
+ "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
989
+ "license": "MIT",
990
+ "peer": true,
991
+ "dependencies": {
992
+ "object-assign": "^4",
993
+ "vary": "^1"
994
+ },
995
+ "engines": {
996
+ "node": ">= 0.10"
997
+ }
998
+ },
999
+ "node_modules/cross-spawn": {
1000
+ "version": "7.0.6",
1001
+ "resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz",
1002
+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
1003
+ "license": "MIT",
1004
+ "peer": true,
1005
+ "dependencies": {
1006
+ "path-key": "^3.1.0",
1007
+ "shebang-command": "^2.0.0",
1008
+ "which": "^2.0.1"
1009
+ },
1010
+ "engines": {
1011
+ "node": ">= 8"
1012
+ }
1013
+ },
1014
+ "node_modules/csstype": {
1015
+ "version": "3.1.3",
1016
+ "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.1.3.tgz",
1017
+ "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
1018
+ "dev": true,
1019
+ "license": "MIT"
1020
+ },
1021
+ "node_modules/debug": {
1022
+ "version": "4.4.1",
1023
+ "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.1.tgz",
1024
+ "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
1025
+ "license": "MIT",
1026
+ "dependencies": {
1027
+ "ms": "^2.1.3"
1028
+ },
1029
+ "engines": {
1030
+ "node": ">=6.0"
1031
+ },
1032
+ "peerDependenciesMeta": {
1033
+ "supports-color": {
1034
+ "optional": true
1035
+ }
1036
+ }
1037
+ },
1038
+ "node_modules/depd": {
1039
+ "version": "2.0.0",
1040
+ "resolved": "https://registry.npmmirror.com/depd/-/depd-2.0.0.tgz",
1041
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
1042
+ "license": "MIT",
1043
+ "peer": true,
1044
+ "engines": {
1045
+ "node": ">= 0.8"
1046
+ }
1047
+ },
1048
+ "node_modules/dompurify": {
1049
+ "version": "3.2.6",
1050
+ "resolved": "https://registry.npmmirror.com/dompurify/-/dompurify-3.2.6.tgz",
1051
+ "integrity": "sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ==",
1052
+ "license": "(MPL-2.0 OR Apache-2.0)",
1053
+ "optionalDependencies": {
1054
+ "@types/trusted-types": "^2.0.7"
1055
+ }
1056
+ },
1057
+ "node_modules/dunder-proto": {
1058
+ "version": "1.0.1",
1059
+ "resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz",
1060
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
1061
+ "license": "MIT",
1062
+ "peer": true,
1063
+ "dependencies": {
1064
+ "call-bind-apply-helpers": "^1.0.1",
1065
+ "es-errors": "^1.3.0",
1066
+ "gopd": "^1.2.0"
1067
+ },
1068
+ "engines": {
1069
+ "node": ">= 0.4"
1070
+ }
1071
+ },
1072
+ "node_modules/ecdsa-sig-formatter": {
1073
+ "version": "1.0.11",
1074
+ "resolved": "https://registry.npmmirror.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
1075
+ "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
1076
+ "license": "Apache-2.0",
1077
+ "dependencies": {
1078
+ "safe-buffer": "^5.0.1"
1079
+ }
1080
+ },
1081
+ "node_modules/ee-first": {
1082
+ "version": "1.1.1",
1083
+ "resolved": "https://registry.npmmirror.com/ee-first/-/ee-first-1.1.1.tgz",
1084
+ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
1085
+ "license": "MIT",
1086
+ "peer": true
1087
+ },
1088
+ "node_modules/encodeurl": {
1089
+ "version": "2.0.0",
1090
+ "resolved": "https://registry.npmmirror.com/encodeurl/-/encodeurl-2.0.0.tgz",
1091
+ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
1092
+ "license": "MIT",
1093
+ "peer": true,
1094
+ "engines": {
1095
+ "node": ">= 0.8"
1096
+ }
1097
+ },
1098
+ "node_modules/es-define-property": {
1099
+ "version": "1.0.1",
1100
+ "resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz",
1101
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
1102
+ "license": "MIT",
1103
+ "peer": true,
1104
+ "engines": {
1105
+ "node": ">= 0.4"
1106
+ }
1107
+ },
1108
+ "node_modules/es-errors": {
1109
+ "version": "1.3.0",
1110
+ "resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz",
1111
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
1112
+ "license": "MIT",
1113
+ "peer": true,
1114
+ "engines": {
1115
+ "node": ">= 0.4"
1116
+ }
1117
+ },
1118
+ "node_modules/es-object-atoms": {
1119
+ "version": "1.1.1",
1120
+ "resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
1121
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
1122
+ "license": "MIT",
1123
+ "peer": true,
1124
+ "dependencies": {
1125
+ "es-errors": "^1.3.0"
1126
+ },
1127
+ "engines": {
1128
+ "node": ">= 0.4"
1129
+ }
1130
+ },
1131
+ "node_modules/esbuild": {
1132
+ "version": "0.25.4",
1133
+ "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.25.4.tgz",
1134
+ "integrity": "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q==",
1135
+ "dev": true,
1136
+ "hasInstallScript": true,
1137
+ "license": "MIT",
1138
+ "bin": {
1139
+ "esbuild": "bin/esbuild"
1140
+ },
1141
+ "engines": {
1142
+ "node": ">=18"
1143
+ },
1144
+ "optionalDependencies": {
1145
+ "@esbuild/aix-ppc64": "0.25.4",
1146
+ "@esbuild/android-arm": "0.25.4",
1147
+ "@esbuild/android-arm64": "0.25.4",
1148
+ "@esbuild/android-x64": "0.25.4",
1149
+ "@esbuild/darwin-arm64": "0.25.4",
1150
+ "@esbuild/darwin-x64": "0.25.4",
1151
+ "@esbuild/freebsd-arm64": "0.25.4",
1152
+ "@esbuild/freebsd-x64": "0.25.4",
1153
+ "@esbuild/linux-arm": "0.25.4",
1154
+ "@esbuild/linux-arm64": "0.25.4",
1155
+ "@esbuild/linux-ia32": "0.25.4",
1156
+ "@esbuild/linux-loong64": "0.25.4",
1157
+ "@esbuild/linux-mips64el": "0.25.4",
1158
+ "@esbuild/linux-ppc64": "0.25.4",
1159
+ "@esbuild/linux-riscv64": "0.25.4",
1160
+ "@esbuild/linux-s390x": "0.25.4",
1161
+ "@esbuild/linux-x64": "0.25.4",
1162
+ "@esbuild/netbsd-arm64": "0.25.4",
1163
+ "@esbuild/netbsd-x64": "0.25.4",
1164
+ "@esbuild/openbsd-arm64": "0.25.4",
1165
+ "@esbuild/openbsd-x64": "0.25.4",
1166
+ "@esbuild/sunos-x64": "0.25.4",
1167
+ "@esbuild/win32-arm64": "0.25.4",
1168
+ "@esbuild/win32-ia32": "0.25.4",
1169
+ "@esbuild/win32-x64": "0.25.4"
1170
+ }
1171
+ },
1172
+ "node_modules/escape-html": {
1173
+ "version": "1.0.3",
1174
+ "resolved": "https://registry.npmmirror.com/escape-html/-/escape-html-1.0.3.tgz",
1175
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
1176
+ "license": "MIT",
1177
+ "peer": true
1178
+ },
1179
+ "node_modules/etag": {
1180
+ "version": "1.8.1",
1181
+ "resolved": "https://registry.npmmirror.com/etag/-/etag-1.8.1.tgz",
1182
+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
1183
+ "license": "MIT",
1184
+ "peer": true,
1185
+ "engines": {
1186
+ "node": ">= 0.6"
1187
+ }
1188
+ },
1189
+ "node_modules/eventsource": {
1190
+ "version": "3.0.7",
1191
+ "resolved": "https://registry.npmmirror.com/eventsource/-/eventsource-3.0.7.tgz",
1192
+ "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==",
1193
+ "license": "MIT",
1194
+ "peer": true,
1195
+ "dependencies": {
1196
+ "eventsource-parser": "^3.0.1"
1197
+ },
1198
+ "engines": {
1199
+ "node": ">=18.0.0"
1200
+ }
1201
+ },
1202
+ "node_modules/eventsource-parser": {
1203
+ "version": "3.0.2",
1204
+ "resolved": "https://registry.npmmirror.com/eventsource-parser/-/eventsource-parser-3.0.2.tgz",
1205
+ "integrity": "sha512-6RxOBZ/cYgd8usLwsEl+EC09Au/9BcmCKYF2/xbml6DNczf7nv0MQb+7BA2F+li6//I+28VNlQR37XfQtcAJuA==",
1206
+ "license": "MIT",
1207
+ "peer": true,
1208
+ "engines": {
1209
+ "node": ">=18.0.0"
1210
+ }
1211
+ },
1212
+ "node_modules/express": {
1213
+ "version": "5.1.0",
1214
+ "resolved": "https://registry.npmmirror.com/express/-/express-5.1.0.tgz",
1215
+ "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==",
1216
+ "license": "MIT",
1217
+ "peer": true,
1218
+ "dependencies": {
1219
+ "accepts": "^2.0.0",
1220
+ "body-parser": "^2.2.0",
1221
+ "content-disposition": "^1.0.0",
1222
+ "content-type": "^1.0.5",
1223
+ "cookie": "^0.7.1",
1224
+ "cookie-signature": "^1.2.1",
1225
+ "debug": "^4.4.0",
1226
+ "encodeurl": "^2.0.0",
1227
+ "escape-html": "^1.0.3",
1228
+ "etag": "^1.8.1",
1229
+ "finalhandler": "^2.1.0",
1230
+ "fresh": "^2.0.0",
1231
+ "http-errors": "^2.0.0",
1232
+ "merge-descriptors": "^2.0.0",
1233
+ "mime-types": "^3.0.0",
1234
+ "on-finished": "^2.4.1",
1235
+ "once": "^1.4.0",
1236
+ "parseurl": "^1.3.3",
1237
+ "proxy-addr": "^2.0.7",
1238
+ "qs": "^6.14.0",
1239
+ "range-parser": "^1.2.1",
1240
+ "router": "^2.2.0",
1241
+ "send": "^1.1.0",
1242
+ "serve-static": "^2.2.0",
1243
+ "statuses": "^2.0.1",
1244
+ "type-is": "^2.0.1",
1245
+ "vary": "^1.1.2"
1246
+ },
1247
+ "engines": {
1248
+ "node": ">= 18"
1249
+ },
1250
+ "funding": {
1251
+ "type": "opencollective",
1252
+ "url": "https://opencollective.com/express"
1253
+ }
1254
+ },
1255
+ "node_modules/express-rate-limit": {
1256
+ "version": "7.5.0",
1257
+ "resolved": "https://registry.npmmirror.com/express-rate-limit/-/express-rate-limit-7.5.0.tgz",
1258
+ "integrity": "sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg==",
1259
+ "license": "MIT",
1260
+ "peer": true,
1261
+ "engines": {
1262
+ "node": ">= 16"
1263
+ },
1264
+ "funding": {
1265
+ "url": "https://github.com/sponsors/express-rate-limit"
1266
+ },
1267
+ "peerDependencies": {
1268
+ "express": "^4.11 || 5 || ^5.0.0-beta.1"
1269
+ }
1270
+ },
1271
+ "node_modules/extend": {
1272
+ "version": "3.0.2",
1273
+ "resolved": "https://registry.npmmirror.com/extend/-/extend-3.0.2.tgz",
1274
+ "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
1275
+ "license": "MIT"
1276
+ },
1277
+ "node_modules/fast-deep-equal": {
1278
+ "version": "3.1.3",
1279
+ "resolved": "https://registry.npmmirror.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
1280
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
1281
+ "license": "MIT",
1282
+ "peer": true
1283
+ },
1284
+ "node_modules/fast-json-stable-stringify": {
1285
+ "version": "2.1.0",
1286
+ "resolved": "https://registry.npmmirror.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
1287
+ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
1288
+ "license": "MIT",
1289
+ "peer": true
1290
+ },
1291
+ "node_modules/fdir": {
1292
+ "version": "6.4.4",
1293
+ "resolved": "https://registry.npmmirror.com/fdir/-/fdir-6.4.4.tgz",
1294
+ "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==",
1295
+ "dev": true,
1296
+ "license": "MIT",
1297
+ "peerDependencies": {
1298
+ "picomatch": "^3 || ^4"
1299
+ },
1300
+ "peerDependenciesMeta": {
1301
+ "picomatch": {
1302
+ "optional": true
1303
+ }
1304
+ }
1305
+ },
1306
+ "node_modules/finalhandler": {
1307
+ "version": "2.1.0",
1308
+ "resolved": "https://registry.npmmirror.com/finalhandler/-/finalhandler-2.1.0.tgz",
1309
+ "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==",
1310
+ "license": "MIT",
1311
+ "peer": true,
1312
+ "dependencies": {
1313
+ "debug": "^4.4.0",
1314
+ "encodeurl": "^2.0.0",
1315
+ "escape-html": "^1.0.3",
1316
+ "on-finished": "^2.4.1",
1317
+ "parseurl": "^1.3.3",
1318
+ "statuses": "^2.0.1"
1319
+ },
1320
+ "engines": {
1321
+ "node": ">= 0.8"
1322
+ }
1323
+ },
1324
+ "node_modules/forwarded": {
1325
+ "version": "0.2.0",
1326
+ "resolved": "https://registry.npmmirror.com/forwarded/-/forwarded-0.2.0.tgz",
1327
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
1328
+ "license": "MIT",
1329
+ "peer": true,
1330
+ "engines": {
1331
+ "node": ">= 0.6"
1332
+ }
1333
+ },
1334
+ "node_modules/fresh": {
1335
+ "version": "2.0.0",
1336
+ "resolved": "https://registry.npmmirror.com/fresh/-/fresh-2.0.0.tgz",
1337
+ "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
1338
+ "license": "MIT",
1339
+ "peer": true,
1340
+ "engines": {
1341
+ "node": ">= 0.8"
1342
+ }
1343
+ },
1344
+ "node_modules/fsevents": {
1345
+ "version": "2.3.3",
1346
+ "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz",
1347
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
1348
+ "dev": true,
1349
+ "hasInstallScript": true,
1350
+ "license": "MIT",
1351
+ "optional": true,
1352
+ "os": [
1353
+ "darwin"
1354
+ ],
1355
+ "engines": {
1356
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
1357
+ }
1358
+ },
1359
+ "node_modules/function-bind": {
1360
+ "version": "1.1.2",
1361
+ "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz",
1362
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
1363
+ "license": "MIT",
1364
+ "peer": true,
1365
+ "funding": {
1366
+ "url": "https://github.com/sponsors/ljharb"
1367
+ }
1368
+ },
1369
+ "node_modules/gaxios": {
1370
+ "version": "6.7.1",
1371
+ "resolved": "https://registry.npmmirror.com/gaxios/-/gaxios-6.7.1.tgz",
1372
+ "integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==",
1373
+ "license": "Apache-2.0",
1374
+ "dependencies": {
1375
+ "extend": "^3.0.2",
1376
+ "https-proxy-agent": "^7.0.1",
1377
+ "is-stream": "^2.0.0",
1378
+ "node-fetch": "^2.6.9",
1379
+ "uuid": "^9.0.1"
1380
+ },
1381
+ "engines": {
1382
+ "node": ">=14"
1383
+ }
1384
+ },
1385
+ "node_modules/gcp-metadata": {
1386
+ "version": "6.1.1",
1387
+ "resolved": "https://registry.npmmirror.com/gcp-metadata/-/gcp-metadata-6.1.1.tgz",
1388
+ "integrity": "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==",
1389
+ "license": "Apache-2.0",
1390
+ "dependencies": {
1391
+ "gaxios": "^6.1.1",
1392
+ "google-logging-utils": "^0.0.2",
1393
+ "json-bigint": "^1.0.0"
1394
+ },
1395
+ "engines": {
1396
+ "node": ">=14"
1397
+ }
1398
+ },
1399
+ "node_modules/get-intrinsic": {
1400
+ "version": "1.3.0",
1401
+ "resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
1402
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
1403
+ "license": "MIT",
1404
+ "peer": true,
1405
+ "dependencies": {
1406
+ "call-bind-apply-helpers": "^1.0.2",
1407
+ "es-define-property": "^1.0.1",
1408
+ "es-errors": "^1.3.0",
1409
+ "es-object-atoms": "^1.1.1",
1410
+ "function-bind": "^1.1.2",
1411
+ "get-proto": "^1.0.1",
1412
+ "gopd": "^1.2.0",
1413
+ "has-symbols": "^1.1.0",
1414
+ "hasown": "^2.0.2",
1415
+ "math-intrinsics": "^1.1.0"
1416
+ },
1417
+ "engines": {
1418
+ "node": ">= 0.4"
1419
+ },
1420
+ "funding": {
1421
+ "url": "https://github.com/sponsors/ljharb"
1422
+ }
1423
+ },
1424
+ "node_modules/get-proto": {
1425
+ "version": "1.0.1",
1426
+ "resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz",
1427
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
1428
+ "license": "MIT",
1429
+ "peer": true,
1430
+ "dependencies": {
1431
+ "dunder-proto": "^1.0.1",
1432
+ "es-object-atoms": "^1.0.0"
1433
+ },
1434
+ "engines": {
1435
+ "node": ">= 0.4"
1436
+ }
1437
+ },
1438
+ "node_modules/google-auth-library": {
1439
+ "version": "9.15.1",
1440
+ "resolved": "https://registry.npmmirror.com/google-auth-library/-/google-auth-library-9.15.1.tgz",
1441
+ "integrity": "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==",
1442
+ "license": "Apache-2.0",
1443
+ "dependencies": {
1444
+ "base64-js": "^1.3.0",
1445
+ "ecdsa-sig-formatter": "^1.0.11",
1446
+ "gaxios": "^6.1.1",
1447
+ "gcp-metadata": "^6.1.0",
1448
+ "gtoken": "^7.0.0",
1449
+ "jws": "^4.0.0"
1450
+ },
1451
+ "engines": {
1452
+ "node": ">=14"
1453
+ }
1454
+ },
1455
+ "node_modules/google-logging-utils": {
1456
+ "version": "0.0.2",
1457
+ "resolved": "https://registry.npmmirror.com/google-logging-utils/-/google-logging-utils-0.0.2.tgz",
1458
+ "integrity": "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==",
1459
+ "license": "Apache-2.0",
1460
+ "engines": {
1461
+ "node": ">=14"
1462
+ }
1463
+ },
1464
+ "node_modules/gopd": {
1465
+ "version": "1.2.0",
1466
+ "resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz",
1467
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
1468
+ "license": "MIT",
1469
+ "peer": true,
1470
+ "engines": {
1471
+ "node": ">= 0.4"
1472
+ },
1473
+ "funding": {
1474
+ "url": "https://github.com/sponsors/ljharb"
1475
+ }
1476
+ },
1477
+ "node_modules/gtoken": {
1478
+ "version": "7.1.0",
1479
+ "resolved": "https://registry.npmmirror.com/gtoken/-/gtoken-7.1.0.tgz",
1480
+ "integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==",
1481
+ "license": "MIT",
1482
+ "dependencies": {
1483
+ "gaxios": "^6.0.0",
1484
+ "jws": "^4.0.0"
1485
+ },
1486
+ "engines": {
1487
+ "node": ">=14.0.0"
1488
+ }
1489
+ },
1490
+ "node_modules/has-symbols": {
1491
+ "version": "1.1.0",
1492
+ "resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz",
1493
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
1494
+ "license": "MIT",
1495
+ "peer": true,
1496
+ "engines": {
1497
+ "node": ">= 0.4"
1498
+ },
1499
+ "funding": {
1500
+ "url": "https://github.com/sponsors/ljharb"
1501
+ }
1502
+ },
1503
+ "node_modules/hasown": {
1504
+ "version": "2.0.2",
1505
+ "resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz",
1506
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
1507
+ "license": "MIT",
1508
+ "peer": true,
1509
+ "dependencies": {
1510
+ "function-bind": "^1.1.2"
1511
+ },
1512
+ "engines": {
1513
+ "node": ">= 0.4"
1514
+ }
1515
+ },
1516
+ "node_modules/http-errors": {
1517
+ "version": "2.0.0",
1518
+ "resolved": "https://registry.npmmirror.com/http-errors/-/http-errors-2.0.0.tgz",
1519
+ "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
1520
+ "license": "MIT",
1521
+ "peer": true,
1522
+ "dependencies": {
1523
+ "depd": "2.0.0",
1524
+ "inherits": "2.0.4",
1525
+ "setprototypeof": "1.2.0",
1526
+ "statuses": "2.0.1",
1527
+ "toidentifier": "1.0.1"
1528
+ },
1529
+ "engines": {
1530
+ "node": ">= 0.8"
1531
+ }
1532
+ },
1533
+ "node_modules/https-proxy-agent": {
1534
+ "version": "7.0.6",
1535
+ "resolved": "https://registry.npmmirror.com/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
1536
+ "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
1537
+ "license": "MIT",
1538
+ "dependencies": {
1539
+ "agent-base": "^7.1.2",
1540
+ "debug": "4"
1541
+ },
1542
+ "engines": {
1543
+ "node": ">= 14"
1544
+ }
1545
+ },
1546
+ "node_modules/iconv-lite": {
1547
+ "version": "0.6.3",
1548
+ "resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.6.3.tgz",
1549
+ "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
1550
+ "license": "MIT",
1551
+ "peer": true,
1552
+ "dependencies": {
1553
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
1554
+ },
1555
+ "engines": {
1556
+ "node": ">=0.10.0"
1557
+ }
1558
+ },
1559
+ "node_modules/inherits": {
1560
+ "version": "2.0.4",
1561
+ "resolved": "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz",
1562
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
1563
+ "license": "ISC",
1564
+ "peer": true
1565
+ },
1566
+ "node_modules/ipaddr.js": {
1567
+ "version": "1.9.1",
1568
+ "resolved": "https://registry.npmmirror.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
1569
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
1570
+ "license": "MIT",
1571
+ "peer": true,
1572
+ "engines": {
1573
+ "node": ">= 0.10"
1574
+ }
1575
+ },
1576
+ "node_modules/is-promise": {
1577
+ "version": "4.0.0",
1578
+ "resolved": "https://registry.npmmirror.com/is-promise/-/is-promise-4.0.0.tgz",
1579
+ "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
1580
+ "license": "MIT",
1581
+ "peer": true
1582
+ },
1583
+ "node_modules/is-stream": {
1584
+ "version": "2.0.1",
1585
+ "resolved": "https://registry.npmmirror.com/is-stream/-/is-stream-2.0.1.tgz",
1586
+ "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
1587
+ "license": "MIT",
1588
+ "engines": {
1589
+ "node": ">=8"
1590
+ },
1591
+ "funding": {
1592
+ "url": "https://github.com/sponsors/sindresorhus"
1593
+ }
1594
+ },
1595
+ "node_modules/isexe": {
1596
+ "version": "2.0.0",
1597
+ "resolved": "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz",
1598
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
1599
+ "license": "ISC",
1600
+ "peer": true
1601
+ },
1602
+ "node_modules/json-bigint": {
1603
+ "version": "1.0.0",
1604
+ "resolved": "https://registry.npmmirror.com/json-bigint/-/json-bigint-1.0.0.tgz",
1605
+ "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==",
1606
+ "license": "MIT",
1607
+ "dependencies": {
1608
+ "bignumber.js": "^9.0.0"
1609
+ }
1610
+ },
1611
+ "node_modules/json-schema-traverse": {
1612
+ "version": "0.4.1",
1613
+ "resolved": "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
1614
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
1615
+ "license": "MIT",
1616
+ "peer": true
1617
+ },
1618
+ "node_modules/jwa": {
1619
+ "version": "2.0.1",
1620
+ "resolved": "https://registry.npmmirror.com/jwa/-/jwa-2.0.1.tgz",
1621
+ "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==",
1622
+ "license": "MIT",
1623
+ "dependencies": {
1624
+ "buffer-equal-constant-time": "^1.0.1",
1625
+ "ecdsa-sig-formatter": "1.0.11",
1626
+ "safe-buffer": "^5.0.1"
1627
+ }
1628
+ },
1629
+ "node_modules/jws": {
1630
+ "version": "4.0.0",
1631
+ "resolved": "https://registry.npmmirror.com/jws/-/jws-4.0.0.tgz",
1632
+ "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==",
1633
+ "license": "MIT",
1634
+ "dependencies": {
1635
+ "jwa": "^2.0.0",
1636
+ "safe-buffer": "^5.0.1"
1637
+ }
1638
+ },
1639
+ "node_modules/lucide-react": {
1640
+ "version": "0.511.0",
1641
+ "resolved": "https://registry.npmmirror.com/lucide-react/-/lucide-react-0.511.0.tgz",
1642
+ "integrity": "sha512-VK5a2ydJ7xm8GvBeKLS9mu1pVK6ucef9780JVUjw6bAjJL/QXnd4Y0p7SPeOUMC27YhzNCZvm5d/QX0Tp3rc0w==",
1643
+ "license": "ISC",
1644
+ "peerDependencies": {
1645
+ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
1646
+ }
1647
+ },
1648
+ "node_modules/marked": {
1649
+ "version": "13.0.3",
1650
+ "resolved": "https://registry.npmmirror.com/marked/-/marked-13.0.3.tgz",
1651
+ "integrity": "sha512-rqRix3/TWzE9rIoFGIn8JmsVfhiuC8VIQ8IdX5TfzmeBucdY05/0UlzKaw0eVtpcN/OdVFpBk7CjKGo9iHJ/zA==",
1652
+ "license": "MIT",
1653
+ "bin": {
1654
+ "marked": "bin/marked.js"
1655
+ },
1656
+ "engines": {
1657
+ "node": ">= 18"
1658
+ }
1659
+ },
1660
+ "node_modules/math-intrinsics": {
1661
+ "version": "1.1.0",
1662
+ "resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
1663
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
1664
+ "license": "MIT",
1665
+ "peer": true,
1666
+ "engines": {
1667
+ "node": ">= 0.4"
1668
+ }
1669
+ },
1670
+ "node_modules/media-typer": {
1671
+ "version": "1.1.0",
1672
+ "resolved": "https://registry.npmmirror.com/media-typer/-/media-typer-1.1.0.tgz",
1673
+ "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==",
1674
+ "license": "MIT",
1675
+ "peer": true,
1676
+ "engines": {
1677
+ "node": ">= 0.8"
1678
+ }
1679
+ },
1680
+ "node_modules/merge-descriptors": {
1681
+ "version": "2.0.0",
1682
+ "resolved": "https://registry.npmmirror.com/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
1683
+ "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==",
1684
+ "license": "MIT",
1685
+ "peer": true,
1686
+ "engines": {
1687
+ "node": ">=18"
1688
+ },
1689
+ "funding": {
1690
+ "url": "https://github.com/sponsors/sindresorhus"
1691
+ }
1692
+ },
1693
+ "node_modules/mime-db": {
1694
+ "version": "1.54.0",
1695
+ "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.54.0.tgz",
1696
+ "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
1697
+ "license": "MIT",
1698
+ "peer": true,
1699
+ "engines": {
1700
+ "node": ">= 0.6"
1701
+ }
1702
+ },
1703
+ "node_modules/mime-types": {
1704
+ "version": "3.0.1",
1705
+ "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-3.0.1.tgz",
1706
+ "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==",
1707
+ "license": "MIT",
1708
+ "peer": true,
1709
+ "dependencies": {
1710
+ "mime-db": "^1.54.0"
1711
+ },
1712
+ "engines": {
1713
+ "node": ">= 0.6"
1714
+ }
1715
+ },
1716
+ "node_modules/ms": {
1717
+ "version": "2.1.3",
1718
+ "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz",
1719
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
1720
+ "license": "MIT"
1721
+ },
1722
+ "node_modules/nanoid": {
1723
+ "version": "3.3.11",
1724
+ "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.11.tgz",
1725
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
1726
+ "dev": true,
1727
+ "funding": [
1728
+ {
1729
+ "type": "github",
1730
+ "url": "https://github.com/sponsors/ai"
1731
+ }
1732
+ ],
1733
+ "license": "MIT",
1734
+ "bin": {
1735
+ "nanoid": "bin/nanoid.cjs"
1736
+ },
1737
+ "engines": {
1738
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
1739
+ }
1740
+ },
1741
+ "node_modules/negotiator": {
1742
+ "version": "1.0.0",
1743
+ "resolved": "https://registry.npmmirror.com/negotiator/-/negotiator-1.0.0.tgz",
1744
+ "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
1745
+ "license": "MIT",
1746
+ "peer": true,
1747
+ "engines": {
1748
+ "node": ">= 0.6"
1749
+ }
1750
+ },
1751
+ "node_modules/node-fetch": {
1752
+ "version": "2.7.0",
1753
+ "resolved": "https://registry.npmmirror.com/node-fetch/-/node-fetch-2.7.0.tgz",
1754
+ "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
1755
+ "license": "MIT",
1756
+ "dependencies": {
1757
+ "whatwg-url": "^5.0.0"
1758
+ },
1759
+ "engines": {
1760
+ "node": "4.x || >=6.0.0"
1761
+ },
1762
+ "peerDependencies": {
1763
+ "encoding": "^0.1.0"
1764
+ },
1765
+ "peerDependenciesMeta": {
1766
+ "encoding": {
1767
+ "optional": true
1768
+ }
1769
+ }
1770
+ },
1771
+ "node_modules/object-assign": {
1772
+ "version": "4.1.1",
1773
+ "resolved": "https://registry.npmmirror.com/object-assign/-/object-assign-4.1.1.tgz",
1774
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
1775
+ "license": "MIT",
1776
+ "peer": true,
1777
+ "engines": {
1778
+ "node": ">=0.10.0"
1779
+ }
1780
+ },
1781
+ "node_modules/object-inspect": {
1782
+ "version": "1.13.4",
1783
+ "resolved": "https://registry.npmmirror.com/object-inspect/-/object-inspect-1.13.4.tgz",
1784
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
1785
+ "license": "MIT",
1786
+ "peer": true,
1787
+ "engines": {
1788
+ "node": ">= 0.4"
1789
+ },
1790
+ "funding": {
1791
+ "url": "https://github.com/sponsors/ljharb"
1792
+ }
1793
+ },
1794
+ "node_modules/on-finished": {
1795
+ "version": "2.4.1",
1796
+ "resolved": "https://registry.npmmirror.com/on-finished/-/on-finished-2.4.1.tgz",
1797
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
1798
+ "license": "MIT",
1799
+ "peer": true,
1800
+ "dependencies": {
1801
+ "ee-first": "1.1.1"
1802
+ },
1803
+ "engines": {
1804
+ "node": ">= 0.8"
1805
+ }
1806
+ },
1807
+ "node_modules/once": {
1808
+ "version": "1.4.0",
1809
+ "resolved": "https://registry.npmmirror.com/once/-/once-1.4.0.tgz",
1810
+ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
1811
+ "license": "ISC",
1812
+ "peer": true,
1813
+ "dependencies": {
1814
+ "wrappy": "1"
1815
+ }
1816
+ },
1817
+ "node_modules/parseurl": {
1818
+ "version": "1.3.3",
1819
+ "resolved": "https://registry.npmmirror.com/parseurl/-/parseurl-1.3.3.tgz",
1820
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
1821
+ "license": "MIT",
1822
+ "peer": true,
1823
+ "engines": {
1824
+ "node": ">= 0.8"
1825
+ }
1826
+ },
1827
+ "node_modules/path-key": {
1828
+ "version": "3.1.1",
1829
+ "resolved": "https://registry.npmmirror.com/path-key/-/path-key-3.1.1.tgz",
1830
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
1831
+ "license": "MIT",
1832
+ "peer": true,
1833
+ "engines": {
1834
+ "node": ">=8"
1835
+ }
1836
+ },
1837
+ "node_modules/path-to-regexp": {
1838
+ "version": "8.2.0",
1839
+ "resolved": "https://registry.npmmirror.com/path-to-regexp/-/path-to-regexp-8.2.0.tgz",
1840
+ "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==",
1841
+ "license": "MIT",
1842
+ "peer": true,
1843
+ "engines": {
1844
+ "node": ">=16"
1845
+ }
1846
+ },
1847
+ "node_modules/picocolors": {
1848
+ "version": "1.1.1",
1849
+ "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz",
1850
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
1851
+ "dev": true,
1852
+ "license": "ISC"
1853
+ },
1854
+ "node_modules/picomatch": {
1855
+ "version": "4.0.2",
1856
+ "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.2.tgz",
1857
+ "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
1858
+ "dev": true,
1859
+ "license": "MIT",
1860
+ "engines": {
1861
+ "node": ">=12"
1862
+ },
1863
+ "funding": {
1864
+ "url": "https://github.com/sponsors/jonschlinkert"
1865
+ }
1866
+ },
1867
+ "node_modules/pkce-challenge": {
1868
+ "version": "5.0.0",
1869
+ "resolved": "https://registry.npmmirror.com/pkce-challenge/-/pkce-challenge-5.0.0.tgz",
1870
+ "integrity": "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==",
1871
+ "license": "MIT",
1872
+ "peer": true,
1873
+ "engines": {
1874
+ "node": ">=16.20.0"
1875
+ }
1876
+ },
1877
+ "node_modules/postcss": {
1878
+ "version": "8.5.3",
1879
+ "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.3.tgz",
1880
+ "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==",
1881
+ "dev": true,
1882
+ "funding": [
1883
+ {
1884
+ "type": "opencollective",
1885
+ "url": "https://opencollective.com/postcss/"
1886
+ },
1887
+ {
1888
+ "type": "tidelift",
1889
+ "url": "https://tidelift.com/funding/github/npm/postcss"
1890
+ },
1891
+ {
1892
+ "type": "github",
1893
+ "url": "https://github.com/sponsors/ai"
1894
+ }
1895
+ ],
1896
+ "license": "MIT",
1897
+ "dependencies": {
1898
+ "nanoid": "^3.3.8",
1899
+ "picocolors": "^1.1.1",
1900
+ "source-map-js": "^1.2.1"
1901
+ },
1902
+ "engines": {
1903
+ "node": "^10 || ^12 || >=14"
1904
+ }
1905
+ },
1906
+ "node_modules/proxy-addr": {
1907
+ "version": "2.0.7",
1908
+ "resolved": "https://registry.npmmirror.com/proxy-addr/-/proxy-addr-2.0.7.tgz",
1909
+ "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
1910
+ "license": "MIT",
1911
+ "peer": true,
1912
+ "dependencies": {
1913
+ "forwarded": "0.2.0",
1914
+ "ipaddr.js": "1.9.1"
1915
+ },
1916
+ "engines": {
1917
+ "node": ">= 0.10"
1918
+ }
1919
+ },
1920
+ "node_modules/punycode": {
1921
+ "version": "2.3.1",
1922
+ "resolved": "https://registry.npmmirror.com/punycode/-/punycode-2.3.1.tgz",
1923
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
1924
+ "license": "MIT",
1925
+ "peer": true,
1926
+ "engines": {
1927
+ "node": ">=6"
1928
+ }
1929
+ },
1930
+ "node_modules/qs": {
1931
+ "version": "6.14.0",
1932
+ "resolved": "https://registry.npmmirror.com/qs/-/qs-6.14.0.tgz",
1933
+ "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==",
1934
+ "license": "BSD-3-Clause",
1935
+ "peer": true,
1936
+ "dependencies": {
1937
+ "side-channel": "^1.1.0"
1938
+ },
1939
+ "engines": {
1940
+ "node": ">=0.6"
1941
+ },
1942
+ "funding": {
1943
+ "url": "https://github.com/sponsors/ljharb"
1944
+ }
1945
+ },
1946
+ "node_modules/range-parser": {
1947
+ "version": "1.2.1",
1948
+ "resolved": "https://registry.npmmirror.com/range-parser/-/range-parser-1.2.1.tgz",
1949
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
1950
+ "license": "MIT",
1951
+ "peer": true,
1952
+ "engines": {
1953
+ "node": ">= 0.6"
1954
+ }
1955
+ },
1956
+ "node_modules/raw-body": {
1957
+ "version": "3.0.0",
1958
+ "resolved": "https://registry.npmmirror.com/raw-body/-/raw-body-3.0.0.tgz",
1959
+ "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==",
1960
+ "license": "MIT",
1961
+ "peer": true,
1962
+ "dependencies": {
1963
+ "bytes": "3.1.2",
1964
+ "http-errors": "2.0.0",
1965
+ "iconv-lite": "0.6.3",
1966
+ "unpipe": "1.0.0"
1967
+ },
1968
+ "engines": {
1969
+ "node": ">= 0.8"
1970
+ }
1971
+ },
1972
+ "node_modules/react": {
1973
+ "version": "19.1.0",
1974
+ "resolved": "https://registry.npmmirror.com/react/-/react-19.1.0.tgz",
1975
+ "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==",
1976
+ "license": "MIT",
1977
+ "engines": {
1978
+ "node": ">=0.10.0"
1979
+ }
1980
+ },
1981
+ "node_modules/react-dom": {
1982
+ "version": "19.1.0",
1983
+ "resolved": "https://registry.npmmirror.com/react-dom/-/react-dom-19.1.0.tgz",
1984
+ "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==",
1985
+ "license": "MIT",
1986
+ "dependencies": {
1987
+ "scheduler": "^0.26.0"
1988
+ },
1989
+ "peerDependencies": {
1990
+ "react": "^19.1.0"
1991
+ }
1992
+ },
1993
+ "node_modules/rollup": {
1994
+ "version": "4.41.1",
1995
+ "resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.41.1.tgz",
1996
+ "integrity": "sha512-cPmwD3FnFv8rKMBc1MxWCwVQFxwf1JEmSX3iQXrRVVG15zerAIXRjMFVWnd5Q5QvgKF7Aj+5ykXFhUl+QGnyOw==",
1997
+ "dev": true,
1998
+ "license": "MIT",
1999
+ "dependencies": {
2000
+ "@types/estree": "1.0.7"
2001
+ },
2002
+ "bin": {
2003
+ "rollup": "dist/bin/rollup"
2004
+ },
2005
+ "engines": {
2006
+ "node": ">=18.0.0",
2007
+ "npm": ">=8.0.0"
2008
+ },
2009
+ "optionalDependencies": {
2010
+ "@rollup/rollup-android-arm-eabi": "4.41.1",
2011
+ "@rollup/rollup-android-arm64": "4.41.1",
2012
+ "@rollup/rollup-darwin-arm64": "4.41.1",
2013
+ "@rollup/rollup-darwin-x64": "4.41.1",
2014
+ "@rollup/rollup-freebsd-arm64": "4.41.1",
2015
+ "@rollup/rollup-freebsd-x64": "4.41.1",
2016
+ "@rollup/rollup-linux-arm-gnueabihf": "4.41.1",
2017
+ "@rollup/rollup-linux-arm-musleabihf": "4.41.1",
2018
+ "@rollup/rollup-linux-arm64-gnu": "4.41.1",
2019
+ "@rollup/rollup-linux-arm64-musl": "4.41.1",
2020
+ "@rollup/rollup-linux-loongarch64-gnu": "4.41.1",
2021
+ "@rollup/rollup-linux-powerpc64le-gnu": "4.41.1",
2022
+ "@rollup/rollup-linux-riscv64-gnu": "4.41.1",
2023
+ "@rollup/rollup-linux-riscv64-musl": "4.41.1",
2024
+ "@rollup/rollup-linux-s390x-gnu": "4.41.1",
2025
+ "@rollup/rollup-linux-x64-gnu": "4.41.1",
2026
+ "@rollup/rollup-linux-x64-musl": "4.41.1",
2027
+ "@rollup/rollup-win32-arm64-msvc": "4.41.1",
2028
+ "@rollup/rollup-win32-ia32-msvc": "4.41.1",
2029
+ "@rollup/rollup-win32-x64-msvc": "4.41.1",
2030
+ "fsevents": "~2.3.2"
2031
+ }
2032
+ },
2033
+ "node_modules/router": {
2034
+ "version": "2.2.0",
2035
+ "resolved": "https://registry.npmmirror.com/router/-/router-2.2.0.tgz",
2036
+ "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==",
2037
+ "license": "MIT",
2038
+ "peer": true,
2039
+ "dependencies": {
2040
+ "debug": "^4.4.0",
2041
+ "depd": "^2.0.0",
2042
+ "is-promise": "^4.0.0",
2043
+ "parseurl": "^1.3.3",
2044
+ "path-to-regexp": "^8.0.0"
2045
+ },
2046
+ "engines": {
2047
+ "node": ">= 18"
2048
+ }
2049
+ },
2050
+ "node_modules/safe-buffer": {
2051
+ "version": "5.2.1",
2052
+ "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.2.1.tgz",
2053
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
2054
+ "funding": [
2055
+ {
2056
+ "type": "github",
2057
+ "url": "https://github.com/sponsors/feross"
2058
+ },
2059
+ {
2060
+ "type": "patreon",
2061
+ "url": "https://www.patreon.com/feross"
2062
+ },
2063
+ {
2064
+ "type": "consulting",
2065
+ "url": "https://feross.org/support"
2066
+ }
2067
+ ],
2068
+ "license": "MIT"
2069
+ },
2070
+ "node_modules/safer-buffer": {
2071
+ "version": "2.1.2",
2072
+ "resolved": "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz",
2073
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
2074
+ "license": "MIT",
2075
+ "peer": true
2076
+ },
2077
+ "node_modules/scheduler": {
2078
+ "version": "0.26.0",
2079
+ "resolved": "https://registry.npmmirror.com/scheduler/-/scheduler-0.26.0.tgz",
2080
+ "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==",
2081
+ "license": "MIT"
2082
+ },
2083
+ "node_modules/send": {
2084
+ "version": "1.2.0",
2085
+ "resolved": "https://registry.npmmirror.com/send/-/send-1.2.0.tgz",
2086
+ "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==",
2087
+ "license": "MIT",
2088
+ "peer": true,
2089
+ "dependencies": {
2090
+ "debug": "^4.3.5",
2091
+ "encodeurl": "^2.0.0",
2092
+ "escape-html": "^1.0.3",
2093
+ "etag": "^1.8.1",
2094
+ "fresh": "^2.0.0",
2095
+ "http-errors": "^2.0.0",
2096
+ "mime-types": "^3.0.1",
2097
+ "ms": "^2.1.3",
2098
+ "on-finished": "^2.4.1",
2099
+ "range-parser": "^1.2.1",
2100
+ "statuses": "^2.0.1"
2101
+ },
2102
+ "engines": {
2103
+ "node": ">= 18"
2104
+ }
2105
+ },
2106
+ "node_modules/serve-static": {
2107
+ "version": "2.2.0",
2108
+ "resolved": "https://registry.npmmirror.com/serve-static/-/serve-static-2.2.0.tgz",
2109
+ "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==",
2110
+ "license": "MIT",
2111
+ "peer": true,
2112
+ "dependencies": {
2113
+ "encodeurl": "^2.0.0",
2114
+ "escape-html": "^1.0.3",
2115
+ "parseurl": "^1.3.3",
2116
+ "send": "^1.2.0"
2117
+ },
2118
+ "engines": {
2119
+ "node": ">= 18"
2120
+ }
2121
+ },
2122
+ "node_modules/setprototypeof": {
2123
+ "version": "1.2.0",
2124
+ "resolved": "https://registry.npmmirror.com/setprototypeof/-/setprototypeof-1.2.0.tgz",
2125
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
2126
+ "license": "ISC",
2127
+ "peer": true
2128
+ },
2129
+ "node_modules/shebang-command": {
2130
+ "version": "2.0.0",
2131
+ "resolved": "https://registry.npmmirror.com/shebang-command/-/shebang-command-2.0.0.tgz",
2132
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
2133
+ "license": "MIT",
2134
+ "peer": true,
2135
+ "dependencies": {
2136
+ "shebang-regex": "^3.0.0"
2137
+ },
2138
+ "engines": {
2139
+ "node": ">=8"
2140
+ }
2141
+ },
2142
+ "node_modules/shebang-regex": {
2143
+ "version": "3.0.0",
2144
+ "resolved": "https://registry.npmmirror.com/shebang-regex/-/shebang-regex-3.0.0.tgz",
2145
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
2146
+ "license": "MIT",
2147
+ "peer": true,
2148
+ "engines": {
2149
+ "node": ">=8"
2150
+ }
2151
+ },
2152
+ "node_modules/side-channel": {
2153
+ "version": "1.1.0",
2154
+ "resolved": "https://registry.npmmirror.com/side-channel/-/side-channel-1.1.0.tgz",
2155
+ "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
2156
+ "license": "MIT",
2157
+ "peer": true,
2158
+ "dependencies": {
2159
+ "es-errors": "^1.3.0",
2160
+ "object-inspect": "^1.13.3",
2161
+ "side-channel-list": "^1.0.0",
2162
+ "side-channel-map": "^1.0.1",
2163
+ "side-channel-weakmap": "^1.0.2"
2164
+ },
2165
+ "engines": {
2166
+ "node": ">= 0.4"
2167
+ },
2168
+ "funding": {
2169
+ "url": "https://github.com/sponsors/ljharb"
2170
+ }
2171
+ },
2172
+ "node_modules/side-channel-list": {
2173
+ "version": "1.0.0",
2174
+ "resolved": "https://registry.npmmirror.com/side-channel-list/-/side-channel-list-1.0.0.tgz",
2175
+ "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
2176
+ "license": "MIT",
2177
+ "peer": true,
2178
+ "dependencies": {
2179
+ "es-errors": "^1.3.0",
2180
+ "object-inspect": "^1.13.3"
2181
+ },
2182
+ "engines": {
2183
+ "node": ">= 0.4"
2184
+ },
2185
+ "funding": {
2186
+ "url": "https://github.com/sponsors/ljharb"
2187
+ }
2188
+ },
2189
+ "node_modules/side-channel-map": {
2190
+ "version": "1.0.1",
2191
+ "resolved": "https://registry.npmmirror.com/side-channel-map/-/side-channel-map-1.0.1.tgz",
2192
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
2193
+ "license": "MIT",
2194
+ "peer": true,
2195
+ "dependencies": {
2196
+ "call-bound": "^1.0.2",
2197
+ "es-errors": "^1.3.0",
2198
+ "get-intrinsic": "^1.2.5",
2199
+ "object-inspect": "^1.13.3"
2200
+ },
2201
+ "engines": {
2202
+ "node": ">= 0.4"
2203
+ },
2204
+ "funding": {
2205
+ "url": "https://github.com/sponsors/ljharb"
2206
+ }
2207
+ },
2208
+ "node_modules/side-channel-weakmap": {
2209
+ "version": "1.0.2",
2210
+ "resolved": "https://registry.npmmirror.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
2211
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
2212
+ "license": "MIT",
2213
+ "peer": true,
2214
+ "dependencies": {
2215
+ "call-bound": "^1.0.2",
2216
+ "es-errors": "^1.3.0",
2217
+ "get-intrinsic": "^1.2.5",
2218
+ "object-inspect": "^1.13.3",
2219
+ "side-channel-map": "^1.0.1"
2220
+ },
2221
+ "engines": {
2222
+ "node": ">= 0.4"
2223
+ },
2224
+ "funding": {
2225
+ "url": "https://github.com/sponsors/ljharb"
2226
+ }
2227
+ },
2228
+ "node_modules/source-map-js": {
2229
+ "version": "1.2.1",
2230
+ "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz",
2231
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
2232
+ "dev": true,
2233
+ "license": "BSD-3-Clause",
2234
+ "engines": {
2235
+ "node": ">=0.10.0"
2236
+ }
2237
+ },
2238
+ "node_modules/statuses": {
2239
+ "version": "2.0.1",
2240
+ "resolved": "https://registry.npmmirror.com/statuses/-/statuses-2.0.1.tgz",
2241
+ "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
2242
+ "license": "MIT",
2243
+ "peer": true,
2244
+ "engines": {
2245
+ "node": ">= 0.8"
2246
+ }
2247
+ },
2248
+ "node_modules/tinyglobby": {
2249
+ "version": "0.2.14",
2250
+ "resolved": "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.14.tgz",
2251
+ "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==",
2252
+ "dev": true,
2253
+ "license": "MIT",
2254
+ "dependencies": {
2255
+ "fdir": "^6.4.4",
2256
+ "picomatch": "^4.0.2"
2257
+ },
2258
+ "engines": {
2259
+ "node": ">=12.0.0"
2260
+ },
2261
+ "funding": {
2262
+ "url": "https://github.com/sponsors/SuperchupuDev"
2263
+ }
2264
+ },
2265
+ "node_modules/toidentifier": {
2266
+ "version": "1.0.1",
2267
+ "resolved": "https://registry.npmmirror.com/toidentifier/-/toidentifier-1.0.1.tgz",
2268
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
2269
+ "license": "MIT",
2270
+ "peer": true,
2271
+ "engines": {
2272
+ "node": ">=0.6"
2273
+ }
2274
+ },
2275
+ "node_modules/tr46": {
2276
+ "version": "0.0.3",
2277
+ "resolved": "https://registry.npmmirror.com/tr46/-/tr46-0.0.3.tgz",
2278
+ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
2279
+ "license": "MIT"
2280
+ },
2281
+ "node_modules/type-is": {
2282
+ "version": "2.0.1",
2283
+ "resolved": "https://registry.npmmirror.com/type-is/-/type-is-2.0.1.tgz",
2284
+ "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==",
2285
+ "license": "MIT",
2286
+ "peer": true,
2287
+ "dependencies": {
2288
+ "content-type": "^1.0.5",
2289
+ "media-typer": "^1.1.0",
2290
+ "mime-types": "^3.0.0"
2291
+ },
2292
+ "engines": {
2293
+ "node": ">= 0.6"
2294
+ }
2295
+ },
2296
+ "node_modules/typescript": {
2297
+ "version": "5.7.3",
2298
+ "resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.7.3.tgz",
2299
+ "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==",
2300
+ "dev": true,
2301
+ "license": "Apache-2.0",
2302
+ "bin": {
2303
+ "tsc": "bin/tsc",
2304
+ "tsserver": "bin/tsserver"
2305
+ },
2306
+ "engines": {
2307
+ "node": ">=14.17"
2308
+ }
2309
+ },
2310
+ "node_modules/undici-types": {
2311
+ "version": "6.21.0",
2312
+ "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-6.21.0.tgz",
2313
+ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
2314
+ "dev": true,
2315
+ "license": "MIT"
2316
+ },
2317
+ "node_modules/unpipe": {
2318
+ "version": "1.0.0",
2319
+ "resolved": "https://registry.npmmirror.com/unpipe/-/unpipe-1.0.0.tgz",
2320
+ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
2321
+ "license": "MIT",
2322
+ "peer": true,
2323
+ "engines": {
2324
+ "node": ">= 0.8"
2325
+ }
2326
+ },
2327
+ "node_modules/uri-js": {
2328
+ "version": "4.4.1",
2329
+ "resolved": "https://registry.npmmirror.com/uri-js/-/uri-js-4.4.1.tgz",
2330
+ "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
2331
+ "license": "BSD-2-Clause",
2332
+ "peer": true,
2333
+ "dependencies": {
2334
+ "punycode": "^2.1.0"
2335
+ }
2336
+ },
2337
+ "node_modules/uuid": {
2338
+ "version": "9.0.1",
2339
+ "resolved": "https://registry.npmmirror.com/uuid/-/uuid-9.0.1.tgz",
2340
+ "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
2341
+ "funding": [
2342
+ "https://github.com/sponsors/broofa",
2343
+ "https://github.com/sponsors/ctavan"
2344
+ ],
2345
+ "license": "MIT",
2346
+ "bin": {
2347
+ "uuid": "dist/bin/uuid"
2348
+ }
2349
+ },
2350
+ "node_modules/vary": {
2351
+ "version": "1.1.2",
2352
+ "resolved": "https://registry.npmmirror.com/vary/-/vary-1.1.2.tgz",
2353
+ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
2354
+ "license": "MIT",
2355
+ "peer": true,
2356
+ "engines": {
2357
+ "node": ">= 0.8"
2358
+ }
2359
+ },
2360
+ "node_modules/vite": {
2361
+ "version": "6.3.5",
2362
+ "resolved": "https://registry.npmmirror.com/vite/-/vite-6.3.5.tgz",
2363
+ "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==",
2364
+ "dev": true,
2365
+ "license": "MIT",
2366
+ "dependencies": {
2367
+ "esbuild": "^0.25.0",
2368
+ "fdir": "^6.4.4",
2369
+ "picomatch": "^4.0.2",
2370
+ "postcss": "^8.5.3",
2371
+ "rollup": "^4.34.9",
2372
+ "tinyglobby": "^0.2.13"
2373
+ },
2374
+ "bin": {
2375
+ "vite": "bin/vite.js"
2376
+ },
2377
+ "engines": {
2378
+ "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
2379
+ },
2380
+ "funding": {
2381
+ "url": "https://github.com/vitejs/vite?sponsor=1"
2382
+ },
2383
+ "optionalDependencies": {
2384
+ "fsevents": "~2.3.3"
2385
+ },
2386
+ "peerDependencies": {
2387
+ "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
2388
+ "jiti": ">=1.21.0",
2389
+ "less": "*",
2390
+ "lightningcss": "^1.21.0",
2391
+ "sass": "*",
2392
+ "sass-embedded": "*",
2393
+ "stylus": "*",
2394
+ "sugarss": "*",
2395
+ "terser": "^5.16.0",
2396
+ "tsx": "^4.8.1",
2397
+ "yaml": "^2.4.2"
2398
+ },
2399
+ "peerDependenciesMeta": {
2400
+ "@types/node": {
2401
+ "optional": true
2402
+ },
2403
+ "jiti": {
2404
+ "optional": true
2405
+ },
2406
+ "less": {
2407
+ "optional": true
2408
+ },
2409
+ "lightningcss": {
2410
+ "optional": true
2411
+ },
2412
+ "sass": {
2413
+ "optional": true
2414
+ },
2415
+ "sass-embedded": {
2416
+ "optional": true
2417
+ },
2418
+ "stylus": {
2419
+ "optional": true
2420
+ },
2421
+ "sugarss": {
2422
+ "optional": true
2423
+ },
2424
+ "terser": {
2425
+ "optional": true
2426
+ },
2427
+ "tsx": {
2428
+ "optional": true
2429
+ },
2430
+ "yaml": {
2431
+ "optional": true
2432
+ }
2433
+ }
2434
+ },
2435
+ "node_modules/webidl-conversions": {
2436
+ "version": "3.0.1",
2437
+ "resolved": "https://registry.npmmirror.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
2438
+ "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
2439
+ "license": "BSD-2-Clause"
2440
+ },
2441
+ "node_modules/whatwg-url": {
2442
+ "version": "5.0.0",
2443
+ "resolved": "https://registry.npmmirror.com/whatwg-url/-/whatwg-url-5.0.0.tgz",
2444
+ "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
2445
+ "license": "MIT",
2446
+ "dependencies": {
2447
+ "tr46": "~0.0.3",
2448
+ "webidl-conversions": "^3.0.0"
2449
+ }
2450
+ },
2451
+ "node_modules/which": {
2452
+ "version": "2.0.2",
2453
+ "resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz",
2454
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
2455
+ "license": "ISC",
2456
+ "peer": true,
2457
+ "dependencies": {
2458
+ "isexe": "^2.0.0"
2459
+ },
2460
+ "bin": {
2461
+ "node-which": "bin/node-which"
2462
+ },
2463
+ "engines": {
2464
+ "node": ">= 8"
2465
+ }
2466
+ },
2467
+ "node_modules/wrappy": {
2468
+ "version": "1.0.2",
2469
+ "resolved": "https://registry.npmmirror.com/wrappy/-/wrappy-1.0.2.tgz",
2470
+ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
2471
+ "license": "ISC",
2472
+ "peer": true
2473
+ },
2474
+ "node_modules/ws": {
2475
+ "version": "8.18.2",
2476
+ "resolved": "https://registry.npmmirror.com/ws/-/ws-8.18.2.tgz",
2477
+ "integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==",
2478
+ "license": "MIT",
2479
+ "engines": {
2480
+ "node": ">=10.0.0"
2481
+ },
2482
+ "peerDependencies": {
2483
+ "bufferutil": "^4.0.1",
2484
+ "utf-8-validate": ">=5.0.2"
2485
+ },
2486
+ "peerDependenciesMeta": {
2487
+ "bufferutil": {
2488
+ "optional": true
2489
+ },
2490
+ "utf-8-validate": {
2491
+ "optional": true
2492
+ }
2493
+ }
2494
+ },
2495
+ "node_modules/zod": {
2496
+ "version": "3.25.28",
2497
+ "resolved": "https://registry.npmmirror.com/zod/-/zod-3.25.28.tgz",
2498
+ "integrity": "sha512-/nt/67WYKnr5by3YS7LroZJbtcCBurDKKPBPWWzaxvVCGuG/NOsiKkrjoOhI8mJ+SQUXEbUzeB3S+6XDUEEj7Q==",
2499
+ "license": "MIT",
2500
+ "funding": {
2501
+ "url": "https://github.com/sponsors/colinhacks"
2502
+ }
2503
+ },
2504
+ "node_modules/zod-to-json-schema": {
2505
+ "version": "3.24.5",
2506
+ "resolved": "https://registry.npmmirror.com/zod-to-json-schema/-/zod-to-json-schema-3.24.5.tgz",
2507
+ "integrity": "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==",
2508
+ "license": "ISC",
2509
+ "peerDependencies": {
2510
+ "zod": "^3.24.1"
2511
+ }
2512
+ }
2513
+ }
2514
+ }
package.json ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "dual-ai-chat(融合3)",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "vite build",
9
+ "preview": "vite preview"
10
+ },
11
+ "dependencies": {
12
+ "@google/genai": "^1.0.1",
13
+ "dompurify": "^3.1.6",
14
+ "lucide-react": "^0.511.0",
15
+ "marked": "^13.0.2",
16
+ "react": "^19.1.0",
17
+ "react-dom": "^19.1.0"
18
+ },
19
+ "devDependencies": {
20
+ "@types/node": "^22.14.0",
21
+ "@types/react": "^19.1.5",
22
+ "typescript": "~5.7.2",
23
+ "vite": "^6.2.0"
24
+ }
25
+ }
services/openaiService.ts ADDED
@@ -0,0 +1,197 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ interface OpenAIMessage {
2
+ role: 'system' | 'user' | 'assistant' | 'developer';
3
+ content: string | Array<{
4
+ type: 'text' | 'image_url';
5
+ text?: string;
6
+ image_url?: {
7
+ url: string;
8
+ detail?: 'low' | 'high' | 'auto';
9
+ };
10
+ }>;
11
+ }
12
+
13
+ interface OpenAIResponse {
14
+ text: string;
15
+ durationMs: number;
16
+ error?: string;
17
+ }
18
+
19
+ interface OpenAIStreamChunk {
20
+ id: string;
21
+ object: string;
22
+ created: number;
23
+ model: string;
24
+ choices: Array<{
25
+ index: number;
26
+ delta: {
27
+ role?: string;
28
+ content?: string;
29
+ };
30
+ finish_reason?: string | null;
31
+ }>;
32
+ }
33
+
34
+ const DEFAULT_OPENAI_API_BASE = 'https://api.openai.com/v1';
35
+
36
+ export const generateResponse = async (
37
+ prompt: string,
38
+ modelName: string,
39
+ systemInstruction?: string,
40
+ shouldUseReducedCapacity: boolean = false,
41
+ imagePart?: { inlineData: { mimeType: string; data: string } },
42
+ customBaseUrl?: string,
43
+ apiKey?: string,
44
+ onStreamChunk?: (newChunk: string, fullText: string, isComplete: boolean) => void
45
+ ): Promise<OpenAIResponse> => {
46
+ const startTime = performance.now();
47
+
48
+ try {
49
+ if (!apiKey?.trim()) {
50
+ throw new Error("API密钥未设置。请配置您的OpenAI API密钥。");
51
+ }
52
+
53
+ const messages: OpenAIMessage[] = [];
54
+
55
+ if (systemInstruction) {
56
+ messages.push({
57
+ role: 'system',
58
+ content: systemInstruction
59
+ });
60
+ }
61
+
62
+ if (imagePart) {
63
+ const imageUrl = `data:${imagePart.inlineData.mimeType};base64,${imagePart.inlineData.data}`;
64
+ messages.push({
65
+ role: 'user',
66
+ content: [
67
+ {
68
+ type: 'text',
69
+ text: prompt
70
+ },
71
+ {
72
+ type: 'image_url',
73
+ image_url: {
74
+ url: imageUrl,
75
+ detail: 'auto'
76
+ }
77
+ }
78
+ ]
79
+ });
80
+ } else {
81
+ messages.push({
82
+ role: 'user',
83
+ content: prompt
84
+ });
85
+ }
86
+
87
+ const requestBody = {
88
+ model: modelName,
89
+ messages: messages,
90
+ stream: !!onStreamChunk, // 只有提供回调时才使用流式
91
+ temperature: shouldUseReducedCapacity ? 0.3 : 0.7,
92
+ max_tokens: shouldUseReducedCapacity ? 1000 : 4000
93
+ };
94
+
95
+ const apiBase = customBaseUrl || DEFAULT_OPENAI_API_BASE;
96
+
97
+ const response = await fetch(`${apiBase}/chat/completions`, {
98
+ method: 'POST',
99
+ headers: {
100
+ 'Content-Type': 'application/json',
101
+ 'Authorization': `Bearer ${apiKey}`
102
+ },
103
+ body: JSON.stringify(requestBody)
104
+ });
105
+
106
+ if (!response.ok) {
107
+ const errorData = await response.json().catch(() => ({}));
108
+ const errorMessage = errorData.error?.message || response.statusText;
109
+
110
+ if (response.status === 401) {
111
+ throw new Error(`API密钥无效或已过期: ${errorMessage}`);
112
+ } else if (response.status === 429) {
113
+ throw new Error(`API调用频率超限: ${errorMessage}`);
114
+ } else if (response.status === 404) {
115
+ throw new Error(`模型不存在或无权访问: ${modelName}`);
116
+ } else {
117
+ throw new Error(`OpenAI API 错误 (${response.status}): ${errorMessage}`);
118
+ }
119
+ }
120
+
121
+ let fullText = '';
122
+ const durationMs = performance.now() - startTime;
123
+
124
+ // 处理流式响应
125
+ if (onStreamChunk && requestBody.stream) {
126
+ const reader = response.body?.getReader();
127
+ const decoder = new TextDecoder();
128
+
129
+ if (!reader) {
130
+ throw new Error('无法读取响应流');
131
+ }
132
+
133
+ try {
134
+ while (true) {
135
+ const { done, value } = await reader.read();
136
+ if (done) break;
137
+
138
+ const chunk = decoder.decode(value);
139
+ const lines = chunk.split('\n');
140
+
141
+ for (const line of lines) {
142
+ const trimmed = line.trim();
143
+ if (trimmed.startsWith('data: ')) {
144
+ const jsonStr = trimmed.slice(6);
145
+ if (jsonStr === '[DONE]') {
146
+ onStreamChunk('', fullText, true);
147
+ break;
148
+ }
149
+
150
+ try {
151
+ const parsed: OpenAIStreamChunk = JSON.parse(jsonStr);
152
+ const content = parsed.choices[0]?.delta?.content;
153
+ if (content) {
154
+ fullText += content;
155
+ // 立即回调以实现实时显示
156
+ onStreamChunk(content, fullText, false);
157
+ }
158
+ } catch (parseError) {
159
+ console.warn('解析流数据时出错:', parseError);
160
+ }
161
+ }
162
+ }
163
+ }
164
+ } finally {
165
+ reader.releaseLock();
166
+ }
167
+ } else {
168
+ // 处理非流式响应(例如图像请求)
169
+ const data = await response.json();
170
+ fullText = data.choices[0]?.message?.content || '';
171
+ }
172
+
173
+ if (!fullText.trim()) {
174
+ throw new Error('AI响应为空,请检查模型配置或重试');
175
+ }
176
+
177
+ return { text: fullText, durationMs };
178
+
179
+ } catch (error) {
180
+ console.error("调用OpenAI API时出��:", error);
181
+ const durationMs = performance.now() - startTime;
182
+
183
+ if (error instanceof Error) {
184
+ if (error.message.includes('API密钥') || error.message.includes('401') || error.message.includes('Unauthorized')) {
185
+ return { text: "API密钥无效或已过期。请检查您的OpenAI API密钥配置。", durationMs, error: "API key not valid" };
186
+ } else if (error.message.includes('429') || error.message.includes('Rate limit')) {
187
+ return { text: "API调用频率超限,请稍后重试。", durationMs, error: "Rate limit exceeded" };
188
+ } else if (error.message.includes('404') || error.message.includes('model')) {
189
+ return { text: `模型 ${modelName} 不存在或无权访问。请检查模型名称或API权限。`, durationMs, error: "Model not found" };
190
+ } else if (error.message.includes('网络') || error.message.includes('fetch')) {
191
+ return { text: "网络连接错误,请检查网络连接后重试。", durationMs, error: "Network error" };
192
+ }
193
+ return { text: `与OpenAI通信时出错: ${error.message}`, durationMs, error: error.message };
194
+ }
195
+ return { text: "与OpenAI通信时发生未知错误。", durationMs, error: "Unknown OpenAI error" };
196
+ }
197
+ };
tsconfig.json ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "experimentalDecorators": true,
5
+ "useDefineForClassFields": false,
6
+ "module": "ESNext",
7
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
8
+ "skipLibCheck": true,
9
+
10
+ /* Bundler mode */
11
+ "moduleResolution": "bundler",
12
+ "allowImportingTsExtensions": true,
13
+ "isolatedModules": true,
14
+ "moduleDetection": "force",
15
+ "noEmit": true,
16
+ "allowJs": true,
17
+ "jsx": "react-jsx",
18
+
19
+ /* Linting */
20
+ "strict": true,
21
+ "noUnusedLocals": true,
22
+ "noUnusedParameters": true,
23
+ "noFallthroughCasesInSwitch": true,
24
+ "noUncheckedSideEffectImports": true,
25
+
26
+ "paths": {
27
+ "@/*" : ["./*"]
28
+ }
29
+ }
30
+ }
types.ts ADDED
@@ -0,0 +1,298 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // 动态消息发送者枚举 - 现在支持自定义角色名称
2
+ export enum MessageSender {
3
+ User = '用户',
4
+ System = '系统',
5
+ // 动态角色将在运行时创建,这里保留一些常用的默认值
6
+ Cognito = 'Cognito',
7
+ Muse = 'Muse',
8
+ }
9
+
10
+ // 消息用途枚举
11
+ export enum MessagePurpose {
12
+ UserInput = 'user-input',
13
+ SystemNotification = 'system-notification',
14
+ CognitoToMuse = 'cognito-to-muse', // AI角色之间的讨论(保持兼容性)
15
+ MuseToCognito = 'muse-to-cognito', // AI角色之间的讨论(保持兼容性)
16
+ FinalResponse = 'final-response', // 最终回复用户
17
+ RoleDiscussion = 'role-discussion', // 通用的角色间讨论
18
+ }
19
+
20
+ // 聊天消息接口
21
+ export interface ChatMessage {
22
+ id: string;
23
+ text: string;
24
+ sender: MessageSender | string; // 现在支持动态角色名称
25
+ purpose: MessagePurpose;
26
+ timestamp: Date;
27
+ durationMs?: number; // AI消息的生成时间
28
+ image?: { // 用户消息的可选图片数据
29
+ dataUrl: string; // 用于显示的base64数据URL
30
+ name: string;
31
+ type: string;
32
+ };
33
+ roleId?: string; // 可选的角色ID,用于关联具体的AI角色配置
34
+ isStreaming?: boolean; // 标记消息是否正在流式传输
35
+ }
36
+
37
+ // 讨论记录接口
38
+ export interface DiscussionRecord {
39
+ id: string;
40
+ timestamp: Date;
41
+ userQuery: string;
42
+ userImage?: {
43
+ name: string;
44
+ type: string;
45
+ size: number;
46
+ };
47
+ discussionMode: string;
48
+ activeRoles: Array<{
49
+ id: string;
50
+ name: string;
51
+ modelName: string;
52
+ channelName: string;
53
+ }>;
54
+ turns: Array<{
55
+ id: string;
56
+ role: string;
57
+ roleId?: string;
58
+ message: string;
59
+ timestamp: Date;
60
+ durationMs?: number;
61
+ purpose: MessagePurpose;
62
+ }>;
63
+ notepadUpdates: Array<{
64
+ id: string;
65
+ updater: string;
66
+ updaterId?: string;
67
+ content: string;
68
+ timestamp: Date;
69
+ }>;
70
+ finalAnswer?: {
71
+ content: string;
72
+ provider: string;
73
+ providerId?: string;
74
+ timestamp: Date;
75
+ durationMs?: number;
76
+ };
77
+ totalDuration: number; // 总讨论时长(毫秒)
78
+ isCompleted: boolean; // 讨论是否正常完成
79
+ wasInterrupted: boolean; // 是否被用户中断
80
+ interruptedAt?: Date; // 中断时间
81
+ settings: {
82
+ discussionMode: string;
83
+ manualFixedTurns?: number;
84
+ isReducedCapacityEnabled: boolean;
85
+ activeRoleCount: number;
86
+ };
87
+ metadata: {
88
+ version: string;
89
+ exportedAt?: Date;
90
+ messageCount: number;
91
+ notepadUpdateCount: number;
92
+ };
93
+ }
94
+
95
+ // 讨论统计接口
96
+ export interface DiscussionStats {
97
+ totalTurns: number;
98
+ averageResponseTime: number;
99
+ longestResponseTime: number;
100
+ shortestResponseTime: number;
101
+ totalTokensUsed?: number; // 如果API提供token使用情况
102
+ roleParticipation: Record<string, {
103
+ turnCount: number;
104
+ totalResponseTime: number;
105
+ averageResponseTime: number;
106
+ }>;
107
+ notepadUpdateFrequency: number;
108
+ }
109
+
110
+ // 流式传输状态接口
111
+ export interface StreamingState {
112
+ messageId: string;
113
+ currentText: string;
114
+ targetText: string;
115
+ isComplete: boolean;
116
+ startTime: Date;
117
+ speed: number; // 字符/秒
118
+ }
119
+
120
+ // AI模型配置接口(从constants.ts导入,但在这里声明以保持类型完整性)
121
+ export interface AiModelConfig {
122
+ id: string;
123
+ name: string;
124
+ apiName: string;
125
+ baseUrl?: string;
126
+ supportsImages: boolean;
127
+ supportsReducedCapacity: boolean;
128
+ category: string;
129
+ maxTokens: number;
130
+ temperature: number;
131
+ isCustom: boolean;
132
+ createdAt: Date;
133
+ }
134
+
135
+ // AI角色配置接口
136
+ export interface AiRoleConfig {
137
+ id: string;
138
+ name: string;
139
+ systemPrompt: string;
140
+ modelId: string;
141
+ isActive: boolean;
142
+ color?: string; // 可选的UI显示颜色
143
+ description?: string; // 可选的角色描述
144
+ }
145
+
146
+ // 讨论配置接口
147
+ export interface DiscussionConfig {
148
+ mode: 'fixed' | 'ai-driven';
149
+ fixedTurns?: number;
150
+ maxTurnsPerRole?: number;
151
+ allowSameModel?: boolean; // 是否允许多个角色使用相同模型
152
+ enableNotepadSharing?: boolean; // 是否启用记事本共享
153
+ enableStreamingTypewriter?: boolean; // 是否启用流式打字机效果
154
+ typewriterSpeed?: number; // 打字机速度(毫秒/字符)
155
+ }
156
+
157
+ // API响应接口
158
+ export interface ApiResponse {
159
+ text: string;
160
+ durationMs: number;
161
+ error?: string;
162
+ usage?: {
163
+ promptTokens: number;
164
+ completionTokens: number;
165
+ totalTokens: number;
166
+ };
167
+ streamingChunks?: string[]; // 流式响应的块
168
+ }
169
+
170
+ // 配置导出/导入接口
171
+ export interface ConfigExportData {
172
+ models: AiModelConfig[];
173
+ roles: AiRoleConfig[];
174
+ discussionConfig?: DiscussionConfig;
175
+ discussionRecords?: DiscussionRecord[]; // 可选的讨论记录
176
+ exportedAt: string;
177
+ version: string;
178
+ }
179
+
180
+ // 讨论记录导出接口
181
+ export interface DiscussionExportData {
182
+ record: DiscussionRecord;
183
+ stats: DiscussionStats;
184
+ fullTranscript: string; // 完整的文本记录
185
+ exportFormat: 'json' | 'markdown' | 'html' | 'txt';
186
+ exportedAt: string;
187
+ version: string;
188
+ }
189
+
190
+ // 验证结果接口
191
+ export interface ValidationResult {
192
+ isValid: boolean;
193
+ errors: string[];
194
+ warnings?: string[];
195
+ }
196
+
197
+ // 角色状态接口(运行时使用)
198
+ export interface RoleState {
199
+ id: string;
200
+ name: string;
201
+ isProcessing: boolean;
202
+ lastResponse?: string;
203
+ totalResponseTime?: number;
204
+ messageCount?: number;
205
+ currentStreamingMessageId?: string; // 当前正在流式传输的消息ID
206
+ }
207
+
208
+ // 会话状态接口
209
+ export interface SessionState {
210
+ isActive: boolean;
211
+ startTime?: Date;
212
+ currentTurn: number;
213
+ activeRoles: RoleState[];
214
+ discussionLog: Array<{
215
+ roleId: string;
216
+ roleName: string;
217
+ message: string;
218
+ timestamp: Date;
219
+ }>;
220
+ canBeInterrupted: boolean; // 是否可以被中断
221
+ interruptionRequested: boolean; // 是否请求了中断
222
+ }
223
+
224
+ // 讨论控制接口
225
+ export interface DiscussionControl {
226
+ canStart: boolean;
227
+ canPause: boolean;
228
+ canResume: boolean;
229
+ canInterrupt: boolean;
230
+ canExport: boolean;
231
+ currentPhase: 'idle' | 'initializing' | 'discussing' | 'synthesizing' | 'completed' | 'interrupted';
232
+ estimatedTimeRemaining?: number; // 估算剩余时间(毫秒)
233
+ }
234
+
235
+ // 导出选项接口
236
+ export interface ExportOptions {
237
+ format: 'json' | 'markdown' | 'html' | 'txt';
238
+ includeMetadata: boolean;
239
+ includeStats: boolean;
240
+ includeNotepadHistory: boolean;
241
+ includeSystemMessages: boolean;
242
+ timestampFormat: 'iso' | 'local' | 'relative';
243
+ compressOutput: boolean;
244
+ }
245
+
246
+ // 消息过滤器接口
247
+ export interface MessageFilter {
248
+ senders?: (MessageSender | string)[];
249
+ purposes?: MessagePurpose[];
250
+ dateRange?: {
251
+ start: Date;
252
+ end: Date;
253
+ };
254
+ textSearch?: string;
255
+ hasDuration?: boolean;
256
+ hasImage?: boolean;
257
+ minDuration?: number;
258
+ maxDuration?: number;
259
+ }
260
+
261
+ // 通知接口
262
+ export interface Notification {
263
+ id: string;
264
+ type: 'info' | 'success' | 'warning' | 'error';
265
+ title: string;
266
+ message: string;
267
+ timestamp: Date;
268
+ duration?: number; // 自动消失时间(毫秒)
269
+ actions?: Array<{
270
+ label: string;
271
+ action: () => void;
272
+ style?: 'primary' | 'secondary' | 'danger';
273
+ }>;
274
+ }
275
+
276
+ // 应用状态接口
277
+ export interface AppState {
278
+ ui: {
279
+ theme: 'light' | 'dark' | 'auto';
280
+ sidebarCollapsed: boolean;
281
+ roleManagerOpen: boolean;
282
+ configManagerOpen: boolean;
283
+ notifications: Notification[];
284
+ };
285
+ discussion: {
286
+ current?: DiscussionRecord;
287
+ history: DiscussionRecord[];
288
+ control: DiscussionControl;
289
+ streaming: Map<string, StreamingState>;
290
+ };
291
+ session: SessionState;
292
+ config: {
293
+ channels: any[];
294
+ models: any[];
295
+ roles: any[];
296
+ discussionSettings: DiscussionConfig;
297
+ };
298
+ }
vite.config.ts ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import path from 'path';
2
+ import { defineConfig } from 'vite';
3
+
4
+ export default defineConfig({
5
+ resolve: {
6
+ alias: {
7
+ '@': path.resolve(__dirname, '.'),
8
+ }
9
+ },
10
+ build: {
11
+ target: 'es2020',
12
+ rollupOptions: {
13
+ output: {
14
+ manualChunks: undefined,
15
+ }
16
+ }
17
+ },
18
+ server: {
19
+ port: 7860,
20
+ host: '0.0.0.0', // 允许外部访问
21
+ open: false // Docker环境中不自动打开浏览器
22
+ },
23
+ preview: {
24
+ port: 7860,
25
+ host: '0.0.0.0'
26
+ }
27
+ });