import React, { useState, useEffect, useRef } from 'react'; import { ChatMessage, MessageSender, MessagePurpose } from './types'; import { generateResponse } from './services/openaiService'; import ChatInput from './components/ChatInput'; import MessageBubble from './components/MessageBubble'; import Notepad from './components/Notepad'; import ModelConfigManager from './components/ModelConfigManager'; import { AiModel, AiRole, ApiChannel, ModelConfigManager as ConfigManager, DEFAULT_MANUAL_FIXED_TURNS, MIN_MANUAL_FIXED_TURNS, MAX_MANUAL_FIXED_TURNS, MAX_AI_DRIVEN_DISCUSSION_TURNS_PER_MODEL, INITIAL_NOTEPAD_CONTENT, NOTEPAD_INSTRUCTION_PROMPT_PART, NOTEPAD_UPDATE_TAG_START, NOTEPAD_UPDATE_TAG_END, DISCUSSION_COMPLETE_TAG, AI_DRIVEN_DISCUSSION_INSTRUCTION_PROMPT_PART, DiscussionMode } from './constants'; import { BotMessageSquare, AlertTriangle, RefreshCcw, SlidersHorizontal, Users, MessagesSquare, Bot, ChevronDown, Settings, Play, Pause, Square, Download } from 'lucide-react'; interface ParsedAIResponse { spokenText: string; newNotepadContent: string | null; discussionShouldEnd?: boolean; } interface ActiveRole extends AiRole { model: AiModel; channel: ApiChannel; isProcessing?: boolean; } interface DiscussionState { currentRoleIndex: number; currentTurn: number; discussionLog: string[]; isFirstMessage: boolean; previousAISignaledStop: boolean; discussionEndCount: number; userQuery: string; imageApiPart?: any; commonPromptInstructions: string; roleOrder: ActiveRole[]; maxTurnsForLoop: number; } const parseAIResponse = (responseText: string): ParsedAIResponse => { let currentText = responseText.trim(); let spokenText = ""; let newNotepadContent: string | null = null; let discussionShouldEnd = false; let notepadActionText = ""; let discussionActionText = ""; const notepadStartIndex = currentText.lastIndexOf(NOTEPAD_UPDATE_TAG_START); const notepadEndIndex = currentText.lastIndexOf(NOTEPAD_UPDATE_TAG_END); if (notepadStartIndex !== -1 && notepadEndIndex !== -1 && notepadEndIndex > notepadStartIndex && currentText.endsWith(NOTEPAD_UPDATE_TAG_END)) { newNotepadContent = currentText.substring(notepadStartIndex + NOTEPAD_UPDATE_TAG_START.length, notepadEndIndex).trim(); spokenText = currentText.substring(0, notepadStartIndex).trim(); if (newNotepadContent) { notepadActionText = "更新了记事本"; } else { notepadActionText = "尝试更新记事本但内容为空"; } } else { spokenText = currentText; } if (spokenText.includes(DISCUSSION_COMPLETE_TAG)) { discussionShouldEnd = true; spokenText = spokenText.replace(new RegExp(DISCUSSION_COMPLETE_TAG.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), "").trim(); discussionActionText = "建议结束讨论"; } if (!spokenText.trim()) { if (notepadActionText && discussionActionText) { spokenText = `(AI ${notepadActionText}并${discussionActionText})`; } else if (notepadActionText) { spokenText = `(AI ${notepadActionText})`; } else if (discussionActionText) { spokenText = `(AI ${discussionActionText})`; } else { spokenText = "(AI 未提供额外文本回复)"; } } return { spokenText: spokenText.trim(), newNotepadContent, discussionShouldEnd }; }; const fileToBase64 = (file: File): Promise => { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.readAsDataURL(file); reader.onload = () => { const result = reader.result as string; resolve(result.split(',')[1]); }; reader.onerror = (error) => reject(error); }); }; const createDynamicMessageSender = (roleName: string): MessageSender => { return roleName as MessageSender; }; const App: React.FC = () => { const [messages, setMessages] = useState([]); const [isLoading, setIsLoading] = useState(false); const [currentTotalProcessingTimeMs, setCurrentTotalProcessingTimeMs] = useState(0); const [notepadContent, setNotepadContent] = useState(INITIAL_NOTEPAD_CONTENT); const [lastNotepadUpdateBy, setLastNotepadUpdateBy] = useState(null); const [discussionMode, setDiscussionMode] = useState(DiscussionMode.FixedTurns); const [manualFixedTurns, setManualFixedTurns] = useState(DEFAULT_MANUAL_FIXED_TURNS); const [isReducedCapacityEnabled, setIsReducedCapacityEnabled] = useState(false); const [activeRoles, setActiveRoles] = useState([]); const [channels, setChannels] = useState([]); const [models, setModels] = useState([]); const [isConfigManagerOpen, setIsConfigManagerOpen] = useState(false); const [isRoleSelectorOpen, setIsRoleSelectorOpen] = useState(false); const [isDiscussionActive, setIsDiscussionActive] = useState(false); const [streamingMessages, setStreamingMessages] = useState>(new Map()); const [currentDiscussion, setCurrentDiscussion] = useState(null); const chatContainerRef = useRef(null); const currentQueryStartTimeRef = useRef(null); const cancelRequestRef = useRef(false); // 实时流式消息管理 const createStreamingMessage = (sender: MessageSender, purpose: MessagePurpose): string => { const messageId = Date.now().toString() + Math.random().toString(36).substr(2, 9); const message: ChatMessage = { id: messageId, text: '', sender, purpose, timestamp: new Date() }; setMessages(prev => [...prev, message]); setStreamingMessages(prev => new Map(prev.set(messageId, { text: '', isComplete: false }))); return messageId; }; const updateStreamingMessage = (messageId: string, fullText: string, isComplete: boolean, durationMs?: number) => { setMessages(prev => prev.map(msg => msg.id === messageId ? { ...msg, text: fullText, durationMs: isComplete ? durationMs : msg.durationMs } : msg )); }; const loadConfiguration = () => { const allChannels = ConfigManager.getChannels(); const allModels = ConfigManager.getModels(); const allRoles = ConfigManager.getActiveRoles(); setChannels(allChannels); setModels(allModels); const rolesWithModelsAndChannels: ActiveRole[] = allRoles.map(role => { const model = allModels.find(m => m.id === role.modelId); if (!model) { console.warn(`Role ${role.name} references non-existent model ${role.modelId}`); return null; } const channel = allChannels.find(ch => ch.id === model.channelId); if (!channel) { console.warn(`Model ${model.name} references non-existent channel ${model.channelId}`); return null; } return { ...role, model, channel }; }).filter(Boolean) as ActiveRole[]; setActiveRoles(rolesWithModelsAndChannels); }; useEffect(() => { loadConfiguration(); }, []); const addMessage = ( text: string, sender: MessageSender, purpose: MessagePurpose, durationMs?: number, image?: ChatMessage['image'] ) => { const messageId = Date.now().toString() + Math.random().toString(36).substr(2, 9); const message: ChatMessage = { id: messageId, text, sender, purpose, timestamp: new Date(), durationMs, image }; setMessages(prev => [...prev, message]); return messageId; }; const interruptDiscussion = () => { if (isLoading && isDiscussionActive) { cancelRequestRef.current = true; setIsLoading(false); setIsDiscussionActive(false); setCurrentDiscussion(null); if (currentTotalProcessingTimeMs > 0) { addMessage( `讨论已被用户中断 (已进行 ${(currentTotalProcessingTimeMs / 1000).toFixed(2)}秒)`, MessageSender.System, MessagePurpose.SystemNotification ); } setCurrentTotalProcessingTimeMs(0); if (currentQueryStartTimeRef.current) { currentQueryStartTimeRef.current = null; } } }; const exportDiscussionRecord = () => { if (messages.length === 0) { addMessage('当前没有可导出的消息记录', MessageSender.System, MessagePurpose.SystemNotification); return; } // 生成简洁的文本格式导出 let exportText = `=== Multi-Mind Chat 对话记录 ===\n`; exportText += `导出时间: ${new Date().toLocaleString()}\n`; exportText += `消息总数: ${messages.length}\n\n`; messages.forEach(msg => { if (msg.purpose !== MessagePurpose.SystemNotification) { const timeStr = msg.timestamp.toLocaleTimeString(); const durationStr = msg.durationMs ? ` (${(msg.durationMs / 1000).toFixed(2)}s)` : ''; exportText += `[${timeStr}] ${msg.sender}${durationStr}: ${msg.text}\n\n`; if (msg.image) { exportText += ` [附件: ${msg.image.name} - ${msg.image.type}]\n\n`; } } }); if (notepadContent !== INITIAL_NOTEPAD_CONTENT) { exportText += `=== 最终记事本内容 ===\n`; exportText += `${notepadContent}\n\n`; } const blob = new Blob([exportText], { type: 'text/plain;charset=utf-8' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `对话记录-${new Date().toISOString().split('T')[0]}-${Date.now()}.txt`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); addMessage('对话记录已导出', MessageSender.System, MessagePurpose.SystemNotification); }; const getWelcomeMessageText = () => { const modeDescription = discussionMode === DiscussionMode.FixedTurns ? `固定轮次对话 (${manualFixedTurns}轮)` : "AI驱动对话"; const roleNames = activeRoles.map(role => role.name).join(' 和 '); const roleCount = activeRoles.length; const channelCount = channels.length; if (channelCount === 0) { return `欢迎使用Multi-Mind Chat 智囊团!请先配置API渠道。点击设置按钮开始配置。`; } else if (roleCount === 0) { return `欢迎使用Multi-Mind Chat 智囊团!已配置 ${channelCount} 个API渠道,请继续配置AI角色和模型。点击设置按钮开始配置。`; } else if (roleCount === 1) { return `欢迎使用Multi-Mind Chat 智囊团!当前模式: ${modeDescription}。当前只有一个活跃角色: ${roleNames}。建议添加更多角色以获得更好的协作体验。`; } else { return `欢迎使用Multi-Mind Chat 智囊团!当前模式: ${modeDescription}。活跃的AI角色: ${roleNames}。这些角色将协作讨论您的问题并使用共享记事本。`; } }; const initializeChat = () => { setMessages([]); setNotepadContent(INITIAL_NOTEPAD_CONTENT); setLastNotepadUpdateBy(null); setIsDiscussionActive(false); setStreamingMessages(new Map()); setCurrentDiscussion(null); addMessage( getWelcomeMessageText(), MessageSender.System, MessagePurpose.SystemNotification ); }; useEffect(() => { initializeChat(); }, [activeRoles, discussionMode, manualFixedTurns, channels]); useEffect(() => { if (chatContainerRef.current) { chatContainerRef.current.scrollTop = chatContainerRef.current.scrollHeight; } }, [messages]); useEffect(() => { let intervalId: number | undefined; if (isLoading && currentQueryStartTimeRef.current) { intervalId = window.setInterval(() => { if (currentQueryStartTimeRef.current) { setCurrentTotalProcessingTimeMs(performance.now() - currentQueryStartTimeRef.current); } }, 100); } else { if (intervalId) clearInterval(intervalId); } return () => { if (intervalId) clearInterval(intervalId); }; }, [isLoading]); const handleClearChat = () => { if (isLoading) { cancelRequestRef.current = true; } setIsLoading(false); setIsDiscussionActive(false); setCurrentDiscussion(null); setCurrentTotalProcessingTimeMs(0); if (currentQueryStartTimeRef.current) { currentQueryStartTimeRef.current = null; } setMessages([]); setNotepadContent(INITIAL_NOTEPAD_CONTENT); setLastNotepadUpdateBy(null); setStreamingMessages(new Map()); addMessage( getWelcomeMessageText(), MessageSender.System, MessagePurpose.SystemNotification ); }; const handleManualFixedTurnsChange = (e: React.ChangeEvent) => { let value = parseInt(e.target.value, 10); if (isNaN(value)) { value = DEFAULT_MANUAL_FIXED_TURNS; } value = Math.max(MIN_MANUAL_FIXED_TURNS, Math.min(MAX_MANUAL_FIXED_TURNS, value)); setManualFixedTurns(value); }; const toggleRoleActiveState = (roleId: string) => { const role = activeRoles.find(r => r.id === roleId); if (role) { ConfigManager.updateRole(roleId, { isActive: !role.isActive }); loadConfiguration(); } }; const processNextRole = (state: DiscussionState) => { if (cancelRequestRef.current) { setIsDiscussionActive(false); setCurrentDiscussion(null); return; } // 检查是否完成所有讨论 if (state.currentTurn === 0 && state.currentRoleIndex >= state.roleOrder.length) { // 第一轮结束,开始多轮讨论 if (!state.previousAISignaledStop && state.maxTurnsForLoop > 0) { const newState = { ...state, currentTurn: 1, currentRoleIndex: 0, discussionEndCount: 0 }; setCurrentDiscussion(newState); processNextRole(newState); return; } else { processFinalAnswer(state); return; } } else if (state.currentTurn > 0 && (state.currentTurn >= state.maxTurnsForLoop || state.previousAISignaledStop || state.currentRoleIndex >= state.roleOrder.length)) { processFinalAnswer(state); return; } const currentRole = state.roleOrder[state.currentRoleIndex]; const shouldUseReducedCapacity = isReducedCapacityEnabled && currentRole.model.supportsReducedCapacity; // 显示系统通知 addMessage( `${currentRole.name} 正在${state.currentTurn === 0 ? '分析问题并提供观点' : '回应其他角色的观点'} (使用 ${currentRole.model.name} - ${currentRole.channel.name})...`, MessageSender.System, MessagePurpose.SystemNotification ); // 立即创建消息气泡 const purpose = state.currentRoleIndex % 2 === 0 ? MessagePurpose.CognitoToMuse : MessagePurpose.MuseToCognito; const messageId = Date.now().toString() + Math.random().toString(36).substr(2, 9); // 立即添加空消息到界面,用户会看到正在输入的效果 const initialMessage: ChatMessage = { id: messageId, text: '', // 开始时为空,等待流式输入 sender: createDynamicMessageSender(currentRole.name), purpose, timestamp: new Date() }; setMessages(prev => [...prev, initialMessage]); // 构建提示词 let prompt: string; if (state.isFirstMessage) { prompt = `用户的查询 (中文) 是: "${state.userQuery}". ${state.imageApiPart ? "用户还提供了一张图片。请在您的分析和回复中同时考虑此图片和文本查询。" : ""} 请针对此查询提供您的初步想法或分析。这是一个多AI协作的环境,其他AI角色稍后会对您的观点进行回应和讨论。用中文回答。\n${state.commonPromptInstructions}`; } else if (state.currentTurn === 0) { const otherRoles = state.roleOrder.filter(r => r.id !== currentRole.id).map(r => r.name).join(' 和 '); prompt = `用户的查询 (中文) 是: "${state.userQuery}". ${state.imageApiPart ? "用户还提供了一张图片。请在您的分析和回复中同时考虑此图片和文本查询。" : ""} 当前讨论 (均为中文):\n${state.discussionLog.join("\n")}\n您正在与 ${otherRoles} 协作讨论这个问题。请提供您的观点和分析。用中文回答。\n${state.commonPromptInstructions}`; } else { const otherRoles = state.roleOrder.filter(r => r.id !== currentRole.id).map(r => r.name).join(' 和 '); prompt = `用户的查询 (中文) 是: "${state.userQuery}". ${state.imageApiPart ? "用户还提供了一张图片。请在您的分析和回复中同时考虑此图片和文本查询。" : ""} 当前讨论 (均为中文):\n${state.discussionLog.join("\n")}\n您正在与 ${otherRoles} 协作讨论。请对前面的讨论进行回应,提供您的进一步见解或不同观点。保持简洁并使用中文。\n${NOTEPAD_INSTRUCTION_PROMPT_PART.replace('{notepadContent}', notepadContent)}`; if (discussionMode === DiscussionMode.AiDriven && state.previousAISignaledStop) { prompt += `\n注意:之前有AI角色建议结束讨论。如果您同意,请在回复中包含 ${DISCUSSION_COMPLETE_TAG}。否则,请继续讨论。`; } else if (discussionMode === DiscussionMode.AiDriven) { prompt += AI_DRIVEN_DISCUSSION_INSTRUCTION_PROMPT_PART; } } // 用于后台累积完整响应文本的变量 let accumulatedText = ''; // 开始流式API调用 generateResponse( prompt, currentRole.model.apiName, currentRole.systemPrompt, shouldUseReducedCapacity, state.imageApiPart, currentRole.channel.baseUrl, currentRole.channel.apiKey, // 关键的流式回调函数 (newChunk: string, fullText: string, isComplete: boolean) => { // newChunk: 本次新接收到的文本块 // fullText: API累积的完整文本(用于后续处理) // isComplete: 是否完成 // 更新后台累积的完整文本 accumulatedText = fullText; // 实时更新界面显示 - 关键是这里直接使用 fullText 进行显示 setMessages(prev => prev.map(msg => msg.id === messageId ? { ...msg, text: fullText, // 直接显示累积的完整文本,实现打字机效果 durationMs: isComplete ? (performance.now() - (currentQueryStartTimeRef.current || 0)) : undefined } : msg )); }, currentRole.model.maxTokens // 传递模型配置的 maxTokens ).then(response => { if (cancelRequestRef.current) { setIsDiscussionActive(false); setCurrentDiscussion(null); return; } if (response.error) { if (response.error.includes("API key not valid") || response.error.includes("401")) { setMessages(prev => prev.map(msg => msg.id === messageId ? { ...msg, text: `API密钥无效 (渠道: ${currentRole.channel.name}),请在配置界面中检查密钥设置。`, durationMs: response.durationMs } : msg )); setIsLoading(false); setIsDiscussionActive(false); setCurrentDiscussion(null); return; } throw new Error(`${currentRole.name}: ${response.text}`); } // 确保最终消息内容正确 setMessages(prev => prev.map(msg => msg.id === messageId ? { ...msg, text: response.text, durationMs: response.durationMs } : msg )); // 解析响应 const parsedResponse = parseAIResponse(response.text); if (parsedResponse.newNotepadContent !== null) { setNotepadContent(parsedResponse.newNotepadContent); setLastNotepadUpdateBy(createDynamicMessageSender(currentRole.name)); } // 更新讨论日志 - 使用解析后的文本 const newDiscussionLog = [...state.discussionLog, `${currentRole.name}: ${parsedResponse.spokenText}`]; // 更新状态 let newState = { ...state, discussionLog: newDiscussionLog, isFirstMessage: false, currentRoleIndex: state.currentRoleIndex + 1 }; // 处理AI驱动模式的结束信号 if (discussionMode === DiscussionMode.AiDriven && parsedResponse.discussionShouldEnd) { if (state.currentTurn > 0) { newState.discussionEndCount++; if (state.previousAISignaledStop || newState.discussionEndCount >= Math.ceil(state.roleOrder.length / 2)) { addMessage(`多数AI角色已同意结束讨论。`, MessageSender.System, MessagePurpose.SystemNotification); newState.previousAISignaledStop = true; } else { addMessage(`${currentRole.name} 已建议结束讨论。`, MessageSender.System, MessagePurpose.SystemNotification); } } else { newState.previousAISignaledStop = true; addMessage(`${currentRole.name} 已建议结束讨论。`, MessageSender.System, MessagePurpose.SystemNotification); } } setCurrentDiscussion(newState); // 直接处理下一个角色,不使用setTimeout延迟 processNextRole(newState); }).catch(error => { console.error("处理AI响应时出错:", error); addMessage(`错误: ${error instanceof Error ? error.message : "处理响应时发生未知错误"}`, MessageSender.System, MessagePurpose.SystemNotification); setIsLoading(false); setIsDiscussionActive(false); setCurrentDiscussion(null); }); }; const processFinalAnswer = (state: DiscussionState) => { const finalAnswerRole = state.roleOrder[0]; const shouldUseReducedCapacity = isReducedCapacityEnabled && finalAnswerRole.model.supportsReducedCapacity; addMessage( `${finalAnswerRole.name} 正在综合所有讨论内容,准备最终答案 (使用 ${finalAnswerRole.model.name} - ${finalAnswerRole.channel.name})...`, MessageSender.System, MessagePurpose.SystemNotification ); // 立即创建最终答案消息气泡 const finalMessageId = Date.now().toString() + Math.random().toString(36).substr(2, 9); const finalMessage: ChatMessage = { id: finalMessageId, text: '', // 开始时为空 sender: createDynamicMessageSender(finalAnswerRole.name), purpose: MessagePurpose.FinalResponse, timestamp: new Date() }; setMessages(prev => [...prev, finalMessage]); const finalPrompt = `用户最初的查询 (中文) 是: "${state.userQuery}". ${state.imageApiPart ? "用户还提供了一张图片。请在您的分析和回复中同时考虑此图片和文本查询。" : ""} 您和其他AI角色进行了以下讨论 (均为中文):\n${state.discussionLog.join("\n")}\n基于整个协作讨论过程和共享记事本的最终状态,请综合所有关键观点,为用户提供一个全面、有用的最终答案。直接回复用户,确保答案结构良好,易于理解,并使用中文。如果相关,您可以在答案中引用记事本内容。如果需要,您也可以最后一次更新记事本。\n${NOTEPAD_INSTRUCTION_PROMPT_PART.replace('{notepadContent}', notepadContent)}`; generateResponse( finalPrompt, finalAnswerRole.model.apiName, finalAnswerRole.systemPrompt, shouldUseReducedCapacity, state.imageApiPart, finalAnswerRole.channel.baseUrl, finalAnswerRole.channel.apiKey, // 最终答案的流式回调 (newChunk: string, fullText: string, isComplete: boolean) => { setMessages(prev => prev.map(msg => msg.id === finalMessageId ? { ...msg, text: fullText, // 实时显示累积文本 durationMs: isComplete ? (performance.now() - (currentQueryStartTimeRef.current || 0)) : undefined } : msg )); }, finalAnswerRole.model.maxTokens // 传递模型配置的 maxTokens ).then(finalResponse => { if (cancelRequestRef.current) { setIsDiscussionActive(false); setCurrentDiscussion(null); return; } if (finalResponse.error) { if (finalResponse.error.includes("API key not valid") || finalResponse.error.includes("401")) { setMessages(prev => prev.map(msg => msg.id === finalMessageId ? { ...msg, text: `API密钥无效 (渠道: ${finalAnswerRole.channel.name}),请在配置界面中检查密钥设置。`, durationMs: finalResponse.durationMs } : msg )); setIsLoading(false); setIsDiscussionActive(false); setCurrentDiscussion(null); return; } throw new Error(`${finalAnswerRole.name}: ${finalResponse.text}`); } setMessages(prev => prev.map(msg => msg.id === finalMessageId ? { ...msg, text: finalResponse.text, durationMs: finalResponse.durationMs } : msg )); const finalParsedResponse = parseAIResponse(finalResponse.text); if (finalParsedResponse.newNotepadContent !== null) { setNotepadContent(finalParsedResponse.newNotepadContent); setLastNotepadUpdateBy(createDynamicMessageSender(finalAnswerRole.name)); } // 讨论完成 setIsLoading(false); setIsDiscussionActive(false); setCurrentDiscussion(null); currentQueryStartTimeRef.current = null; }).catch(error => { console.error("生成最终答案时出错:", error); addMessage(`错误: ${error instanceof Error ? error.message : "生成最终答案时发生未知错误"}`, MessageSender.System, MessagePurpose.SystemNotification); setIsLoading(false); setIsDiscussionActive(false); setCurrentDiscussion(null); }); }; const handleSendMessage = async (userInput: string, imageFile?: File | null) => { if (isLoading) return; if (!userInput.trim() && !imageFile) return; if (channels.length === 0) { addMessage("请先配置API渠道。点击设置按钮添加渠道。", MessageSender.System, MessagePurpose.SystemNotification); return; } if (activeRoles.length === 0) { addMessage("请先配置AI角色。点击设置按钮添加角色。", MessageSender.System, MessagePurpose.SystemNotification); return; } const rolesWithoutApiKey = activeRoles.filter(role => !role.channel.apiKey?.trim()); if (rolesWithoutApiKey.length > 0) { const roleNames = rolesWithoutApiKey.map(role => `${role.name}(${role.channel.name})`).join('、'); addMessage(`以下角色的API渠道缺少API密钥: ${roleNames}。请在配置界面中设置相应的API密钥。`, MessageSender.System, MessagePurpose.SystemNotification); return; } if (imageFile) { const supportsImages = activeRoles.some(role => role.model.supportsImages); if (!supportsImages) { addMessage("当前活跃的角色都不支持图片处理。请添加支持图片的模型和角色,或移除图片。", MessageSender.System, MessagePurpose.SystemNotification); return; } } setIsDiscussionActive(true); cancelRequestRef.current = false; setIsLoading(true); currentQueryStartTimeRef.current = performance.now(); setCurrentTotalProcessingTimeMs(0); let userImageForDisplay: ChatMessage['image'] | undefined = undefined; if (imageFile) { const dataUrl = URL.createObjectURL(imageFile); userImageForDisplay = { dataUrl, name: imageFile.name, type: imageFile.type }; } addMessage(userInput, MessageSender.User, MessagePurpose.UserInput, undefined, userImageForDisplay); let imageApiPart: { inlineData: { mimeType: string; data: string } } | undefined = undefined; if (imageFile) { try { const base64Data = await fileToBase64(imageFile); imageApiPart = { inlineData: { mimeType: imageFile.type, data: base64Data, }, }; } catch (error) { console.error("Error converting file to base64:", error); addMessage("图片处理失败,请重试。", MessageSender.System, MessagePurpose.SystemNotification); setIsLoading(false); setIsDiscussionActive(false); return; } } const discussionModeInstruction = discussionMode === DiscussionMode.AiDriven ? AI_DRIVEN_DISCUSSION_INSTRUCTION_PROMPT_PART : ""; const commonPromptInstructions = NOTEPAD_INSTRUCTION_PROMPT_PART.replace('{notepadContent}', notepadContent) + discussionModeInstruction; const roleOrder = [...activeRoles]; const maxTurnsForLoop = discussionMode === DiscussionMode.AiDriven ? MAX_AI_DRIVEN_DISCUSSION_TURNS_PER_MODEL : manualFixedTurns; // 初始化讨论状态 const discussionState: DiscussionState = { currentRoleIndex: 0, currentTurn: 0, discussionLog: [], isFirstMessage: true, previousAISignaledStop: false, discussionEndCount: 0, userQuery: userInput, imageApiPart, commonPromptInstructions, roleOrder, maxTurnsForLoop }; setCurrentDiscussion(discussionState); // 开始第一个AI的回复(不等待) processNextRole(discussionState); // 清理图片URL if (userImageForDisplay?.dataUrl.startsWith('blob:')) { // 延迟清理,确保消息已渲染 setTimeout(() => { URL.revokeObjectURL(userImageForDisplay.dataUrl); }, 5000); } }; const Separator = () => ; const hasValidChannels = channels.some(ch => ch.apiKey?.trim()); const isSystemReady = hasValidChannels && activeRoles.length > 0; return (

Multi-Mind Chat 智囊团

{/* 讨论控制按钮 */} {isDiscussionActive && (
)} {/* 导出按钮 - 有消息时始终可用 */} {messages.length > 1 && (
)} {/* 角色管理器 */}
{isRoleSelectorOpen && (

活跃角色

{activeRoles.length === 0 ? (

暂无活跃角色

) : (
{activeRoles.map((role) => (

{role.name}

{role.model.name}

渠道: {role.channel.name}

{role.model.supportsImages && ( 图像 )} {role.model.supportsReducedCapacity && ( 优化 )} {!role.channel.apiKey?.trim() && ( 缺少密钥 )}
))}
)}
)} {isRoleSelectorOpen && (
setIsRoleSelectorOpen(false)} /> )}
{discussionMode === DiscussionMode.FixedTurns && (
)}
{messages.map((msg) => { const streamingState = streamingMessages.get(msg.id); const displayMessage = streamingState && !streamingState.isComplete ? { ...msg, text: streamingState.text } : msg; return ; })}
{/* 配置管理器 */} setIsConfigManagerOpen(false)} onConfigChange={loadConfiguration} /> {/* 处理时间显示 */} {(isLoading || (currentTotalProcessingTimeMs > 0 && !isLoading) || (isLoading && currentTotalProcessingTimeMs === 0)) && (
总耗时: {(currentTotalProcessingTimeMs / 1000).toFixed(2)}s {isDiscussionActive && (
讨论进行中...
)}
)} {/* 系统状态提示 */} {!isSystemReady && (
{!hasValidChannels ? '请配置API渠道和密钥' : '请配置AI角色'} 。点击设置按钮进行配置。
)}
); }; export default App;