From dc3130d3872429b2a4b6d4b880593fca27ce85c2 Mon Sep 17 00:00:00 2001 From: Xin Wang Date: Fri, 6 Feb 2026 23:09:24 +0800 Subject: [PATCH] Update web page config --- web/App.tsx | 148 +++++++++++++++++++---- web/README.md | 2 +- web/metadata.json | 5 +- web/package.json | 2 +- web/pages/ASRLibrary.tsx | 235 ++++++++++++++++++++++++++++++++++++ web/pages/Dashboard.tsx | 4 +- web/pages/History.tsx | 138 +++++++++++++++++++-- web/pages/LLMLibrary.tsx | 248 ++++++++++++++++++++++++++++++++++++++ web/pages/ToolLibrary.tsx | 191 +++++++++++++++++++++++++++++ web/services/mockData.ts | 56 ++++++++- web/types.ts | 40 ++++++ 11 files changed, 1028 insertions(+), 41 deletions(-) create mode 100644 web/pages/ASRLibrary.tsx create mode 100644 web/pages/LLMLibrary.tsx create mode 100644 web/pages/ToolLibrary.tsx diff --git a/web/App.tsx b/web/App.tsx index a3d2910..71afd91 100644 --- a/web/App.tsx +++ b/web/App.tsx @@ -1,7 +1,7 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import { HashRouter as Router, Routes, Route, Link, useLocation } from 'react-router-dom'; -import { Bot, Phone, Book, User, LayoutDashboard, Mic2, Video, GitBranch, Zap, PanelLeftClose, PanelLeftOpen, History as HistoryIcon } from 'lucide-react'; +import { Bot, Book, User, LayoutDashboard, Mic2, Video, GitBranch, Zap, PanelLeftClose, PanelLeftOpen, History as HistoryIcon, ChevronDown, ChevronRight, Box, Wand2, Wrench, BrainCircuit, AudioLines } from 'lucide-react'; import { AssistantsPage } from './pages/Assistants'; import { KnowledgeBasePage } from './pages/KnowledgeBase'; @@ -12,30 +12,132 @@ import { VoiceLibraryPage } from './pages/VoiceLibrary'; import { WorkflowsPage } from './pages/Workflows'; import { WorkflowEditorPage } from './pages/WorkflowEditor'; import { AutoTestPage } from './pages/AutoTest'; +import { ToolLibraryPage } from './pages/ToolLibrary'; +import { LLMLibraryPage } from './pages/LLMLibrary'; +import { ASRLibraryPage } from './pages/ASRLibrary'; -const SidebarItem: React.FC<{ to: string; icon: React.ReactNode; label: string; active: boolean; isCollapsed: boolean }> = ({ to, icon, label, active, isCollapsed }) => ( - -
{icon}
- {!isCollapsed && {label}} - -); +type NavItemType = { + path: string; + label: string; + icon: React.ReactNode; + children?: NavItemType[]; +}; + +const SidebarItem: React.FC<{ + item: NavItemType; + isActive: boolean; + isCollapsed: boolean; + onExpand: () => void; +}> = ({ item, isActive, isCollapsed, onExpand }) => { + const location = useLocation(); + const [isOpen, setIsOpen] = useState(false); + const hasChildren = item.children && item.children.length > 0; + + // Check if any child is active to auto-expand + const isChildActive = hasChildren && item.children!.some(child => location.pathname.startsWith(child.path)); + + useEffect(() => { + if (isChildActive) { + setIsOpen(true); + } + }, [isChildActive]); + + const handleClick = (e: React.MouseEvent) => { + if (hasChildren) { + e.preventDefault(); + if (isCollapsed) { + onExpand(); + setIsOpen(true); + } else { + setIsOpen(!isOpen); + } + } + }; + + const activeClass = "bg-primary/20 text-primary border-r-2 border-primary"; + const inactiveClass = "text-muted-foreground hover:bg-muted/50 hover:text-foreground"; + + if (hasChildren) { + return ( +
+
+
+
{item.icon}
+ {!isCollapsed && {item.label}} +
+ {!isCollapsed && ( +
+ {isOpen ? : } +
+ )} +
+ + {!isCollapsed && isOpen && ( +
+ {item.children!.map(child => { + const childActive = location.pathname.startsWith(child.path); + return ( + +
{child.icon}
+ {child.label} + + ); + })} +
+ )} +
+ ); + } + + return ( + +
{item.icon}
+ {!isCollapsed && {item.label}} + + ); +}; const AppLayout: React.FC<{ children: React.ReactNode }> = ({ children }) => { const location = useLocation(); const [isCollapsed, setIsCollapsed] = useState(false); - const navItems = [ + const navItems: NavItemType[] = [ { path: '/', label: '首页', icon: }, - { path: '/assistants', label: '小助手', icon: }, - { path: '/voices', label: '声音库', icon: }, + { + path: '#creation', + label: '创建助手', + icon: , + children: [ + { path: '/assistants', label: '小助手', icon: }, + { path: '/workflows', label: '工作流', icon: }, + ] + }, { path: '/history', label: '历史记录', icon: }, - { path: '/knowledge', label: '知识库', icon: }, - { path: '/workflows', label: '工作流', icon: }, { path: '/auto-test', label: '测试助手', icon: }, + { + path: '#components', + label: '组件库', + icon: , + children: [ + { path: '/llms', label: '大模型库', icon: }, + { path: '/asr', label: '语音识别', icon: }, + { path: '/voices', label: '声音库', icon: }, + { path: '/knowledge', label: '知识库', icon: }, + { path: '/tools', label: '工具库', icon: }, + ] + }, { path: '/profile', label: '个人中心', icon: }, ]; @@ -60,11 +162,10 @@ const AppLayout: React.FC<{ children: React.ReactNode }> = ({ children }) => { {navItems.map(item => ( setIsCollapsed(false)} /> ))} @@ -110,7 +211,10 @@ const App: React.FC = () => { } /> } /> } /> + } /> + } /> } /> + } /> } /> } /> } /> @@ -123,4 +227,4 @@ const App: React.FC = () => { ); }; -export default App; +export default App; \ No newline at end of file diff --git a/web/README.md b/web/README.md index 4ec187a..a64d1ba 100644 --- a/web/README.md +++ b/web/README.md @@ -6,7 +6,7 @@ This contains everything you need to run your app locally. -View your app in AI Studio: https://ai.studio/apps/drive/1GwAKXIF6lVjo1AZPHjVDL6w3hXkG_zFQ +View your app in AI Studio: https://ai.studio/apps/drive/1Cg9WH_2bOQEHVVj-lSN5l2oUtfjZK8hF ## Run Locally diff --git a/web/metadata.json b/web/metadata.json index 863ed15..6fbed41 100644 --- a/web/metadata.json +++ b/web/metadata.json @@ -1,9 +1,8 @@ - { - "name": "AI视频助手", + "name": "AI视频助手2", "description": "A minimalist AI Assistant management system featuring agent configuration, knowledge base management, call logs, and a debugging suite.", "requestFramePermissions": [ "microphone", "camera" ] -} +} \ No newline at end of file diff --git a/web/package.json b/web/package.json index a0ba80d..f9faf9f 100644 --- a/web/package.json +++ b/web/package.json @@ -1,5 +1,5 @@ { - "name": "ai视频助手", + "name": "ai视频助手2", "private": true, "version": "0.0.0", "type": "module", diff --git a/web/pages/ASRLibrary.tsx b/web/pages/ASRLibrary.tsx new file mode 100644 index 0000000..a47b845 --- /dev/null +++ b/web/pages/ASRLibrary.tsx @@ -0,0 +1,235 @@ + +import React, { useState } from 'react'; +import { Search, Filter, Plus, Trash2, Key, Server, Ear, Globe, Languages } from 'lucide-react'; +import { Button, Input, TableHeader, TableRow, TableHead, TableCell, Dialog, Badge } from '../components/UI'; +import { mockASRModels } from '../services/mockData'; +import { ASRModel } from '../types'; + +export const ASRLibraryPage: React.FC = () => { + const [models, setModels] = useState(mockASRModels); + const [searchTerm, setSearchTerm] = useState(''); + const [vendorFilter, setVendorFilter] = useState('all'); + const [langFilter, setLangFilter] = useState('all'); + const [isAddModalOpen, setIsAddModalOpen] = useState(false); + + // Form State + const [newModel, setNewModel] = useState>({ + vendor: 'OpenAI Compatible', + language: 'zh' + }); + + const filteredModels = models.filter(m => { + const matchesSearch = m.name.toLowerCase().includes(searchTerm.toLowerCase()); + const matchesVendor = vendorFilter === 'all' || m.vendor === vendorFilter; + const matchesLang = langFilter === 'all' || m.language === langFilter || (langFilter !== 'all' && m.language === 'Multi-lingual'); + return matchesSearch && matchesVendor && matchesLang; + }); + + const handleAddModel = () => { + if (!newModel.name || !newModel.baseUrl || !newModel.apiKey) { + alert("请填写完整信息"); + return; + } + + const model: ASRModel = { + id: `asr_${Date.now()}`, + name: newModel.name, + vendor: newModel.vendor as 'OpenAI Compatible', + language: newModel.language || 'zh', + baseUrl: newModel.baseUrl, + apiKey: newModel.apiKey + }; + + setModels([model, ...models]); + setIsAddModalOpen(false); + setNewModel({ vendor: 'OpenAI Compatible', language: 'zh', name: '', baseUrl: '', apiKey: '' }); + }; + + const handleDeleteModel = (id: string) => { + if (confirm('确认删除该语音识别模型吗?')) { + setModels(prev => prev.filter(m => m.id !== id)); + } + }; + + const maskApiKey = (key: string) => { + if (!key || key.length < 8) return '********'; + return `${key.substring(0, 3)}****${key.substring(key.length - 4)}`; + }; + + return ( +
+
+

语音识别

+ +
+ +
+
+ + setSearchTerm(e.target.value)} + /> +
+
+ + +
+
+ +
+
+ +
+ + + + 模型名称 + 接口类型 + 语言 + Base URL + API Key + 操作 + + + + {filteredModels.map(model => ( + + + + {model.name} + + + {model.vendor} + + + + {model.language} + + + + {model.baseUrl} + + + {maskApiKey(model.apiKey)} + + + + + + ))} + {filteredModels.length === 0 && ( + + 暂无语音识别模型 + + )} + +
+
+ + setIsAddModalOpen(false)} + title="添加语音识别模型" + footer={ + <> + + + + } + > +
+
+ + +
+ +
+ +
+ {(['zh', 'en', 'Multi-lingual'] as const).map(l => ( + + ))} +
+
+ +
+ + setNewModel({...newModel, name: e.target.value})} + placeholder="例如: whisper-1, funasr" + /> +
+ +
+ + setNewModel({...newModel, baseUrl: e.target.value})} + placeholder="https://api.openai.com/v1" + className="font-mono text-xs" + /> +
+ +
+ + setNewModel({...newModel, apiKey: e.target.value})} + placeholder="sk-..." + className="font-mono text-xs" + /> +
+
+
+
+ ); +}; diff --git a/web/pages/Dashboard.tsx b/web/pages/Dashboard.tsx index 7ba8460..6b34ff5 100644 --- a/web/pages/Dashboard.tsx +++ b/web/pages/Dashboard.tsx @@ -140,8 +140,8 @@ export const DashboardPage: React.FC = () => { - {/* 6. Platform Feature Intro - Moved to Bottom, Full Width */} -
+ {/* 6. Platform Feature Intro - Updated Background */} +
diff --git a/web/pages/History.tsx b/web/pages/History.tsx index 818d503..51cc056 100644 --- a/web/pages/History.tsx +++ b/web/pages/History.tsx @@ -1,29 +1,35 @@ import React, { useState } from 'react'; -import { Download, Search, Calendar, Filter } from 'lucide-react'; -import { Button, Input, TableHeader, TableRow, TableHead, TableCell, Badge } from '../components/UI'; +import { Download, Search, Calendar, Filter, MessageSquare, Mic, Video, Eye, X, Play } from 'lucide-react'; +import { Button, Input, TableHeader, TableRow, TableHead, TableCell, Badge, Drawer } from '../components/UI'; import { mockCallLogs } from '../services/mockData'; +import { CallLog, InteractionType } from '../types'; export const HistoryPage: React.FC = () => { const [logs] = useState(mockCallLogs); const [searchTerm, setSearchTerm] = useState(''); const [statusFilter, setStatusFilter] = useState<'all' | 'connected' | 'missed'>('all'); const [sourceFilter, setSourceFilter] = useState<'all' | 'debug' | 'external'>('all'); + const [typeFilter, setTypeFilter] = useState<'all' | InteractionType>('all'); + + const [selectedLog, setSelectedLog] = useState(null); const filteredLogs = logs.filter(log => { const matchesSearch = log.agentName.toLowerCase().includes(searchTerm.toLowerCase()); const matchesStatus = statusFilter === 'all' || log.status === statusFilter; const matchesSource = sourceFilter === 'all' || log.source === sourceFilter; - return matchesSearch && matchesStatus && matchesSource; + const matchesType = typeFilter === 'all' || log.type === typeFilter; + return matchesSearch && matchesStatus && matchesSource && matchesType; }); const handleExport = () => { // Generate CSV content - const headers = ['ID', 'Agent', 'Source', 'Status', 'Start Time', 'Duration']; + const headers = ['ID', 'Agent', 'Source', 'Type', 'Status', 'Start Time', 'Duration']; const rows = filteredLogs.map(log => [ log.id, log.agentName, - log.source, + log.source, + log.type, log.status, log.startTime, log.duration @@ -47,7 +53,7 @@ export const HistoryPage: React.FC = () => {
-
+
{
+
+ +
setSearchTerm(e.target.value)} + /> +
+
+ + +
+
+ +
+
+ +
+ + + + 模型名称 + 厂商 + 类型 + Base URL + 操作 + + + + {filteredModels.map(model => ( + + + + {model.name} + + + {model.vendor} + + + + {model.type.toUpperCase()} + + + + {model.baseUrl} + + + + + + ))} + {filteredModels.length === 0 && ( + + 暂无模型数据 + + )} + +
+
+ + setIsAddModalOpen(false)} + title="添加大模型" + footer={ + <> + + + + } + > +
+
+ + +
+ +
+ +
+ {(['text', 'embedding', 'rerank'] as const).map(t => ( + + ))} +
+
+ +
+ + setNewModel({...newModel, name: e.target.value})} + placeholder="例如: gpt-4o, deepseek-chat" + /> +
+ +
+ + setNewModel({...newModel, baseUrl: e.target.value})} + placeholder="https://api.openai.com/v1" + className="font-mono text-xs" + /> +
+ +
+ + setNewModel({...newModel, apiKey: e.target.value})} + placeholder="sk-..." + className="font-mono text-xs" + /> +
+ + {newModel.type === 'text' && ( +
+
+ + {newModel.temperature} +
+ setNewModel({...newModel, temperature: parseFloat(e.target.value)})} + className="w-full h-1.5 bg-secondary rounded-lg appearance-none cursor-pointer accent-primary" + /> +
+ )} +
+
+
+ ); +}; diff --git a/web/pages/ToolLibrary.tsx b/web/pages/ToolLibrary.tsx new file mode 100644 index 0000000..77c1720 --- /dev/null +++ b/web/pages/ToolLibrary.tsx @@ -0,0 +1,191 @@ + +import React, { useState } from 'react'; +import { Search, Filter, Plus, Wrench, Terminal, Globe, Camera, CameraOff, Image, Images, CloudSun, Calendar, TrendingUp, Coins, Trash2, Edit2, X, Box } from 'lucide-react'; +import { Button, Input, Badge, Dialog } from '../components/UI'; +import { mockTools } from '../services/mockData'; +import { Tool } from '../types'; + +// Map icon strings to React Nodes +const iconMap: Record = { + Camera: , + CameraOff: , + Image: , + Images: , + CloudSun: , + Calendar: , + TrendingUp: , + Coins: , + Terminal: , + Globe: , + Wrench: , +}; + +export const ToolLibraryPage: React.FC = () => { + const [tools, setTools] = useState(mockTools); + const [searchTerm, setSearchTerm] = useState(''); + const [categoryFilter, setCategoryFilter] = useState<'all' | 'system' | 'query'>('all'); + const [isAddModalOpen, setIsAddModalOpen] = useState(false); + + // New Tool Form + const [newToolName, setNewToolName] = useState(''); + const [newToolDesc, setNewToolDesc] = useState(''); + const [newToolCategory, setNewToolCategory] = useState<'system' | 'query'>('system'); + + const filteredTools = tools.filter(tool => { + const matchesSearch = tool.name.toLowerCase().includes(searchTerm.toLowerCase()); + const matchesCategory = categoryFilter === 'all' || tool.category === categoryFilter; + return matchesSearch && matchesCategory; + }); + + const handleAddTool = () => { + if (!newToolName.trim()) return; + const newTool: Tool = { + id: `custom_${Date.now()}`, + name: newToolName, + description: newToolDesc, + category: newToolCategory, + icon: newToolCategory === 'system' ? 'Terminal' : 'Globe', + isCustom: true + }; + setTools([...tools, newTool]); + setIsAddModalOpen(false); + setNewToolName(''); + setNewToolDesc(''); + }; + + const handleDeleteTool = (e: React.MouseEvent, id: string) => { + e.stopPropagation(); + if (confirm('确认删除该工具吗?')) { + setTools(prev => prev.filter(t => t.id !== id)); + } + }; + + return ( +
+
+

工具库

+ +
+ +
+
+ + setSearchTerm(e.target.value)} + /> +
+
+ + +
+
+ +
+ {filteredTools.map(tool => ( +
+
+ {iconMap[tool.icon] || } +
+
+
+ {tool.name} + {tool.isCustom && CUSTOM} +
+
+ + {tool.category === 'system' ? 'SYSTEM' : 'QUERY'} + + ID: {tool.id} +
+

{tool.description}

+
+ + {tool.isCustom && ( +
+ +
+ )} +
+ ))} + {filteredTools.length === 0 && ( +
+ +

未找到相关工具

+
+ )} +
+ + setIsAddModalOpen(false)} + title="添加自定义工具" + footer={ + <> + + + + } + > +
+
+ +
+ + +
+
+ +
+ + setNewToolName(e.target.value)} + placeholder="例如: 智能家居控制" + autoFocus + /> +
+
+ +