import React, { useState, useEffect, useRef } from 'react'; import { Plus, Search, Play, Copy, Trash2, Mic, MessageSquare, Save, Video, PhoneOff, Camera, ArrowLeftRight, Send, Phone, Rocket, AlertTriangle, PhoneCall, CameraOff, Image, Images, CloudSun, Calendar, TrendingUp, Coins, Wrench, Globe, Terminal, X, ClipboardCheck, Sparkles, Volume2, Timer, ChevronDown, Database, Server, Zap, ExternalLink, Key, BrainCircuit, Ear, Book, Filter } from 'lucide-react'; import { Button, Input, Badge, Drawer, Dialog } from '../components/UI'; import { ASRModel, Assistant, KnowledgeBase, LLMModel, TabValue, Tool, Voice } from '../types'; import { createAssistant, deleteAssistant, fetchASRModels, fetchAssistants, fetchKnowledgeBases, fetchLLMModels, fetchTools, fetchVoices, updateAssistant as updateAssistantApi } from '../services/backendApi'; const isSiliconflowVendor = (vendor?: string) => { const normalized = String(vendor || '').trim().toLowerCase(); return normalized === 'siliconflow' || normalized === '硅基流动'; }; const SILICONFLOW_DEFAULT_MODEL = 'FunAudioLLM/CosyVoice2-0.5B'; const buildSiliconflowVoiceKey = (voiceId: string, model?: string) => { const id = String(voiceId || '').trim(); if (!id) return ''; if (id.includes(':')) return id; return `${model || SILICONFLOW_DEFAULT_MODEL}:${id}`; }; const resolveRuntimeTtsVoice = (selectedVoiceId: string, voice: Voice) => { const explicitKey = String(voice.voiceKey || '').trim(); if (!isSiliconflowVendor(voice.vendor)) { return explicitKey || selectedVoiceId; } if (voice.isSystem) { const canonical = buildSiliconflowVoiceKey(selectedVoiceId, voice.model); if (!explicitKey) return canonical; const explicitSuffix = explicitKey.includes(':') ? explicitKey.split(':').pop() : explicitKey; if (explicitSuffix && explicitSuffix !== selectedVoiceId) return canonical; } return explicitKey || buildSiliconflowVoiceKey(selectedVoiceId, voice.model); }; const renderToolIcon = (icon: string) => { const className = 'w-4 h-4'; const map: Record = { Camera: , CameraOff: , Image: , Images: , CloudSun: , Calendar: , TrendingUp: , Coins: , Terminal: , Globe: , Wrench: , }; return map[icon] || ; }; export const AssistantsPage: React.FC = () => { const [assistants, setAssistants] = useState([]); const [voices, setVoices] = useState([]); const [knowledgeBases, setKnowledgeBases] = useState([]); const [llmModels, setLlmModels] = useState([]); const [asrModels, setAsrModels] = useState([]); const [tools, setTools] = useState([]); const [searchTerm, setSearchTerm] = useState(''); const [selectedId, setSelectedId] = useState(null); const [activeTab, setActiveTab] = useState(TabValue.GLOBAL); const [debugOpen, setDebugOpen] = useState(false); const [hotwordInput, setHotwordInput] = useState(''); // Publish Modal State const [isPublishModalOpen, setIsPublishModalOpen] = useState(false); const [publishTab, setPublishTab] = useState<'web' | 'api'>('web'); const [deleteId, setDeleteId] = useState(null); const [copySuccess, setCopySuccess] = useState(false); const [saveLoading, setSaveLoading] = useState(false); const [isLoading, setIsLoading] = useState(true); const selectedAssistant = assistants.find(a => a.id === selectedId) || null; const filteredAssistants = assistants.filter(a => a.name.toLowerCase().includes(searchTerm.toLowerCase()) ); useEffect(() => { const loadInitialData = async () => { setIsLoading(true); try { const [assistantList, voiceList, kbList, llmList, asrList, toolList] = await Promise.all([ fetchAssistants(), fetchVoices(), fetchKnowledgeBases(), fetchLLMModels(), fetchASRModels(), fetchTools(), ]); setAssistants(assistantList); setVoices(voiceList); setKnowledgeBases(kbList); setLlmModels(llmList); setAsrModels(asrList); setTools(toolList); if (assistantList.length > 0) { setSelectedId(assistantList[0].id); } } catch (error) { console.error(error); alert('加载助手数据失败,请检查后端服务是否启动。'); } finally { setIsLoading(false); } }; loadInitialData(); }, []); const handleCreate = async () => { const newAssistantPayload: Partial = { name: 'New Assistant', opener: '', prompt: '', knowledgeBaseId: '', language: 'zh', voice: voices[0]?.id || '', speed: 1, hotwords: [], tools: [], interruptionSensitivity: 500, configMode: 'platform', }; try { const created = await createAssistant(newAssistantPayload); setAssistants((prev) => [created, ...prev]); setSelectedId(created.id); setActiveTab(TabValue.GLOBAL); } catch (error) { console.error(error); alert('创建助手失败。'); } }; const handleSave = async () => { if (!selectedAssistant) return; setSaveLoading(true); try { const updated = await updateAssistantApi(selectedAssistant.id, selectedAssistant); setAssistants((prev) => prev.map((item) => (item.id === updated.id ? { ...item, ...updated } : item))); } catch (error) { console.error(error); alert('保存失败,请稍后重试。'); } finally { setSaveLoading(false); } }; const handleCopyId = (id: string, text?: string) => { navigator.clipboard.writeText(text || id); setCopySuccess(true); setTimeout(() => setCopySuccess(false), 2000); }; const handleCopy = async (e: React.MouseEvent, assistant: Assistant) => { e.stopPropagation(); try { const copied = await createAssistant({ ...assistant, name: `${assistant.name} (Copy)`, }); setAssistants((prev) => [copied, ...prev]); } catch (error) { console.error(error); alert('复制助手失败。'); } }; const handleDeleteClick = (e: React.MouseEvent, id: string) => { e.stopPropagation(); setDeleteId(id); }; const confirmDelete = async () => { if (deleteId) { try { await deleteAssistant(deleteId); setAssistants(prev => prev.filter(a => a.id !== deleteId)); if (selectedId === deleteId) setSelectedId(null); setDeleteId(null); } catch (error) { console.error(error); alert('删除失败,请稍后重试。'); } } }; const updateAssistant = (field: keyof Assistant, value: any) => { if (!selectedId) return; setAssistants(prev => prev.map(a => a.id === selectedId ? { ...a, [field]: value } : a)); if (field === 'configMode') { if (value === 'platform') { setActiveTab(TabValue.GLOBAL); } else if (value === 'dify' || value === 'fastgpt') { setActiveTab(TabValue.LINK); } } }; const toggleTool = (toolId: string) => { if (!selectedAssistant) return; const currentTools = selectedAssistant.tools || []; const newTools = currentTools.includes(toolId) ? currentTools.filter(id => id !== toolId) : [...currentTools, toolId]; updateAssistant('tools', newTools); }; const removeImportedTool = (e: React.MouseEvent, tool: Tool) => { e.stopPropagation(); if (!selectedAssistant) return; updateAssistant('tools', (selectedAssistant.tools || []).filter((id) => id !== tool.id)); }; const addHotword = () => { if (hotwordInput.trim() && selectedAssistant) { updateAssistant('hotwords', [...selectedAssistant.hotwords, hotwordInput.trim()]); setHotwordInput(''); } }; const removeHotword = (word: string) => { if (selectedAssistant) { updateAssistant('hotwords', selectedAssistant.hotwords.filter(w => w !== word)); } }; const systemTools = tools.filter((t) => t.enabled !== false && t.category === 'system'); const queryTools = tools.filter((t) => t.enabled !== false && t.category === 'query'); const isExternalConfig = selectedAssistant?.configMode === 'dify' || selectedAssistant?.configMode === 'fastgpt'; const isNoneConfig = selectedAssistant?.configMode === 'none' || !selectedAssistant?.configMode; return (
{/* LEFT COLUMN: List */}

小助手列表

setSearchTerm(e.target.value)} />
{!isLoading && filteredAssistants.map(assistant => (
setSelectedId(assistant.id)} className={`group relative flex flex-col p-4 rounded-xl border transition-all cursor-pointer ${ selectedId === assistant.id ? 'bg-primary/10 border-primary/40 shadow-[0_0_15px_rgba(6,182,212,0.15)]' : 'bg-card/30 border-white/5 hover:bg-white/5 hover:border-white/10' }`} >
{assistant.name} {assistant.configMode && assistant.configMode !== 'none' && (
{assistant.configMode === 'platform' ? '内置' : assistant.configMode}
)}
{assistant.callCount} 次通话
))} {!isLoading && filteredAssistants.length === 0 && (
未找到小助手
)} {isLoading && (
加载中...
)}
{/* RIGHT COLUMN: Config Panel */}
{selectedAssistant ? ( <> {/* Header Area */}
UUID: {selectedAssistant.id}
updateAssistant('name', e.target.value)} className="font-bold bg-white/5 border-white/10 focus:border-primary/50 text-base" />
{!isNoneConfig && (
{selectedAssistant.configMode === 'platform' ? ( <> ) : ( <> )}
)}
{isNoneConfig ? (

请先选择配置方式以展开详细设置

) : (
{activeTab === TabValue.LINK && isExternalConfig && (

接入 {selectedAssistant.configMode === 'dify' ? 'Dify' : 'FastGPT'} 引擎

配置后,视频通话过程中的对话逻辑、知识库检索以及工作流将由外部引擎托管。

updateAssistant('apiUrl', e.target.value)} placeholder={selectedAssistant.configMode === 'dify' ? "https://api.dify.ai/v1" : "https://api.fastgpt.in/api/v1"} className="bg-white/5 border-white/10 focus:border-primary/50 font-mono text-xs" />
updateAssistant('apiKey', e.target.value)} placeholder="请输入应用 API 密钥..." className="bg-white/5 border-white/10 focus:border-primary/50 font-mono text-xs" />
)} {activeTab === TabValue.GLOBAL && selectedAssistant.configMode === 'platform' && (

选择用于驱动该助手对话的大语言模型。

updateAssistant('opener', e.target.value)} placeholder="例如:您好,我是您的专属AI助手..." className="bg-white/5 border-white/10 focus:border-primary/50" />

接通通话后的第一句话。