Update web page config
This commit is contained in:
148
web/App.tsx
148
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 }) => (
|
||||
<Link
|
||||
to={to}
|
||||
title={isCollapsed ? label : undefined}
|
||||
className={`flex items-center space-x-3 px-4 py-3 rounded-md transition-all duration-300 ${active ? 'bg-primary/20 text-primary border-r-2 border-primary' : 'text-muted-foreground hover:bg-muted/50 hover:text-foreground'} ${isCollapsed ? 'justify-center space-x-0 px-2' : ''}`}
|
||||
>
|
||||
<div className="shrink-0">{icon}</div>
|
||||
{!isCollapsed && <span className="font-medium text-sm animate-in fade-in duration-300 whitespace-nowrap overflow-hidden">{label}</span>}
|
||||
</Link>
|
||||
);
|
||||
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 (
|
||||
<div className="space-y-1">
|
||||
<div
|
||||
onClick={handleClick}
|
||||
className={`flex items-center justify-between px-4 py-3 rounded-md transition-all duration-300 cursor-pointer ${isChildActive ? 'text-primary' : 'text-muted-foreground hover:bg-muted/50 hover:text-foreground'} ${isCollapsed ? 'justify-center px-2' : ''}`}
|
||||
title={isCollapsed ? item.label : undefined}
|
||||
>
|
||||
<div className={`flex items-center space-x-3 ${isCollapsed ? 'justify-center w-full' : ''}`}>
|
||||
<div className="shrink-0">{item.icon}</div>
|
||||
{!isCollapsed && <span className="font-medium text-sm whitespace-nowrap overflow-hidden">{item.label}</span>}
|
||||
</div>
|
||||
{!isCollapsed && (
|
||||
<div className="shrink-0 ml-2">
|
||||
{isOpen ? <ChevronDown className="h-4 w-4" /> : <ChevronRight className="h-4 w-4" />}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!isCollapsed && isOpen && (
|
||||
<div className="ml-4 space-y-1 border-l border-white/10 pl-2 animate-in slide-in-from-left-2 duration-200">
|
||||
{item.children!.map(child => {
|
||||
const childActive = location.pathname.startsWith(child.path);
|
||||
return (
|
||||
<Link
|
||||
key={child.path}
|
||||
to={child.path}
|
||||
className={`flex items-center space-x-3 px-4 py-2 rounded-md transition-all duration-300 text-sm ${childActive ? 'text-primary bg-primary/10' : 'text-muted-foreground hover:text-foreground'}`}
|
||||
>
|
||||
<div className="shrink-0 opacity-70 scale-90">{child.icon}</div>
|
||||
<span className="font-medium whitespace-nowrap overflow-hidden">{child.label}</span>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Link
|
||||
to={item.path}
|
||||
title={isCollapsed ? item.label : undefined}
|
||||
className={`flex items-center space-x-3 px-4 py-3 rounded-md transition-all duration-300 ${isActive ? activeClass : inactiveClass} ${isCollapsed ? 'justify-center space-x-0 px-2' : ''}`}
|
||||
>
|
||||
<div className="shrink-0">{item.icon}</div>
|
||||
{!isCollapsed && <span className="font-medium text-sm animate-in fade-in duration-300 whitespace-nowrap overflow-hidden">{item.label}</span>}
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
const AppLayout: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
const location = useLocation();
|
||||
const [isCollapsed, setIsCollapsed] = useState(false);
|
||||
|
||||
const navItems = [
|
||||
const navItems: NavItemType[] = [
|
||||
{ path: '/', label: '首页', icon: <LayoutDashboard className="h-5 w-5" /> },
|
||||
{ path: '/assistants', label: '小助手', icon: <Bot className="h-5 w-5" /> },
|
||||
{ path: '/voices', label: '声音库', icon: <Mic2 className="h-5 w-5" /> },
|
||||
{
|
||||
path: '#creation',
|
||||
label: '创建助手',
|
||||
icon: <Wand2 className="h-5 w-5" />,
|
||||
children: [
|
||||
{ path: '/assistants', label: '小助手', icon: <Bot className="h-5 w-5" /> },
|
||||
{ path: '/workflows', label: '工作流', icon: <GitBranch className="h-5 w-5" /> },
|
||||
]
|
||||
},
|
||||
{ path: '/history', label: '历史记录', icon: <HistoryIcon className="h-5 w-5" /> },
|
||||
{ path: '/knowledge', label: '知识库', icon: <Book className="h-5 w-5" /> },
|
||||
{ path: '/workflows', label: '工作流', icon: <GitBranch className="h-5 w-5" /> },
|
||||
{ path: '/auto-test', label: '测试助手', icon: <Zap className="h-5 w-5" /> },
|
||||
{
|
||||
path: '#components',
|
||||
label: '组件库',
|
||||
icon: <Box className="h-5 w-5" />,
|
||||
children: [
|
||||
{ path: '/llms', label: '大模型库', icon: <BrainCircuit className="h-5 w-5" /> },
|
||||
{ path: '/asr', label: '语音识别', icon: <AudioLines className="h-5 w-5" /> },
|
||||
{ path: '/voices', label: '声音库', icon: <Mic2 className="h-5 w-5" /> },
|
||||
{ path: '/knowledge', label: '知识库', icon: <Book className="h-5 w-5" /> },
|
||||
{ path: '/tools', label: '工具库', icon: <Wrench className="h-5 w-5" /> },
|
||||
]
|
||||
},
|
||||
{ path: '/profile', label: '个人中心', icon: <User className="h-5 w-5" /> },
|
||||
];
|
||||
|
||||
@@ -60,11 +162,10 @@ const AppLayout: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
{navItems.map(item => (
|
||||
<SidebarItem
|
||||
key={item.path}
|
||||
to={item.path}
|
||||
icon={item.icon}
|
||||
label={item.label}
|
||||
item={item}
|
||||
isCollapsed={isCollapsed}
|
||||
active={item.path === '/' ? location.pathname === '/' : location.pathname.startsWith(item.path)}
|
||||
isActive={item.path === '/' ? location.pathname === '/' : location.pathname.startsWith(item.path)}
|
||||
onExpand={() => setIsCollapsed(false)}
|
||||
/>
|
||||
))}
|
||||
</nav>
|
||||
@@ -110,7 +211,10 @@ const App: React.FC = () => {
|
||||
<Route path="/" element={<DashboardPage />} />
|
||||
<Route path="/assistants" element={<AssistantsPage />} />
|
||||
<Route path="/voices" element={<VoiceLibraryPage />} />
|
||||
<Route path="/llms" element={<LLMLibraryPage />} />
|
||||
<Route path="/asr" element={<ASRLibraryPage />} />
|
||||
<Route path="/knowledge" element={<KnowledgeBasePage />} />
|
||||
<Route path="/tools" element={<ToolLibraryPage />} />
|
||||
<Route path="/history" element={<HistoryPage />} />
|
||||
<Route path="/workflows" element={<WorkflowsPage />} />
|
||||
<Route path="/workflows/new" element={<WorkflowEditorPage />} />
|
||||
@@ -123,4 +227,4 @@ const App: React.FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
export default App;
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "ai视频助手",
|
||||
"name": "ai视频助手2",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
|
||||
235
web/pages/ASRLibrary.tsx
Normal file
235
web/pages/ASRLibrary.tsx
Normal file
@@ -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<ASRModel[]>(mockASRModels);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [vendorFilter, setVendorFilter] = useState<string>('all');
|
||||
const [langFilter, setLangFilter] = useState<string>('all');
|
||||
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
|
||||
|
||||
// Form State
|
||||
const [newModel, setNewModel] = useState<Partial<ASRModel>>({
|
||||
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 (
|
||||
<div className="space-y-6 animate-in fade-in py-4 pb-10">
|
||||
<div className="flex items-center justify-between">
|
||||
<h1 className="text-2xl font-bold tracking-tight text-white">语音识别</h1>
|
||||
<Button onClick={() => setIsAddModalOpen(true)} className="shadow-[0_0_15px_rgba(6,182,212,0.4)]">
|
||||
<Plus className="mr-2 h-4 w-4" /> 添加模型
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 bg-card/50 p-4 rounded-lg border border-white/5 shadow-sm">
|
||||
<div className="relative col-span-1 md:col-span-2">
|
||||
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="搜索模型名称..."
|
||||
className="pl-9 border-0 bg-white/5"
|
||||
value={searchTerm}
|
||||
onChange={e => setSearchTerm(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Filter className="h-4 w-4 text-muted-foreground" />
|
||||
<select
|
||||
className="flex h-9 w-full rounded-md border-0 bg-white/5 px-3 py-1 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary/50 [&>option]:bg-card text-foreground"
|
||||
value={vendorFilter}
|
||||
onChange={(e) => setVendorFilter(e.target.value)}
|
||||
>
|
||||
<option value="all">所有接口类型</option>
|
||||
<option value="OpenAI Compatible">OpenAI Compatible</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<select
|
||||
className="flex h-9 w-full rounded-md border-0 bg-white/5 px-3 py-1 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary/50 [&>option]:bg-card text-foreground"
|
||||
value={langFilter}
|
||||
onChange={(e) => setLangFilter(e.target.value)}
|
||||
>
|
||||
<option value="all">所有语言</option>
|
||||
<option value="zh">中文 (Chinese)</option>
|
||||
<option value="en">英文 (English)</option>
|
||||
<option value="Multi-lingual">多语言 (Multi-lingual)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-md border border-white/5 bg-card/40 backdrop-blur-md overflow-hidden">
|
||||
<table className="w-full text-sm">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>模型名称</TableHead>
|
||||
<TableHead>接口类型</TableHead>
|
||||
<TableHead>语言</TableHead>
|
||||
<TableHead>Base URL</TableHead>
|
||||
<TableHead>API Key</TableHead>
|
||||
<TableHead className="text-right">操作</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<tbody>
|
||||
{filteredModels.map(model => (
|
||||
<TableRow key={model.id}>
|
||||
<TableCell className="font-medium text-white flex items-center">
|
||||
<Ear className="w-4 h-4 mr-2 text-primary" />
|
||||
{model.name}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant="outline">{model.vendor}</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant="default" className="bg-purple-500/10 text-purple-400 border-purple-500/20">
|
||||
{model.language}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="font-mono text-xs text-muted-foreground">
|
||||
{model.baseUrl}
|
||||
</TableCell>
|
||||
<TableCell className="font-mono text-xs text-muted-foreground">
|
||||
{maskApiKey(model.apiKey)}
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => handleDeleteModel(model.id)}
|
||||
className="text-muted-foreground hover:text-destructive transition-colors"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
{filteredModels.length === 0 && (
|
||||
<TableRow>
|
||||
<TableCell colSpan={6} className="text-center py-8 text-muted-foreground">暂无语音识别模型</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<Dialog
|
||||
isOpen={isAddModalOpen}
|
||||
onClose={() => setIsAddModalOpen(false)}
|
||||
title="添加语音识别模型"
|
||||
footer={
|
||||
<>
|
||||
<Button variant="ghost" onClick={() => setIsAddModalOpen(false)}>取消</Button>
|
||||
<Button onClick={handleAddModel}>确认添加</Button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-[10px] font-black text-muted-foreground uppercase tracking-widest block">接口类型 (Interface Type)</label>
|
||||
<select
|
||||
className="flex h-10 w-full rounded-md border border-white/10 bg-white/5 px-3 py-1 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary/50 text-foreground appearance-none cursor-pointer [&>option]:bg-card"
|
||||
value={newModel.vendor}
|
||||
onChange={e => setNewModel({...newModel, vendor: e.target.value as any})}
|
||||
>
|
||||
<option value="OpenAI Compatible">OpenAI Compatible</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-[10px] font-black text-muted-foreground uppercase tracking-widest block">支持语言 (Language)</label>
|
||||
<div className="flex bg-white/5 p-1 rounded-lg border border-white/10">
|
||||
{(['zh', 'en', 'Multi-lingual'] as const).map(l => (
|
||||
<button
|
||||
key={l}
|
||||
onClick={() => setNewModel({...newModel, language: l})}
|
||||
className={`flex-1 flex items-center justify-center py-1.5 text-xs font-bold rounded-md transition-all ${newModel.language === l ? 'bg-primary text-primary-foreground shadow-lg' : 'text-muted-foreground hover:text-foreground'}`}
|
||||
>
|
||||
{l === 'zh' && <span className="mr-1">🇨🇳</span>}
|
||||
{l === 'en' && <span className="mr-1">🇺🇸</span>}
|
||||
{l === 'Multi-lingual' && <Globe className="w-3 h-3 mr-1.5" />}
|
||||
{l === 'zh' ? '中文' : l === 'en' ? '英文' : '多语言'}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-[10px] font-black text-muted-foreground uppercase tracking-widest block">模型名称 (Model Name)</label>
|
||||
<Input
|
||||
value={newModel.name}
|
||||
onChange={e => setNewModel({...newModel, name: e.target.value})}
|
||||
placeholder="例如: whisper-1, funasr"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-[10px] font-black text-muted-foreground uppercase tracking-widest block flex items-center">
|
||||
<Server className="w-3 h-3 mr-1.5" /> Base URL
|
||||
</label>
|
||||
<Input
|
||||
value={newModel.baseUrl}
|
||||
onChange={e => setNewModel({...newModel, baseUrl: e.target.value})}
|
||||
placeholder="https://api.openai.com/v1"
|
||||
className="font-mono text-xs"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-[10px] font-black text-muted-foreground uppercase tracking-widest block flex items-center">
|
||||
<Key className="w-3 h-3 mr-1.5" /> API Key
|
||||
</label>
|
||||
<Input
|
||||
type="password"
|
||||
value={newModel.apiKey}
|
||||
onChange={e => setNewModel({...newModel, apiKey: e.target.value})}
|
||||
placeholder="sk-..."
|
||||
className="font-mono text-xs"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -140,8 +140,8 @@ export const DashboardPage: React.FC = () => {
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* 6. Platform Feature Intro - Moved to Bottom, Full Width */}
|
||||
<div className="w-full bg-white/[0.02] border border-white/5 rounded-2xl p-6 animate-in slide-in-from-bottom-4 duration-700 shadow-2xl relative overflow-hidden group pb-10 mb-10">
|
||||
{/* 6. Platform Feature Intro - Updated Background */}
|
||||
<div className="w-full bg-card/30 backdrop-blur-sm border border-white/5 rounded-2xl p-6 animate-in slide-in-from-bottom-4 duration-700 shadow-2xl relative overflow-hidden group pb-10 mb-10">
|
||||
<div className="absolute top-0 right-0 w-64 h-64 bg-primary/5 blur-[100px] -mr-32 -mt-32 rounded-full pointer-events-none group-hover:bg-primary/10 transition-colors"></div>
|
||||
|
||||
<div className="relative z-10">
|
||||
|
||||
@@ -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<CallLog | null>(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 = () => {
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 bg-card/50 p-4 rounded-lg border border-white/5 shadow-sm">
|
||||
<div className="grid grid-cols-1 md:grid-cols-5 gap-4 bg-card/50 p-4 rounded-lg border border-white/5 shadow-sm">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
@@ -69,6 +75,18 @@ export const HistoryPage: React.FC = () => {
|
||||
<option value="external">外部测试 (External)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<select
|
||||
className="flex h-9 w-full rounded-md border-0 bg-white/5 px-3 py-1 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary/50 [&>option]:bg-card"
|
||||
value={typeFilter}
|
||||
onChange={(e) => setTypeFilter(e.target.value as any)}
|
||||
>
|
||||
<option value="all">所有类型</option>
|
||||
<option value="text">文本 (Text)</option>
|
||||
<option value="audio">语音 (Audio)</option>
|
||||
<option value="video">视频 (Video)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<select
|
||||
className="flex h-9 w-full rounded-md border-0 bg-white/5 px-3 py-1 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary/50 [&>option]:bg-card"
|
||||
@@ -92,6 +110,7 @@ export const HistoryPage: React.FC = () => {
|
||||
<TableRow>
|
||||
<TableHead>编号</TableHead>
|
||||
<TableHead>代理小助手</TableHead>
|
||||
<TableHead>类型</TableHead>
|
||||
<TableHead>来源</TableHead>
|
||||
<TableHead>接听状态</TableHead>
|
||||
<TableHead>通话接通时间</TableHead>
|
||||
@@ -100,9 +119,19 @@ export const HistoryPage: React.FC = () => {
|
||||
</TableHeader>
|
||||
<tbody>
|
||||
{filteredLogs.map(log => (
|
||||
<TableRow key={log.id}>
|
||||
<TableCell className="font-mono text-xs text-muted-foreground">#{log.id}</TableCell>
|
||||
<TableCell className="font-medium text-white">{log.agentName}</TableCell>
|
||||
<TableRow key={log.id} className="cursor-pointer hover:bg-white/5 group" onClick={() => setSelectedLog(log)}>
|
||||
<TableCell className="font-mono text-xs text-muted-foreground group-hover:text-primary transition-colors">#{log.id}</TableCell>
|
||||
<TableCell className="font-medium text-white group-hover:text-primary transition-colors flex items-center gap-2">
|
||||
{log.agentName}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-1.5">
|
||||
{log.type === 'text' && <MessageSquare className="w-3.5 h-3.5 text-blue-400" />}
|
||||
{log.type === 'audio' && <Mic className="w-3.5 h-3.5 text-orange-400" />}
|
||||
{log.type === 'video' && <Video className="w-3.5 h-3.5 text-green-400" />}
|
||||
<span className="capitalize text-xs">{log.type}</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant="outline">{log.source === 'debug' ? '调试' : '外部'}</Badge>
|
||||
</TableCell>
|
||||
@@ -117,12 +146,101 @@ export const HistoryPage: React.FC = () => {
|
||||
))}
|
||||
{filteredLogs.length === 0 && (
|
||||
<TableRow>
|
||||
<TableCell colSpan={6} className="text-center py-6 text-muted-foreground">暂无记录</TableCell>
|
||||
<TableCell colSpan={7} className="text-center py-6 text-muted-foreground">暂无记录</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{selectedLog && (
|
||||
<Drawer
|
||||
isOpen={!!selectedLog}
|
||||
onClose={() => setSelectedLog(null)}
|
||||
title="历史记录详情"
|
||||
>
|
||||
<div className="flex flex-col h-full overflow-hidden">
|
||||
<div className="shrink-0 mb-4 p-4 bg-white/5 rounded-xl border border-white/10 space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="font-bold text-lg text-white">{selectedLog.agentName}</h3>
|
||||
<Badge variant="outline" className="uppercase">{selectedLog.type} Record</Badge>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4 text-xs text-muted-foreground">
|
||||
<div>ID: <span className="font-mono text-white/70">#{selectedLog.id}</span></div>
|
||||
<div>时间: <span className="text-white/70">{selectedLog.startTime}</span></div>
|
||||
<div>时长: <span className="text-white/70">{selectedLog.duration}</span></div>
|
||||
<div>状态: <span className={selectedLog.status === 'connected' ? 'text-green-400' : 'text-yellow-400'}>{selectedLog.status}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto space-y-4 pr-1 custom-scrollbar pb-6">
|
||||
{(selectedLog.details && selectedLog.details.length > 0) ? (
|
||||
selectedLog.details.map((detail, index) => (
|
||||
<div key={index} className={`flex flex-col gap-1 ${detail.role === 'user' ? 'items-end' : 'items-start'}`}>
|
||||
<div className={`max-w-[85%] rounded-2xl p-4 shadow-sm border ${
|
||||
detail.role === 'user'
|
||||
? 'bg-primary/10 border-primary/20 rounded-tr-none'
|
||||
: 'bg-card border-white/10 rounded-tl-none'
|
||||
}`}>
|
||||
<div className="flex items-center gap-2 mb-2 opacity-70">
|
||||
<span className="text-[10px] font-bold uppercase tracking-wider text-primary">
|
||||
{detail.role === 'user' ? 'User' : 'AI Assistant'}
|
||||
</span>
|
||||
<span className="text-[10px] text-muted-foreground">{detail.timestamp}</span>
|
||||
</div>
|
||||
|
||||
{/* Video Frames */}
|
||||
{selectedLog.type === 'video' && detail.role === 'user' && detail.imageUrls && detail.imageUrls.length > 0 && (
|
||||
<div className="flex gap-2 overflow-x-auto mb-3 pb-2">
|
||||
{detail.imageUrls.map((url, i) => (
|
||||
<div key={i} className="relative h-20 w-32 rounded-lg overflow-hidden border border-white/10 bg-black/50 shrink-0 group">
|
||||
<img src={url} alt={`Frame ${i}`} className="h-full w-full object-cover" />
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<Eye className="w-5 h-5 text-white" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Content / Transcript */}
|
||||
<div className="text-sm leading-relaxed text-white/90">
|
||||
{selectedLog.type !== 'text' && (
|
||||
<div className="flex items-center gap-2 mb-1.5">
|
||||
<div className={`p-1.5 rounded-full ${detail.role === 'user' ? 'bg-primary/20' : 'bg-white/10'}`}>
|
||||
{selectedLog.type === 'audio' ? <Mic size={12} /> : <Video size={12} />}
|
||||
</div>
|
||||
<span className="text-[10px] uppercase font-mono text-muted-foreground">Transcript</span>
|
||||
</div>
|
||||
)}
|
||||
{detail.content}
|
||||
</div>
|
||||
|
||||
{/* Audio Player Placeholder for Audio/Video types */}
|
||||
{selectedLog.type !== 'text' && (
|
||||
<div className="mt-3 flex items-center gap-2 p-2 rounded-lg bg-black/20 border border-white/5">
|
||||
<button className="w-6 h-6 rounded-full bg-white/10 hover:bg-primary hover:text-white flex items-center justify-center transition-colors">
|
||||
<Play size={10} className="ml-0.5" />
|
||||
</button>
|
||||
<div className="h-1 flex-1 bg-white/10 rounded-full overflow-hidden">
|
||||
<div className="h-full w-1/3 bg-primary/50"></div>
|
||||
</div>
|
||||
<span className="text-[10px] font-mono text-muted-foreground">00:05</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="h-full flex flex-col items-center justify-center text-muted-foreground opacity-50 space-y-2">
|
||||
<MessageSquare className="w-10 h-10" />
|
||||
<p className="text-sm">暂无对话详情数据</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Drawer>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
248
web/pages/LLMLibrary.tsx
Normal file
248
web/pages/LLMLibrary.tsx
Normal file
@@ -0,0 +1,248 @@
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { Search, Filter, Plus, BrainCircuit, Trash2, Key, Settings2, Server, Thermometer } from 'lucide-react';
|
||||
import { Button, Input, TableHeader, TableRow, TableHead, TableCell, Dialog, Badge } from '../components/UI';
|
||||
import { mockLLMModels } from '../services/mockData';
|
||||
import { LLMModel } from '../types';
|
||||
|
||||
export const LLMLibraryPage: React.FC = () => {
|
||||
const [models, setModels] = useState<LLMModel[]>(mockLLMModels);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [vendorFilter, setVendorFilter] = useState<string>('all');
|
||||
const [typeFilter, setTypeFilter] = useState<string>('all');
|
||||
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
|
||||
|
||||
// Form State
|
||||
const [newModel, setNewModel] = useState<Partial<LLMModel>>({
|
||||
vendor: 'OpenAI Compatible',
|
||||
type: 'text',
|
||||
temperature: 0.7
|
||||
});
|
||||
|
||||
const filteredModels = models.filter(m => {
|
||||
const matchesSearch = m.name.toLowerCase().includes(searchTerm.toLowerCase());
|
||||
const matchesVendor = vendorFilter === 'all' || m.vendor === vendorFilter;
|
||||
const matchesType = typeFilter === 'all' || m.type === typeFilter;
|
||||
return matchesSearch && matchesVendor && matchesType;
|
||||
});
|
||||
|
||||
const handleAddModel = () => {
|
||||
if (!newModel.name || !newModel.baseUrl || !newModel.apiKey) {
|
||||
alert("请填写完整信息");
|
||||
return;
|
||||
}
|
||||
|
||||
const model: LLMModel = {
|
||||
id: `m_${Date.now()}`,
|
||||
name: newModel.name,
|
||||
vendor: newModel.vendor as string,
|
||||
type: newModel.type as 'text' | 'embedding' | 'rerank',
|
||||
baseUrl: newModel.baseUrl,
|
||||
apiKey: newModel.apiKey,
|
||||
temperature: newModel.type === 'text' ? newModel.temperature : undefined
|
||||
};
|
||||
|
||||
setModels([model, ...models]);
|
||||
setIsAddModalOpen(false);
|
||||
setNewModel({ vendor: 'OpenAI Compatible', type: 'text', temperature: 0.7, name: '', baseUrl: '', apiKey: '' });
|
||||
};
|
||||
|
||||
const handleDeleteModel = (id: string) => {
|
||||
if (confirm('确认删除该模型配置吗?')) {
|
||||
setModels(prev => prev.filter(m => m.id !== id));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6 animate-in fade-in py-4 pb-10">
|
||||
<div className="flex items-center justify-between">
|
||||
<h1 className="text-2xl font-bold tracking-tight text-white">大模型库</h1>
|
||||
<Button onClick={() => setIsAddModalOpen(true)} className="shadow-[0_0_15px_rgba(6,182,212,0.4)]">
|
||||
<Plus className="mr-2 h-4 w-4" /> 添加模型
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 bg-card/50 p-4 rounded-lg border border-white/5 shadow-sm">
|
||||
<div className="relative col-span-1 md:col-span-2">
|
||||
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="搜索模型名称..."
|
||||
className="pl-9 border-0 bg-white/5"
|
||||
value={searchTerm}
|
||||
onChange={e => setSearchTerm(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Filter className="h-4 w-4 text-muted-foreground" />
|
||||
<select
|
||||
className="flex h-9 w-full rounded-md border-0 bg-white/5 px-3 py-1 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary/50 [&>option]:bg-card text-foreground"
|
||||
value={vendorFilter}
|
||||
onChange={(e) => setVendorFilter(e.target.value)}
|
||||
>
|
||||
<option value="all">所有厂商</option>
|
||||
<option value="OpenAI Compatible">OpenAI Compatible</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<select
|
||||
className="flex h-9 w-full rounded-md border-0 bg-white/5 px-3 py-1 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary/50 [&>option]:bg-card text-foreground"
|
||||
value={typeFilter}
|
||||
onChange={(e) => setTypeFilter(e.target.value)}
|
||||
>
|
||||
<option value="all">所有类型</option>
|
||||
<option value="text">文本 (Text)</option>
|
||||
<option value="embedding">嵌入 (Embedding)</option>
|
||||
<option value="rerank">重排 (Rerank)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-md border border-white/5 bg-card/40 backdrop-blur-md overflow-hidden">
|
||||
<table className="w-full text-sm">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>模型名称</TableHead>
|
||||
<TableHead>厂商</TableHead>
|
||||
<TableHead>类型</TableHead>
|
||||
<TableHead>Base URL</TableHead>
|
||||
<TableHead className="text-right">操作</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<tbody>
|
||||
{filteredModels.map(model => (
|
||||
<TableRow key={model.id}>
|
||||
<TableCell className="font-medium text-white flex items-center">
|
||||
<BrainCircuit className="w-4 h-4 mr-2 text-primary" />
|
||||
{model.name}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant="outline">{model.vendor}</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant={model.type === 'text' ? 'default' : 'outline'} className={model.type !== 'text' ? 'text-blue-400 border-blue-400/20 bg-blue-400/5' : ''}>
|
||||
{model.type.toUpperCase()}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="font-mono text-xs text-muted-foreground">
|
||||
{model.baseUrl}
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => handleDeleteModel(model.id)}
|
||||
className="text-muted-foreground hover:text-destructive transition-colors"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
{filteredModels.length === 0 && (
|
||||
<TableRow>
|
||||
<TableCell colSpan={5} className="text-center py-8 text-muted-foreground">暂无模型数据</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<Dialog
|
||||
isOpen={isAddModalOpen}
|
||||
onClose={() => setIsAddModalOpen(false)}
|
||||
title="添加大模型"
|
||||
footer={
|
||||
<>
|
||||
<Button variant="ghost" onClick={() => setIsAddModalOpen(false)}>取消</Button>
|
||||
<Button onClick={handleAddModel}>确认添加</Button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-[10px] font-black text-muted-foreground uppercase tracking-widest block">厂商 (Vendor)</label>
|
||||
<select
|
||||
className="flex h-10 w-full rounded-md border border-white/10 bg-white/5 px-3 py-1 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary/50 text-foreground appearance-none cursor-pointer [&>option]:bg-card"
|
||||
value={newModel.vendor}
|
||||
onChange={e => setNewModel({...newModel, vendor: e.target.value})}
|
||||
>
|
||||
<option value="OpenAI Compatible">OpenAI Compatible</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-[10px] font-black text-muted-foreground uppercase tracking-widest block">模型类型 (Type)</label>
|
||||
<div className="flex bg-white/5 p-1 rounded-lg border border-white/10">
|
||||
{(['text', 'embedding', 'rerank'] as const).map(t => (
|
||||
<button
|
||||
key={t}
|
||||
onClick={() => setNewModel({...newModel, type: t})}
|
||||
className={`flex-1 flex items-center justify-center py-1.5 text-xs font-bold rounded-md transition-all ${newModel.type === t ? 'bg-primary text-primary-foreground shadow-lg' : 'text-muted-foreground hover:text-foreground'}`}
|
||||
>
|
||||
{t === 'text' && <Settings2 className="w-3 h-3 mr-1.5" />}
|
||||
{t === 'embedding' && <BrainCircuit className="w-3 h-3 mr-1.5" />}
|
||||
{t === 'rerank' && <Filter className="w-3 h-3 mr-1.5" />}
|
||||
{t === 'text' ? '文本' : t === 'embedding' ? '嵌入' : '重排'}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-[10px] font-black text-muted-foreground uppercase tracking-widest block">模型名称 (Model Name)</label>
|
||||
<Input
|
||||
value={newModel.name}
|
||||
onChange={e => setNewModel({...newModel, name: e.target.value})}
|
||||
placeholder="例如: gpt-4o, deepseek-chat"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-[10px] font-black text-muted-foreground uppercase tracking-widest block flex items-center">
|
||||
<Server className="w-3 h-3 mr-1.5" /> Base URL
|
||||
</label>
|
||||
<Input
|
||||
value={newModel.baseUrl}
|
||||
onChange={e => setNewModel({...newModel, baseUrl: e.target.value})}
|
||||
placeholder="https://api.openai.com/v1"
|
||||
className="font-mono text-xs"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-[10px] font-black text-muted-foreground uppercase tracking-widest block flex items-center">
|
||||
<Key className="w-3 h-3 mr-1.5" /> API Key
|
||||
</label>
|
||||
<Input
|
||||
type="password"
|
||||
value={newModel.apiKey}
|
||||
onChange={e => setNewModel({...newModel, apiKey: e.target.value})}
|
||||
placeholder="sk-..."
|
||||
className="font-mono text-xs"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{newModel.type === 'text' && (
|
||||
<div className="space-y-3 pt-2">
|
||||
<div className="flex justify-between items-center mb-1">
|
||||
<label className="text-[10px] font-black text-muted-foreground uppercase tracking-widest block flex items-center">
|
||||
<Thermometer className="w-3 h-3 mr-1.5" /> 温度 (Temperature)
|
||||
</label>
|
||||
<span className="text-[10px] font-mono text-primary bg-primary/10 px-1.5 py-0.5 rounded">{newModel.temperature}</span>
|
||||
</div>
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="2"
|
||||
step="0.1"
|
||||
value={newModel.temperature}
|
||||
onChange={(e) => setNewModel({...newModel, temperature: parseFloat(e.target.value)})}
|
||||
className="w-full h-1.5 bg-secondary rounded-lg appearance-none cursor-pointer accent-primary"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
191
web/pages/ToolLibrary.tsx
Normal file
191
web/pages/ToolLibrary.tsx
Normal file
@@ -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<string, React.ReactNode> = {
|
||||
Camera: <Camera className="w-5 h-5" />,
|
||||
CameraOff: <CameraOff className="w-5 h-5" />,
|
||||
Image: <Image className="w-5 h-5" />,
|
||||
Images: <Images className="w-5 h-5" />,
|
||||
CloudSun: <CloudSun className="w-5 h-5" />,
|
||||
Calendar: <Calendar className="w-5 h-5" />,
|
||||
TrendingUp: <TrendingUp className="w-5 h-5" />,
|
||||
Coins: <Coins className="w-5 h-5" />,
|
||||
Terminal: <Terminal className="w-5 h-5" />,
|
||||
Globe: <Globe className="w-5 h-5" />,
|
||||
Wrench: <Wrench className="w-5 h-5" />,
|
||||
};
|
||||
|
||||
export const ToolLibraryPage: React.FC = () => {
|
||||
const [tools, setTools] = useState<Tool[]>(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 (
|
||||
<div className="space-y-6 animate-in fade-in py-4 pb-10">
|
||||
<div className="flex items-center justify-between">
|
||||
<h1 className="text-2xl font-bold tracking-tight text-white">工具库</h1>
|
||||
<Button onClick={() => setIsAddModalOpen(true)} className="shadow-[0_0_15px_rgba(6,182,212,0.4)]">
|
||||
<Plus className="mr-2 h-4 w-4" /> 添加工具
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 bg-card/50 p-4 rounded-lg border border-white/5 shadow-sm">
|
||||
<div className="relative col-span-1 md:col-span-2">
|
||||
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="搜索工具名称..."
|
||||
className="pl-9 border-0 bg-white/5"
|
||||
value={searchTerm}
|
||||
onChange={e => setSearchTerm(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Filter className="h-4 w-4 text-muted-foreground" />
|
||||
<select
|
||||
className="flex h-9 w-full rounded-md border-0 bg-white/5 px-3 py-1 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary/50 [&>option]:bg-card text-foreground"
|
||||
value={categoryFilter}
|
||||
onChange={(e) => setCategoryFilter(e.target.value as any)}
|
||||
>
|
||||
<option value="all">所有类型</option>
|
||||
<option value="system">系统指令 (System)</option>
|
||||
<option value="query">信息查询 (Query)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{filteredTools.map(tool => (
|
||||
<div
|
||||
key={tool.id}
|
||||
className={`p-5 rounded-xl border transition-all relative group flex items-start space-x-4 bg-card/30 border-white/5 hover:bg-white/5 hover:border-white/10 hover:shadow-lg`}
|
||||
>
|
||||
<div className={`p-3 rounded-lg shrink-0 transition-colors ${tool.category === 'system' ? 'bg-primary/10 text-primary' : 'bg-blue-500/10 text-blue-400'}`}>
|
||||
{iconMap[tool.icon] || <Box className="w-5 h-5" />}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span className="text-base font-bold text-white">{tool.name}</span>
|
||||
{tool.isCustom && <Badge variant="outline" className="text-[9px] h-4 px-1">CUSTOM</Badge>}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Badge variant="outline" className={`text-[10px] border-0 px-0 ${tool.category === 'system' ? 'text-primary' : 'text-blue-400'}`}>
|
||||
{tool.category === 'system' ? 'SYSTEM' : 'QUERY'}
|
||||
</Badge>
|
||||
<span className="text-[10px] text-muted-foreground font-mono opacity-50">ID: {tool.id}</span>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground line-clamp-2 leading-relaxed opacity-80">{tool.description}</p>
|
||||
</div>
|
||||
|
||||
{tool.isCustom && (
|
||||
<div className="absolute top-3 right-3 flex space-x-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<button
|
||||
onClick={(e) => handleDeleteTool(e, tool.id)}
|
||||
className="p-1.5 rounded-md hover:bg-destructive/20 text-muted-foreground hover:text-destructive transition-colors"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{filteredTools.length === 0 && (
|
||||
<div className="col-span-full py-12 flex flex-col items-center justify-center text-muted-foreground opacity-50">
|
||||
<Wrench className="w-12 h-12 mb-4 stroke-1" />
|
||||
<p>未找到相关工具</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Dialog
|
||||
isOpen={isAddModalOpen}
|
||||
onClose={() => setIsAddModalOpen(false)}
|
||||
title="添加自定义工具"
|
||||
footer={
|
||||
<>
|
||||
<Button variant="ghost" onClick={() => setIsAddModalOpen(false)}>取消</Button>
|
||||
<Button onClick={handleAddTool}>确认添加</Button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-[10px] font-black text-muted-foreground uppercase tracking-widest block">工具类型</label>
|
||||
<div className="flex bg-white/5 p-1 rounded-lg border border-white/10">
|
||||
<button
|
||||
onClick={() => setNewToolCategory('system')}
|
||||
className={`flex-1 flex items-center justify-center py-2 text-xs font-bold rounded-md transition-all ${newToolCategory === 'system' ? 'bg-primary text-primary-foreground shadow-lg' : 'text-muted-foreground hover:text-foreground'}`}
|
||||
>
|
||||
<Terminal className="w-3.5 h-3.5 mr-2" /> 系统指令
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setNewToolCategory('query')}
|
||||
className={`flex-1 flex items-center justify-center py-2 text-xs font-bold rounded-md transition-all ${newToolCategory === 'query' ? 'bg-blue-500 text-white shadow-lg' : 'text-muted-foreground hover:text-foreground'}`}
|
||||
>
|
||||
<Globe className="w-3.5 h-3.5 mr-2" /> 信息查询
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-[10px] font-black text-muted-foreground uppercase tracking-widest block">工具名称</label>
|
||||
<Input
|
||||
value={newToolName}
|
||||
onChange={e => setNewToolName(e.target.value)}
|
||||
placeholder="例如: 智能家居控制"
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-[10px] font-black text-muted-foreground uppercase tracking-widest block">工具描述 (给 AI 的说明)</label>
|
||||
<textarea
|
||||
className="flex min-h-[100px] w-full rounded-md border border-white/10 bg-white/5 px-3 py-2 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary/50 text-white"
|
||||
value={newToolDesc}
|
||||
onChange={e => setNewToolDesc(e.target.value)}
|
||||
placeholder="描述该工具的功能,以及 AI 应该在什么情况下调用它..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
|
||||
import { Assistant, CallLog, KnowledgeBase, Voice, Workflow, AutoTestAssistant, TestType, TestMethod } from '../types';
|
||||
import { Assistant, CallLog, KnowledgeBase, Voice, Workflow, AutoTestAssistant, TestType, TestMethod, Tool, LLMModel, ASRModel } from '../types';
|
||||
|
||||
export const mockVoices: Voice[] = [
|
||||
{ id: 'v1', name: 'Xiaoyun', vendor: 'Ali', gender: 'Female', language: 'zh', description: 'Gentle and professional.' },
|
||||
@@ -112,6 +112,13 @@ export const mockCallLogs: CallLog[] = [
|
||||
startTime: '2023-11-20 10:30:00',
|
||||
duration: '5m 23s',
|
||||
agentName: 'Customer Support Bot',
|
||||
type: 'video',
|
||||
details: [
|
||||
{ role: 'user', content: 'Can you see this product?', imageUrls: ['https://images.unsplash.com/photo-1523275335684-37898b6baf30?w=150&h=150&fit=crop'], timestamp: '10:30:05' },
|
||||
{ role: 'assistant', content: 'Yes, I can see the white watch. It looks like a minimalist design.', timestamp: '10:30:08' },
|
||||
{ role: 'user', content: 'How much is it?', imageUrls: [], timestamp: '10:30:15' },
|
||||
{ role: 'assistant', content: 'Based on the database, this model retails for $199.', timestamp: '10:30:18' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'c2',
|
||||
@@ -120,6 +127,11 @@ export const mockCallLogs: CallLog[] = [
|
||||
startTime: '2023-11-20 11:15:00',
|
||||
duration: '1m 10s',
|
||||
agentName: 'Sales Agent',
|
||||
type: 'audio',
|
||||
details: [
|
||||
{ role: 'user', content: 'I am interested in the premium plan.', timestamp: '11:15:02' },
|
||||
{ role: 'assistant', content: 'That is a great choice. The premium plan includes...', timestamp: '11:15:05' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'c3',
|
||||
@@ -128,7 +140,24 @@ export const mockCallLogs: CallLog[] = [
|
||||
startTime: '2023-11-20 12:00:00',
|
||||
duration: '0s',
|
||||
agentName: 'Customer Support Bot',
|
||||
type: 'text',
|
||||
details: []
|
||||
},
|
||||
{
|
||||
id: 'c4',
|
||||
source: 'debug',
|
||||
status: 'connected',
|
||||
startTime: '2023-11-21 09:30:00',
|
||||
duration: '45s',
|
||||
agentName: 'Refund Bot',
|
||||
type: 'text',
|
||||
details: [
|
||||
{ role: 'user', content: '我想申请退款', timestamp: '09:30:01' },
|
||||
{ role: 'assistant', content: '好的,请提供您的订单号。', timestamp: '09:30:02' },
|
||||
{ role: 'user', content: 'ORDER-2024-888', timestamp: '09:30:10' },
|
||||
{ role: 'assistant', content: '收到,正在为您处理...', timestamp: '09:30:12' }
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
export const mockAutoTestAssistants: AutoTestAssistant[] = [
|
||||
@@ -154,6 +183,29 @@ export const mockAutoTestAssistants: AutoTestAssistant[] = [
|
||||
}
|
||||
];
|
||||
|
||||
export const mockTools: Tool[] = [
|
||||
{ id: 'cam_open', name: '打开相机', description: '允许 AI 开启摄像头流', category: 'system', icon: 'Camera' },
|
||||
{ id: 'cam_close', name: '关闭相机', description: '允许 AI 停止摄像头流', category: 'system', icon: 'CameraOff' },
|
||||
{ id: 'take_photo', name: '拍照', description: 'AI 触发单张拍摄', category: 'system', icon: 'Image' },
|
||||
{ id: 'burst_3', name: '连拍三张', description: 'AI 触发快速连拍', category: 'system', icon: 'Images' },
|
||||
{ id: 'q_weather', name: '天气查询', description: '查询实时及未来天气', category: 'query', icon: 'CloudSun' },
|
||||
{ id: 'q_calendar', name: '日历查询', description: '查询日程及节假日信息', category: 'query', icon: 'Calendar' },
|
||||
{ id: 'q_stock', name: '股价查询', description: '查询股票实时行情', category: 'query', icon: 'TrendingUp' },
|
||||
{ id: 'q_exchange', name: '汇率查询', description: '查询多国货币汇率', category: 'query', icon: 'Coins' },
|
||||
{ id: 'custom_1', name: '智能家居控制', description: '控制灯光、窗帘等设备', category: 'system', icon: 'Terminal', isCustom: true },
|
||||
];
|
||||
|
||||
export const mockLLMModels: LLMModel[] = [
|
||||
{ id: 'm1', name: 'GPT-4o', vendor: 'OpenAI Compatible', type: 'text', baseUrl: 'https://api.openai.com/v1', apiKey: 'sk-***', temperature: 0.7 },
|
||||
{ id: 'm2', name: 'DeepSeek-V3', vendor: 'OpenAI Compatible', type: 'text', baseUrl: 'https://api.deepseek.com', apiKey: 'sk-***', temperature: 0.5 },
|
||||
{ id: 'm3', name: 'text-embedding-3-small', vendor: 'OpenAI Compatible', type: 'embedding', baseUrl: 'https://api.openai.com/v1', apiKey: 'sk-***' },
|
||||
];
|
||||
|
||||
export const mockASRModels: ASRModel[] = [
|
||||
{ id: 'asr1', name: 'Whisper-1', vendor: 'OpenAI Compatible', language: 'Multi-lingual', baseUrl: 'https://api.openai.com/v1', apiKey: 'sk-***' },
|
||||
{ id: 'asr2', name: 'SenseVoiceSmall', vendor: 'OpenAI Compatible', language: 'zh', baseUrl: 'https://api.siliconflow.cn/v1', apiKey: 'sk-***' },
|
||||
];
|
||||
|
||||
export interface DashboardStats {
|
||||
totalCalls: number;
|
||||
answerRate: number;
|
||||
@@ -191,4 +243,4 @@ export const getDashboardStats = (timeRange: 'week' | 'month' | 'year', assistan
|
||||
humanTransferCount: transfers,
|
||||
trend
|
||||
};
|
||||
};
|
||||
};
|
||||
40
web/types.ts
40
web/types.ts
@@ -41,6 +41,16 @@ export interface KnowledgeDocument {
|
||||
uploadDate: string;
|
||||
}
|
||||
|
||||
export type InteractionType = 'text' | 'audio' | 'video';
|
||||
|
||||
export interface InteractionDetail {
|
||||
role: 'user' | 'assistant';
|
||||
content: string; // Text content or transcript
|
||||
audioUrl?: string; // Placeholder for audio url
|
||||
imageUrls?: string[]; // For video frames
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
export interface CallLog {
|
||||
id: string;
|
||||
source: 'debug' | 'external';
|
||||
@@ -48,6 +58,8 @@ export interface CallLog {
|
||||
startTime: string;
|
||||
duration: string;
|
||||
agentName: string;
|
||||
type: InteractionType;
|
||||
details?: InteractionDetail[];
|
||||
}
|
||||
|
||||
export interface Workflow {
|
||||
@@ -127,3 +139,31 @@ export interface AutoTestAssistant {
|
||||
intelligentPrompt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface Tool {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
category: 'system' | 'query';
|
||||
icon: string;
|
||||
isCustom?: boolean;
|
||||
}
|
||||
|
||||
export interface LLMModel {
|
||||
id: string;
|
||||
name: string;
|
||||
vendor: string;
|
||||
type: 'text' | 'embedding' | 'rerank';
|
||||
baseUrl: string;
|
||||
apiKey: string;
|
||||
temperature?: number;
|
||||
}
|
||||
|
||||
export interface ASRModel {
|
||||
id: string;
|
||||
name: string;
|
||||
vendor: 'OpenAI Compatible';
|
||||
language: string;
|
||||
baseUrl: string;
|
||||
apiKey: string;
|
||||
}
|
||||
Reference in New Issue
Block a user