Better UX
This commit is contained in:
18
App.tsx
18
App.tsx
@@ -1,11 +1,11 @@
|
||||
|
||||
import React, { useState } 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 } from 'lucide-react';
|
||||
import { Bot, Phone, Book, User, LayoutDashboard, Mic2, Video, GitBranch, Zap, PanelLeftClose, PanelLeftOpen, History as HistoryIcon } from 'lucide-react';
|
||||
|
||||
import { AssistantsPage } from './pages/Assistants';
|
||||
import { KnowledgeBasePage } from './pages/KnowledgeBase';
|
||||
import { CallLogsPage } from './pages/CallLogs';
|
||||
import { HistoryPage } from './pages/History';
|
||||
import { ProfilePage } from './pages/Profile';
|
||||
import { DashboardPage } from './pages/Dashboard';
|
||||
import { VoiceLibraryPage } from './pages/VoiceLibrary';
|
||||
@@ -32,10 +32,10 @@ const AppLayout: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
{ 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: '/call-logs', label: '视频通话记录', icon: <Phone 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: '/auto-test', label: '测试助手', icon: <Zap className="h-5 w-5" /> },
|
||||
{ path: '/profile', label: '个人中心', icon: <User className="h-5 w-5" /> },
|
||||
];
|
||||
|
||||
@@ -51,7 +51,7 @@ const AppLayout: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
</div>
|
||||
{!isCollapsed && (
|
||||
<span className="text-lg font-bold tracking-wide whitespace-nowrap bg-clip-text text-transparent bg-gradient-to-r from-white to-white/80 animate-in slide-in-from-left-2">
|
||||
AI VideoAssistant
|
||||
AI视频助手
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@@ -73,7 +73,7 @@ const AppLayout: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
<div className={`p-4 border-t border-border/40 flex items-center transition-all duration-300 ${isCollapsed ? 'justify-center' : 'justify-between'}`}>
|
||||
{!isCollapsed && (
|
||||
<span className="text-[10px] text-muted-foreground font-mono opacity-60 animate-in fade-in">
|
||||
SYSTEM v2.0
|
||||
SYSTEM v1.0
|
||||
</span>
|
||||
)}
|
||||
<button
|
||||
@@ -92,9 +92,9 @@ const AppLayout: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
<div className="h-8 w-8 bg-gradient-to-br from-cyan-400 to-blue-600 rounded-lg flex items-center justify-center shadow-lg">
|
||||
<Video className="h-5 w-5 text-white" />
|
||||
</div>
|
||||
<span className="font-bold text-lg whitespace-nowrap text-white">AI VideoAssistant</span>
|
||||
<span className="font-bold text-lg whitespace-nowrap text-white">AI视频助手</span>
|
||||
</header>
|
||||
<div className="flex-1 overflow-auto p-4 md:p-6 transition-all duration-300">
|
||||
<div className="flex-1 overflow-auto p-2 md:p-4 transition-all duration-300">
|
||||
{children}
|
||||
</div>
|
||||
</main>
|
||||
@@ -111,7 +111,7 @@ const App: React.FC = () => {
|
||||
<Route path="/assistants" element={<AssistantsPage />} />
|
||||
<Route path="/voices" element={<VoiceLibraryPage />} />
|
||||
<Route path="/knowledge" element={<KnowledgeBasePage />} />
|
||||
<Route path="/call-logs" element={<CallLogsPage />} />
|
||||
<Route path="/history" element={<HistoryPage />} />
|
||||
<Route path="/workflows" element={<WorkflowsPage />} />
|
||||
<Route path="/workflows/new" element={<WorkflowEditorPage />} />
|
||||
<Route path="/workflows/edit/:id" element={<WorkflowEditorPage />} />
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>AI VideoAssistant</title>
|
||||
<title>AI视频助手</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script>
|
||||
tailwind.config = {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
|
||||
{
|
||||
"name": "AI VideoAssistant",
|
||||
"name": "AI视频助手",
|
||||
"description": "A minimalist AI Assistant management system featuring agent configuration, knowledge base management, call logs, and a debugging suite.",
|
||||
"requestFramePermissions": [
|
||||
"microphone",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "ai-videoassistant",
|
||||
"name": "ai视频助手",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { Plus, Search, Play, Copy, Trash2, Edit2, Mic, MessageSquare, Save, Video, PhoneOff, Camera, ArrowLeftRight, Send, Phone, MoreHorizontal, Rocket, AlertTriangle, PhoneCall, CameraOff, Image, Images, CloudSun, Calendar, TrendingUp, Coins, Wrench, Globe, Terminal, X } from 'lucide-react';
|
||||
import { Plus, Search, Play, Copy, Trash2, Edit2, Mic, MessageSquare, Save, Video, PhoneOff, Camera, ArrowLeftRight, Send, Phone, MoreHorizontal, Rocket, AlertTriangle, PhoneCall, CameraOff, Image, Images, CloudSun, Calendar, TrendingUp, Coins, Wrench, Globe, Terminal, X, ClipboardCheck, Sparkles, Volume2, Timer, ChevronDown, Link as LinkIcon, Database, Server, Zap, ExternalLink, Key } from 'lucide-react';
|
||||
import { Button, Input, Card, Badge, Drawer, Dialog } from '../components/UI';
|
||||
import { mockAssistants, mockKnowledgeBases } from '../services/mockData';
|
||||
import { mockAssistants, mockKnowledgeBases, mockVoices } from '../services/mockData';
|
||||
import { Assistant, TabValue } from '../types';
|
||||
import { GoogleGenAI } from "@google/genai";
|
||||
|
||||
@@ -23,9 +23,13 @@ export const AssistantsPage: React.FC = () => {
|
||||
const [debugOpen, setDebugOpen] = useState(false);
|
||||
const [hotwordInput, setHotwordInput] = useState('');
|
||||
|
||||
// Publish Modal State
|
||||
const [isPublishModalOpen, setIsPublishModalOpen] = useState(false);
|
||||
const [publishTab, setPublishTab] = useState<'web' | 'api'>('web');
|
||||
|
||||
// Custom Tools State
|
||||
const [customTools, setCustomTools] = useState<ToolItem[]>([]);
|
||||
const [hiddenToolIds, setHiddenToolIds] = useState<string[]>([]); // Track deleted/hidden base tools
|
||||
const [hiddenToolIds, setHiddenToolIds] = useState<string[]>([]);
|
||||
const [isAddToolModalOpen, setIsAddToolModalOpen] = useState(false);
|
||||
const [addingToCategory, setAddingToCategory] = useState<'system' | 'query'>('system');
|
||||
|
||||
@@ -33,8 +37,9 @@ export const AssistantsPage: React.FC = () => {
|
||||
const [newToolName, setNewToolName] = useState('');
|
||||
const [newToolDesc, setNewToolDesc] = useState('');
|
||||
|
||||
// State for delete confirmation dialog
|
||||
const [deleteId, setDeleteId] = useState<string | null>(null);
|
||||
const [copySuccess, setCopySuccess] = useState(false);
|
||||
const [saveLoading, setSaveLoading] = useState(false);
|
||||
|
||||
const selectedAssistant = assistants.find(a => a.id === selectedId) || null;
|
||||
|
||||
@@ -43,7 +48,7 @@ export const AssistantsPage: React.FC = () => {
|
||||
);
|
||||
|
||||
const handleCreate = () => {
|
||||
const newId = Date.now().toString();
|
||||
const newId = Math.floor(Math.random() * 1000000).toString().padStart(6, '0');
|
||||
const newAssistant: Assistant = {
|
||||
id: newId,
|
||||
name: 'New Assistant',
|
||||
@@ -52,18 +57,37 @@ export const AssistantsPage: React.FC = () => {
|
||||
prompt: '',
|
||||
knowledgeBaseId: '',
|
||||
language: 'zh',
|
||||
voice: 'default',
|
||||
voice: mockVoices[0]?.id || '',
|
||||
speed: 1,
|
||||
hotwords: [],
|
||||
tools: []
|
||||
tools: [],
|
||||
interruptionSensitivity: 500,
|
||||
configMode: 'platform',
|
||||
};
|
||||
setAssistants([...assistants, newAssistant]);
|
||||
setSelectedId(newId);
|
||||
setActiveTab(TabValue.GLOBAL);
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
setSaveLoading(true);
|
||||
// Simulate API call
|
||||
setTimeout(() => {
|
||||
setSaveLoading(false);
|
||||
// In a real app, logic to persist selectedAssistant would go here
|
||||
}, 800);
|
||||
};
|
||||
|
||||
const handleCopyId = (id: string, text?: string) => {
|
||||
navigator.clipboard.writeText(text || id);
|
||||
setCopySuccess(true);
|
||||
setTimeout(() => setCopySuccess(false), 2000);
|
||||
};
|
||||
|
||||
const handleCopy = (e: React.MouseEvent, assistant: Assistant) => {
|
||||
e.stopPropagation();
|
||||
const newAssistant = { ...assistant, id: Date.now().toString(), name: `${assistant.name} (Copy)` };
|
||||
const newId = Math.floor(Math.random() * 1000000).toString().padStart(6, '0');
|
||||
const newAssistant = { ...assistant, id: newId, name: `${assistant.name} (Copy)` };
|
||||
setAssistants([...assistants, newAssistant]);
|
||||
};
|
||||
|
||||
@@ -83,6 +107,14 @@ export const AssistantsPage: React.FC = () => {
|
||||
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) => {
|
||||
@@ -96,13 +128,11 @@ export const AssistantsPage: React.FC = () => {
|
||||
|
||||
const deleteTool = (e: React.MouseEvent, toolId: string) => {
|
||||
e.stopPropagation();
|
||||
// 1. Remove from assistant configurations if enabled
|
||||
setAssistants(prev => prev.map(a => ({
|
||||
...a,
|
||||
tools: a.tools?.filter(id => id !== toolId) || []
|
||||
})));
|
||||
|
||||
// 2. Remove from tool list
|
||||
if (customTools.some(t => t.id === toolId)) {
|
||||
setCustomTools(prev => prev.filter(t => t.id !== toolId));
|
||||
} else {
|
||||
@@ -145,7 +175,6 @@ export const AssistantsPage: React.FC = () => {
|
||||
setIsAddToolModalOpen(true);
|
||||
};
|
||||
|
||||
// Define tools available
|
||||
const baseSystemTools: ToolItem[] = [
|
||||
{ id: 'cam_open', name: '打开相机', icon: <Camera className="w-4 h-4" />, desc: '允许 AI 开启摄像头流', category: 'system' },
|
||||
{ id: 'cam_close', name: '关闭相机', icon: <CameraOff className="w-4 h-4" />, desc: '允许 AI 停止摄像头流', category: 'system' },
|
||||
@@ -163,11 +192,14 @@ export const AssistantsPage: React.FC = () => {
|
||||
const systemTools = [...baseSystemTools, ...customTools.filter(t => t.category === 'system')].filter(t => !hiddenToolIds.includes(t.id));
|
||||
const queryTools = [...baseQueryTools, ...customTools.filter(t => t.category === 'query')].filter(t => !hiddenToolIds.includes(t.id));
|
||||
|
||||
const isExternalConfig = selectedAssistant?.configMode === 'dify' || selectedAssistant?.configMode === 'fastgpt';
|
||||
const isNoneConfig = selectedAssistant?.configMode === 'none' || !selectedAssistant?.configMode;
|
||||
|
||||
return (
|
||||
<div className="flex h-[calc(100vh-6rem)] gap-6 animate-in fade-in">
|
||||
<div className="flex h-[calc(100vh-8rem)] gap-6 animate-in fade-in py-4">
|
||||
{/* LEFT COLUMN: List */}
|
||||
<div className="w-80 flex flex-col gap-4 shrink-0">
|
||||
<div className="flex items-center justify-between px-1">
|
||||
<div className="flex items-center justify-between px-1 text-white">
|
||||
<h2 className="text-xl font-bold tracking-tight text-white">小助手列表</h2>
|
||||
</div>
|
||||
|
||||
@@ -197,10 +229,24 @@ export const AssistantsPage: React.FC = () => {
|
||||
: 'bg-card/30 border-white/5 hover:bg-white/5 hover:border-white/10'
|
||||
}`}
|
||||
>
|
||||
<div className="flex justify-between items-start mb-2">
|
||||
<span className={`font-semibold truncate pr-6 ${selectedId === assistant.id ? 'text-primary' : 'text-foreground'}`}>
|
||||
<div className="flex flex-col gap-1.5 mb-2 pr-16 overflow-hidden">
|
||||
<span className={`font-semibold truncate ${selectedId === assistant.id ? 'text-primary' : 'text-foreground'}`}>
|
||||
{assistant.name}
|
||||
</span>
|
||||
{assistant.configMode && assistant.configMode !== 'none' && (
|
||||
<div className="flex">
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={`text-[9px] uppercase tracking-tighter shrink-0 opacity-70 border-white/10 ${
|
||||
assistant.configMode === 'platform' ? 'text-cyan-400 bg-cyan-400/5' :
|
||||
assistant.configMode === 'dify' ? 'text-blue-400 bg-blue-400/5' :
|
||||
'text-purple-400 bg-purple-400/5'
|
||||
}`}
|
||||
>
|
||||
{assistant.configMode === 'platform' ? '内置' : assistant.configMode}
|
||||
</Badge>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center text-xs text-muted-foreground">
|
||||
@@ -208,8 +254,7 @@ export const AssistantsPage: React.FC = () => {
|
||||
<span>{assistant.callCount} 次通话</span>
|
||||
</div>
|
||||
|
||||
{/* Hover Actions */}
|
||||
<div className="absolute right-2 top-2 flex space-x-1 opacity-0 group-hover:opacity-100 transition-opacity bg-background/50 backdrop-blur-sm rounded-lg p-0.5">
|
||||
<div className="absolute right-2 top-2 flex space-x-1 opacity-0 group-hover:opacity-100 transition-opacity bg-background/50 backdrop-blur-sm rounded-lg p-0.5 shadow-lg border border-white/5">
|
||||
<Button variant="ghost" size="icon" className="h-7 w-7" onClick={(e) => handleCopy(e, assistant)} title="复制">
|
||||
<Copy className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
@@ -232,18 +277,29 @@ export const AssistantsPage: React.FC = () => {
|
||||
{selectedAssistant ? (
|
||||
<>
|
||||
{/* Header Area */}
|
||||
<div className="p-6 border-b border-white/5 bg-white/[0.02] space-y-4">
|
||||
{/* Row 1: Name and Actions */}
|
||||
<div className="flex items-end justify-between gap-4">
|
||||
<div className="p-6 border-b border-white/5 bg-white/[0.02] space-y-5">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="flex-1">
|
||||
<label className="text-[10px] text-muted-foreground font-black tracking-widest uppercase mb-2 block ml-1">ASSISTANT NAME</label>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<label className="text-[10px] text-muted-foreground font-black tracking-widest uppercase ml-1">小助手名称</label>
|
||||
<div className="flex items-center gap-1.5 px-2 py-0.5 rounded-md bg-white/5 border border-white/10 group/id transition-all hover:bg-white/10">
|
||||
<span className="text-[10px] font-mono text-muted-foreground/60 select-all tracking-tight">UUID: {selectedAssistant.id}</span>
|
||||
<button
|
||||
onClick={() => handleCopyId(selectedAssistant.id)}
|
||||
className="text-muted-foreground hover:text-primary transition-colors flex items-center"
|
||||
title="复制ID"
|
||||
>
|
||||
{copySuccess ? <ClipboardCheck className="h-3 w-3 text-green-400" /> : <Copy className="h-3 w-3" />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<Input
|
||||
value={selectedAssistant.name}
|
||||
onChange={(e) => updateAssistant('name', e.target.value)}
|
||||
className="font-bold bg-white/5 border-white/10 focus:border-primary/50 text-base"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="flex items-center space-x-2 pt-6">
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => setDebugOpen(true)}
|
||||
@@ -252,7 +308,15 @@ export const AssistantsPage: React.FC = () => {
|
||||
<Play className="mr-2 h-4 w-4" /> 调试
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => alert("发布成功!")}
|
||||
variant="outline"
|
||||
onClick={handleSave}
|
||||
disabled={saveLoading}
|
||||
className="border border-white/10 hover:border-primary/50 text-foreground"
|
||||
>
|
||||
<Save className={`mr-2 h-4 w-4 ${saveLoading ? 'animate-pulse' : ''}`} /> {saveLoading ? '正在保存...' : '保存'}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => setIsPublishModalOpen(true)}
|
||||
className="shadow-[0_0_20px_rgba(6,182,212,0.3)]"
|
||||
>
|
||||
<Rocket className="mr-2 h-4 w-4" /> 发布
|
||||
@@ -260,8 +324,27 @@ export const AssistantsPage: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Row 2: Tabs */}
|
||||
<div className="flex bg-white/5 p-1 rounded-lg w-fit">
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] text-muted-foreground font-black tracking-widest uppercase ml-1">配置方式</label>
|
||||
<div className="relative group w-full">
|
||||
<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-all focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary/50 text-foreground appearance-none cursor-pointer [&>option]:bg-card"
|
||||
value={selectedAssistant.configMode || 'none'}
|
||||
onChange={(e) => updateAssistant('configMode', e.target.value as any)}
|
||||
>
|
||||
<option value="none">无</option>
|
||||
<option value="platform">平台配置</option>
|
||||
<option value="dify">Dify 接入</option>
|
||||
<option value="fastgpt">FastGPT 接入</option>
|
||||
</select>
|
||||
<ChevronDown className="absolute right-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground pointer-events-none group-hover:text-primary transition-colors" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!isNoneConfig && (
|
||||
<div className="flex bg-white/5 p-1 rounded-lg w-fit border border-white/5 animate-in fade-in slide-in-from-top-1">
|
||||
{selectedAssistant.configMode === 'platform' ? (
|
||||
<>
|
||||
<button
|
||||
onClick={() => setActiveTab(TabValue.GLOBAL)}
|
||||
className={`px-6 py-1.5 text-sm font-medium rounded-md transition-all ${activeTab === TabValue.GLOBAL ? 'bg-primary text-primary-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground hover:bg-white/5'}`}
|
||||
@@ -280,13 +363,77 @@ export const AssistantsPage: React.FC = () => {
|
||||
>
|
||||
工具配置
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<button
|
||||
onClick={() => setActiveTab(TabValue.LINK)}
|
||||
className={`px-6 py-1.5 text-sm font-medium rounded-md transition-all ${activeTab === TabValue.LINK ? 'bg-primary text-primary-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground hover:bg-white/5'}`}
|
||||
>
|
||||
链接设置
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab(TabValue.VOICE)}
|
||||
className={`px-6 py-1.5 text-sm font-medium rounded-md transition-all ${activeTab === TabValue.VOICE ? 'bg-primary text-primary-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground hover:bg-white/5'}`}
|
||||
>
|
||||
语音设置
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto p-8 custom-scrollbar">
|
||||
{isNoneConfig ? (
|
||||
<div className="h-full flex flex-col items-center justify-center text-muted-foreground opacity-40 animate-in fade-in">
|
||||
<AlertTriangle className="w-12 h-12 mb-4" />
|
||||
<p className="text-sm font-medium">请先选择配置方式以展开详细设置</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="max-w-4xl mx-auto space-y-8 animate-in slide-in-from-bottom-2 duration-300">
|
||||
{activeTab === TabValue.LINK && isExternalConfig && (
|
||||
<div className="space-y-6 animate-in fade-in">
|
||||
<div className="p-4 rounded-xl bg-primary/5 border border-primary/20 flex items-start gap-3 mb-4">
|
||||
<Database className="w-5 h-5 text-primary shrink-0 mt-0.5" />
|
||||
<div>
|
||||
<h4 className="text-sm font-bold text-white mb-1">
|
||||
接入 {selectedAssistant.configMode === 'dify' ? 'Dify' : 'FastGPT'} 引擎
|
||||
</h4>
|
||||
<p className="text-xs text-muted-foreground leading-relaxed">
|
||||
配置后,视频通话过程中的对话逻辑、知识库检索以及工作流将由外部引擎托管。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content Scroll Area */}
|
||||
<div className="flex-1 overflow-y-auto p-8 custom-scrollbar">
|
||||
<div className="max-w-4xl mx-auto space-y-8 animate-in slide-in-from-bottom-2 duration-300">
|
||||
{activeTab === TabValue.GLOBAL && (
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-white flex items-center">
|
||||
<Globe className="w-4 h-4 mr-2 text-primary"/> 接口地址 (API URL)
|
||||
</label>
|
||||
<Input
|
||||
value={selectedAssistant.apiUrl || ''}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-white flex items-center">
|
||||
<Terminal className="w-4 h-4 mr-2 text-primary"/> 密钥 (API KEY)
|
||||
</label>
|
||||
<Input
|
||||
type="password"
|
||||
value={selectedAssistant.apiKey || ''}
|
||||
onChange={(e) => updateAssistant('apiKey', e.target.value)}
|
||||
placeholder="请输入应用 API 密钥..."
|
||||
className="bg-white/5 border-white/10 focus:border-primary/50 font-mono text-xs"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === TabValue.GLOBAL && selectedAssistant.configMode === 'platform' && (
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-white flex items-center">
|
||||
@@ -329,57 +476,69 @@ export const AssistantsPage: React.FC = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === TabValue.VOICE && (
|
||||
{activeTab === TabValue.VOICE && !isNoneConfig && (
|
||||
<div className="space-y-8">
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-white">语言 (Language)</label>
|
||||
<label className="text-sm font-medium text-white flex items-center">
|
||||
<Volume2 className="w-4 h-4 mr-2 text-primary"/> 选择音色 (From Voice Library)
|
||||
</label>
|
||||
<div className="relative group">
|
||||
<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 [&>option]:bg-card text-foreground"
|
||||
value={selectedAssistant.language}
|
||||
onChange={(e) => updateAssistant('language', e.target.value)}
|
||||
>
|
||||
<option value="zh">中文 (Chinese)</option>
|
||||
<option value="en">英文 (English)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-white">音色 (Voice)</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 [&>option]:bg-card text-foreground"
|
||||
className="flex h-12 w-full rounded-xl border border-white/10 bg-white/5 px-4 py-1 text-sm shadow-sm transition-all focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary/50 [&>option]:bg-card text-foreground appearance-none cursor-pointer"
|
||||
value={selectedAssistant.voice}
|
||||
onChange={(e) => updateAssistant('voice', e.target.value)}
|
||||
>
|
||||
<option value="default">默认 (Default)</option>
|
||||
<option value="alloy">Alloy</option>
|
||||
<option value="echo">Echo</option>
|
||||
<option value="fable">Fable</option>
|
||||
<option value="onyx">Onyx</option>
|
||||
<option value="nova">Nova</option>
|
||||
<option value="shimmer">Shimmer</option>
|
||||
<option value="" disabled>请选择声音库中的声音...</option>
|
||||
{mockVoices.map(voice => (
|
||||
<option key={voice.id} value={voice.id}>
|
||||
{voice.name} ({voice.vendor} - {voice.gender === 'Male' ? '男' : '女'})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<ChevronDown className="absolute right-4 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground pointer-events-none group-hover:text-primary transition-colors" />
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground flex items-center mt-1">
|
||||
<Sparkles className="w-3 h-3 mr-1 text-primary opacity-70" />
|
||||
音色配置同步自声音库。如需添加更多音色,请前往“声音库”模块。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4 p-4 rounded-xl border border-white/5 bg-white/[0.02]">
|
||||
<div className="flex justify-between items-center">
|
||||
<label className="text-sm font-medium text-white">语速 (Speed)</label>
|
||||
<span className="text-sm font-mono text-primary bg-primary/10 px-2 py-0.5 rounded">{selectedAssistant.speed}x</span>
|
||||
<div className="space-y-4 pt-2">
|
||||
<div className="flex justify-between items-center mb-1">
|
||||
<label className="text-sm font-medium text-white flex items-center">
|
||||
<Timer className="w-4 h-4 mr-2 text-primary"/> 打断灵敏度 (Interruption Sensitivity)
|
||||
</label>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="relative">
|
||||
<Input
|
||||
type="number"
|
||||
value={selectedAssistant.interruptionSensitivity || 500}
|
||||
onChange={(e) => updateAssistant('interruptionSensitivity', parseInt(e.target.value) || 0)}
|
||||
className="w-20 h-8 text-right pr-7 text-xs font-mono bg-black/40 border-white/5"
|
||||
/>
|
||||
<span className="absolute right-2 top-1/2 -translate-y-1/2 text-[10px] text-muted-foreground font-mono">ms</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-6">
|
||||
<input
|
||||
type="range"
|
||||
min="0.5"
|
||||
max="2.0"
|
||||
step="0.1"
|
||||
value={selectedAssistant.speed}
|
||||
onChange={(e) => updateAssistant('speed', parseFloat(e.target.value))}
|
||||
className="w-full h-2 bg-secondary rounded-lg appearance-none cursor-pointer accent-primary"
|
||||
min="0"
|
||||
max="2000"
|
||||
step="50"
|
||||
value={selectedAssistant.interruptionSensitivity || 500}
|
||||
onChange={(e) => updateAssistant('interruptionSensitivity', parseInt(e.target.value))}
|
||||
className="flex-1 h-1.5 bg-secondary rounded-lg appearance-none cursor-pointer accent-primary"
|
||||
/>
|
||||
<div className="flex justify-between text-xs text-muted-foreground">
|
||||
<span>0.5x (Slow)</span>
|
||||
<span>1.0x (Normal)</span>
|
||||
<span>2.0x (Fast)</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-[10px] text-muted-foreground font-mono uppercase tracking-widest px-0.5 opacity-50">
|
||||
<span>0ms (Extreme)</span>
|
||||
<span>1000ms</span>
|
||||
<span>2000ms (Lazy)</span>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground pt-1 italic opacity-60">
|
||||
* 定义用户说话多长时间后 AI 应当停止当前的发言并响应。数值越小响应越快,但也更容易被噪音误导打断。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
@@ -394,16 +553,16 @@ export const AssistantsPage: React.FC = () => {
|
||||
onKeyDown={(e) => e.key === 'Enter' && addHotword()}
|
||||
className="bg-white/5 border-white/10"
|
||||
/>
|
||||
<Button variant="secondary" onClick={addHotword}>添加</Button>
|
||||
<Button variant="secondary" onClick={addHotword} className="px-10 whitespace-nowrap">添加</Button>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2 min-h-[40px] p-2 rounded-lg border border-dashed border-white/10">
|
||||
{selectedAssistant.hotwords.length === 0 && (
|
||||
<span className="text-xs text-muted-foreground py-1">暂无热词</span>
|
||||
<span className="text-xs text-muted-foreground py-1 px-1">暂无热词</span>
|
||||
)}
|
||||
{selectedAssistant.hotwords.map((word, idx) => (
|
||||
<Badge key={idx} variant="outline">
|
||||
<Badge key={idx} variant="outline" className="py-1">
|
||||
{word}
|
||||
<button onClick={() => removeHotword(word)} className="ml-2 hover:text-destructive transition-colors">×</button>
|
||||
<button onClick={() => removeHotword(word)} className="ml-2 hover:text-destructive transition-colors text-lg leading-none">×</button>
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
@@ -412,7 +571,7 @@ export const AssistantsPage: React.FC = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === TabValue.TOOLS && (
|
||||
{activeTab === TabValue.TOOLS && selectedAssistant.configMode === 'platform' && (
|
||||
<div className="space-y-8 animate-in fade-in">
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
@@ -445,7 +604,6 @@ export const AssistantsPage: React.FC = () => {
|
||||
</div>
|
||||
<p className="text-[10px] text-muted-foreground line-clamp-1 opacity-70">{tool.desc}</p>
|
||||
</div>
|
||||
{/* Delete Button */}
|
||||
<button
|
||||
onClick={(e) => deleteTool(e, tool.id)}
|
||||
className="absolute -top-1 -right-1 p-0.5 rounded-full bg-destructive text-white opacity-0 group-hover:opacity-100 transition-opacity hover:scale-110 shadow-lg z-10"
|
||||
@@ -489,7 +647,6 @@ export const AssistantsPage: React.FC = () => {
|
||||
</div>
|
||||
<p className="text-[10px] text-muted-foreground line-clamp-1 opacity-70">{tool.desc}</p>
|
||||
</div>
|
||||
{/* Delete Button */}
|
||||
<button
|
||||
onClick={(e) => deleteTool(e, tool.id)}
|
||||
className="absolute -top-1 -right-1 p-0.5 rounded-full bg-destructive text-white opacity-0 group-hover:opacity-100 transition-opacity hover:scale-110 shadow-lg z-10"
|
||||
@@ -508,6 +665,7 @@ export const AssistantsPage: React.FC = () => {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
@@ -521,6 +679,96 @@ export const AssistantsPage: React.FC = () => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Publish Modal */}
|
||||
<Dialog
|
||||
isOpen={isPublishModalOpen}
|
||||
onClose={() => setIsPublishModalOpen(false)}
|
||||
title="发布小助手"
|
||||
footer={
|
||||
<Button onClick={() => setIsPublishModalOpen(false)}>确认</Button>
|
||||
}
|
||||
>
|
||||
<div className="space-y-6">
|
||||
<div className="flex bg-white/5 p-1 rounded-lg border border-white/10">
|
||||
<button
|
||||
onClick={() => setPublishTab('web')}
|
||||
className={`flex-1 flex items-center justify-center py-2 text-xs font-bold rounded-md transition-all ${publishTab === 'web' ? 'bg-primary text-primary-foreground shadow-lg' : 'text-muted-foreground hover:text-foreground'}`}
|
||||
>
|
||||
<ExternalLink className="w-3.5 h-3.5 mr-2" /> 网页分享
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setPublishTab('api')}
|
||||
className={`flex-1 flex items-center justify-center py-2 text-xs font-bold rounded-md transition-all ${publishTab === 'api' ? 'bg-primary text-primary-foreground shadow-lg' : 'text-muted-foreground hover:text-foreground'}`}
|
||||
>
|
||||
<Server className="w-3.5 h-3.5 mr-2" /> API 接入
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{publishTab === 'web' ? (
|
||||
<div className="space-y-4 animate-in fade-in slide-in-from-top-1">
|
||||
<div className="p-4 rounded-xl bg-primary/5 border border-primary/20 space-y-3">
|
||||
<div className="flex items-center gap-2 text-primary">
|
||||
<Zap className="w-4 h-4" />
|
||||
<h4 className="text-sm font-bold">交互体验站</h4>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground leading-relaxed">
|
||||
该链接允许用户通过独立的浏览器页面与您的智能体进行交互。支持:文本对话、实时音频通话以及双向视频通话。
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] text-muted-foreground font-black tracking-widest uppercase">公开访问链接</label>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
readOnly
|
||||
value={`https://ai-video.com/share/${selectedAssistant?.id}`}
|
||||
className="bg-white/5 border-white/10 font-mono text-[11px] text-primary"
|
||||
/>
|
||||
<Button variant="secondary" size="icon" onClick={() => handleCopyId('', `https://ai-video.com/share/${selectedAssistant?.id}`)}>
|
||||
{copySuccess ? <ClipboardCheck className="h-4 w-4 text-green-400" /> : <Copy className="h-4 w-4" />}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-5 animate-in fade-in slide-in-from-top-1">
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] text-muted-foreground font-black tracking-widest uppercase">API Endpoint (v1)</label>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
readOnly
|
||||
value={`https://api.ai-video.com/v1/call/${selectedAssistant?.id}`}
|
||||
className="bg-white/5 border-white/10 font-mono text-[11px]"
|
||||
/>
|
||||
<Button variant="ghost" size="icon" onClick={() => handleCopyId('', `https://api.ai-video.com/v1/call/${selectedAssistant?.id}`)}>
|
||||
<Copy className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="text-[10px] text-muted-foreground font-black tracking-widest uppercase">Secret API KEY</label>
|
||||
<Badge variant="outline" className="text-[8px] opacity-50">PRIVATE</Badge>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
readOnly
|
||||
type="password"
|
||||
value="sk-ai-video-78x29jKkL1M90vX..."
|
||||
className="bg-white/5 border-white/10 font-mono text-[11px]"
|
||||
/>
|
||||
<Button variant="ghost" size="icon" onClick={() => handleCopyId('', "sk-ai-video-78x29jKkL1M90vX...")}>
|
||||
<Copy className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-[10px] text-muted-foreground italic flex items-center gap-1.5 px-1 mt-2">
|
||||
<Key className="w-3 h-3" /> API Key 仅用于身份鉴权,请务必妥善保存。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Dialog>
|
||||
|
||||
{selectedAssistant && (
|
||||
<DebugDrawer
|
||||
isOpen={debugOpen}
|
||||
@@ -562,7 +810,7 @@ export const AssistantsPage: React.FC = () => {
|
||||
</div>
|
||||
<div className="p-3 bg-primary/5 border border-primary/20 rounded-lg text-[10px] text-muted-foreground flex items-start gap-2">
|
||||
<Wrench className="w-3.5 h-3.5 text-primary shrink-0 mt-0.5" />
|
||||
<p>自定义工具将通过其名称和描述告知 AI 它的用途。您可以在后续的工作流中进一步定义 these 工具的具体行为逻辑。</p>
|
||||
<p>自定义工具将通过其名称 and 描述告知 AI 它的用途。您可以在后续的工作流中进一步定义 these 工具的具体行为逻辑。</p>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { Plus, Search, Play, Copy, Trash2, Zap, MessageSquare, Mic, AlertTriangle, ListFilter, Braces, Rocket } from 'lucide-react';
|
||||
import { Plus, Search, Play, Copy, Trash2, Zap, MessageSquare, Mic, AlertTriangle, ClipboardCheck, X } from 'lucide-react';
|
||||
import { Button, Input, Card, Badge, Dialog } from '../components/UI';
|
||||
import { mockAutoTestAssistants, mockAssistants } from '../services/mockData';
|
||||
import { AutoTestAssistant, TestType, TestMethod } from '../types';
|
||||
@@ -9,93 +9,87 @@ export const AutoTestPage: React.FC = () => {
|
||||
const [testAssistants, setTestAssistants] = useState<AutoTestAssistant[]>(mockAutoTestAssistants);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
const [deleteId, setDeleteId] = useState<string | null>(null);
|
||||
const [copySuccess, setCopySuccess] = useState(false);
|
||||
|
||||
const selectedTestAssistant = testAssistants.find(a => a.id === selectedId) || null;
|
||||
|
||||
const filteredTests = testAssistants.filter(a =>
|
||||
a.name.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
const filteredTests = testAssistants.filter(t =>
|
||||
t.name.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
);
|
||||
|
||||
const selectedTest = testAssistants.find(t => t.id === selectedId) || null;
|
||||
|
||||
const handleCreate = () => {
|
||||
const newId = `at_${Date.now()}`;
|
||||
const newAssistant: AutoTestAssistant = {
|
||||
const newId = crypto.randomUUID();
|
||||
const newTest: AutoTestAssistant = {
|
||||
id: newId,
|
||||
name: '新测试助手',
|
||||
type: TestType.INTELLIGENT,
|
||||
name: '新测试任务',
|
||||
type: TestType.FIXED,
|
||||
method: TestMethod.TEXT,
|
||||
targetAssistantId: mockAssistants[0]?.id || '',
|
||||
fixedWorkflowSteps: [],
|
||||
intelligentPrompt: '你是一个普通的测试用户,试图了解产品信息。',
|
||||
createdAt: new Date().toISOString().replace('T', ' ').substring(0, 16)
|
||||
intelligentPrompt: '',
|
||||
createdAt: new Date().toISOString().split('T')[0],
|
||||
};
|
||||
setTestAssistants([...testAssistants, newAssistant]);
|
||||
setTestAssistants([newTest, ...testAssistants]);
|
||||
setSelectedId(newId);
|
||||
};
|
||||
|
||||
const handleCopy = (e: React.MouseEvent, assistant: AutoTestAssistant) => {
|
||||
const handleCopy = (e: React.MouseEvent, test: AutoTestAssistant) => {
|
||||
e.stopPropagation();
|
||||
const newAssistant = { ...assistant, id: `at_${Date.now()}`, name: `${assistant.name} (Copy)` };
|
||||
setTestAssistants([...testAssistants, newAssistant]);
|
||||
const newId = crypto.randomUUID();
|
||||
const newTest: AutoTestAssistant = {
|
||||
...test,
|
||||
id: newId,
|
||||
name: `${test.name} (复制)`,
|
||||
createdAt: new Date().toISOString().split('T')[0],
|
||||
};
|
||||
setTestAssistants([newTest, ...testAssistants]);
|
||||
setSelectedId(newId);
|
||||
};
|
||||
|
||||
const updateTest = (field: keyof AutoTestAssistant, value: any) => {
|
||||
if (!selectedId) return;
|
||||
setTestAssistants(prev => prev.map(t => t.id === selectedId ? { ...t, [field]: value } : t));
|
||||
};
|
||||
|
||||
const handleDeleteClick = (e: React.MouseEvent, id: string) => {
|
||||
e.stopPropagation();
|
||||
setDeleteId(id);
|
||||
setIsDeleteModalOpen(true);
|
||||
};
|
||||
|
||||
const confirmDelete = () => {
|
||||
if (deleteId) {
|
||||
setTestAssistants(prev => prev.filter(a => a.id !== deleteId));
|
||||
setTestAssistants(prev => prev.filter(t => t.id !== deleteId));
|
||||
if (selectedId === deleteId) setSelectedId(null);
|
||||
setIsDeleteModalOpen(false);
|
||||
setDeleteId(null);
|
||||
}
|
||||
};
|
||||
|
||||
const updateAssistant = (field: keyof AutoTestAssistant, value: any) => {
|
||||
if (!selectedId) return;
|
||||
setTestAssistants(prev => prev.map(a => a.id === selectedId ? { ...a, [field]: value } : a));
|
||||
};
|
||||
|
||||
const handleAddStep = () => {
|
||||
if (selectedTestAssistant) {
|
||||
updateAssistant('fixedWorkflowSteps', [...selectedTestAssistant.fixedWorkflowSteps, '']);
|
||||
}
|
||||
};
|
||||
|
||||
const updateStep = (idx: number, val: string) => {
|
||||
if (selectedTestAssistant) {
|
||||
const newSteps = [...selectedTestAssistant.fixedWorkflowSteps];
|
||||
newSteps[idx] = val;
|
||||
updateAssistant('fixedWorkflowSteps', newSteps);
|
||||
}
|
||||
};
|
||||
|
||||
const removeStep = (idx: number) => {
|
||||
if (selectedTestAssistant) {
|
||||
updateAssistant('fixedWorkflowSteps', selectedTestAssistant.fixedWorkflowSteps.filter((_, i) => i !== idx));
|
||||
}
|
||||
const handleCopyId = (id: string) => {
|
||||
navigator.clipboard.writeText(id);
|
||||
setCopySuccess(true);
|
||||
setTimeout(() => setCopySuccess(false), 2000);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-[calc(100vh-6rem)] gap-6 animate-in fade-in">
|
||||
{/* LEFT COLUMN: Test Assistants List */}
|
||||
<div className="flex h-[calc(100vh-8rem)] gap-6 animate-in fade-in py-4">
|
||||
{/* Left List */}
|
||||
<div className="w-80 flex flex-col gap-4 shrink-0">
|
||||
<div className="flex items-center justify-between px-1">
|
||||
<h2 className="text-xl font-bold tracking-tight">测试助手列表</h2>
|
||||
</div>
|
||||
|
||||
<h2 className="text-xl font-bold tracking-tight text-white">测试助手列表</h2>
|
||||
<div className="flex gap-2">
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="搜索测试助手..."
|
||||
placeholder="搜索测试..."
|
||||
className="pl-9 bg-card/50 border-white/5"
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<Button size="icon" onClick={handleCreate} title="新建测试助手">
|
||||
<Button size="icon" onClick={handleCreate} title="新建测试">
|
||||
<Plus className="h-5 w-5" />
|
||||
</Button>
|
||||
</div>
|
||||
@@ -111,201 +105,194 @@ export const AutoTestPage: React.FC = () => {
|
||||
: 'bg-card/30 border-white/5 hover:bg-white/5 hover:border-white/10'
|
||||
}`}
|
||||
>
|
||||
<div className="flex justify-between items-start mb-2">
|
||||
<span className={`font-semibold truncate pr-6 ${selectedId === test.id ? 'text-primary' : 'text-foreground'}`}>
|
||||
<div className="flex flex-col gap-1.5 mb-2 pr-16 overflow-hidden">
|
||||
<span className={`font-semibold truncate ${selectedId === test.id ? 'text-primary' : 'text-foreground'}`}>
|
||||
{test.name}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-3 text-[10px] text-muted-foreground font-mono uppercase">
|
||||
<Badge variant="outline" className="text-[9px] py-0 px-1.5 opacity-70">
|
||||
{test.type === TestType.FIXED ? '固定流程' : '智能测试'}
|
||||
<div className="flex">
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={`text-[9px] uppercase tracking-tighter shrink-0 opacity-70 border-white/10 ${
|
||||
test.type === TestType.FIXED ? 'text-blue-400 bg-blue-400/5' : 'text-purple-400 bg-purple-400/5'
|
||||
}`}
|
||||
>
|
||||
{test.type === TestType.FIXED ? '固定' : '智能'}
|
||||
</Badge>
|
||||
<div className="flex items-center">
|
||||
{test.method === TestMethod.TEXT ? <MessageSquare className="h-2.5 w-2.5 mr-1" /> : <Mic className="h-2.5 w-2.5 mr-1" />}
|
||||
{test.method === TestMethod.TEXT ? '文本' : '音频'}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-[10px] text-muted-foreground font-mono opacity-60">
|
||||
创建于: {test.createdAt}
|
||||
</div>
|
||||
|
||||
{/* Hover Actions */}
|
||||
<div className="absolute right-2 top-2 flex space-x-1 opacity-0 group-hover:opacity-100 transition-opacity bg-background/50 backdrop-blur-sm rounded-lg p-0.5">
|
||||
<Button variant="ghost" size="icon" className="h-7 w-7" onClick={(e) => handleCopy(e, test)} title="复制">
|
||||
{/* Hover Actions Toolbar */}
|
||||
<div className="absolute right-2 top-2 flex space-x-1 opacity-0 group-hover:opacity-100 transition-opacity bg-background/50 backdrop-blur-sm rounded-lg p-0.5 shadow-lg border border-white/5">
|
||||
<Button variant="ghost" size="icon" className="h-7 w-7" onClick={(e) => handleCopy(e, test)} title="复制测试任务">
|
||||
<Copy className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" className="h-7 w-7 text-destructive hover:text-destructive" onClick={(e) => handleDeleteClick(e, test.id)} title="删除">
|
||||
<Button variant="ghost" size="icon" className="h-7 w-7 text-destructive hover:text-destructive" onClick={(e) => handleDeleteClick(e, test.id)} title="删除测试任务">
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{filteredTests.length === 0 && (
|
||||
<div className="text-center py-10 text-muted-foreground text-sm font-mono opacity-50">
|
||||
[ NO TESTERS FOUND ]
|
||||
<div className="text-center py-10 text-muted-foreground text-sm">
|
||||
未找到测试助手
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* RIGHT COLUMN: Config Panel */}
|
||||
{/* Right Config Panel */}
|
||||
<div className="flex-1 bg-card/20 backdrop-blur-sm border border-white/5 rounded-2xl overflow-hidden flex flex-col relative shadow-xl">
|
||||
{selectedTestAssistant ? (
|
||||
<>
|
||||
{/* Header Area */}
|
||||
<div className="p-6 border-b border-white/5 bg-white/[0.02] space-y-4">
|
||||
{selectedTest ? (
|
||||
<div className="flex flex-col h-full overflow-hidden">
|
||||
<div className="p-6 border-b border-white/5 bg-white/[0.02]">
|
||||
<div className="flex items-end justify-between gap-4">
|
||||
<div className="flex-1">
|
||||
<label className="text-[10px] text-muted-foreground font-black uppercase tracking-widest mb-2 block ml-1">TESTER NAME</label>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<label className="text-[10px] text-muted-foreground font-black tracking-widest uppercase ml-1">测试助手名称</label>
|
||||
</div>
|
||||
<Input
|
||||
value={selectedTestAssistant.name}
|
||||
onChange={(e) => updateAssistant('name', e.target.value)}
|
||||
value={selectedTest.name}
|
||||
onChange={(e) => updateTest('name', e.target.value)}
|
||||
className="font-bold bg-white/5 border-white/10 focus:border-primary/50 text-base"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => alert("开始自动化测试...")}
|
||||
className="shadow-[0_0_20px_rgba(34,197,94,0.3)] bg-green-500 hover:bg-green-600 text-white font-bold"
|
||||
>
|
||||
<Rocket className="mr-2 h-4 w-4" /> 开始测试
|
||||
<Button onClick={() => alert("开始自动化测试脚本生成...")}>
|
||||
<Zap className="mr-2 h-4 w-4" /> 开始测试
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Scroll Area */}
|
||||
<div className="flex-1 overflow-y-auto p-8 custom-scrollbar">
|
||||
<div className="max-w-4xl mx-auto space-y-8 animate-in slide-in-from-bottom-2 duration-300">
|
||||
|
||||
{/* Basic Config Grid */}
|
||||
<div className="grid grid-cols-2 gap-8">
|
||||
<div className="space-y-3">
|
||||
<label className="text-[10px] font-black text-muted-foreground uppercase tracking-[0.2em] flex items-center">
|
||||
<Zap className="h-3 w-3 mr-2 text-primary" /> 测试类型
|
||||
</label>
|
||||
<div className="flex p-1 bg-white/5 rounded-lg">
|
||||
<button
|
||||
onClick={() => updateAssistant('type', TestType.FIXED)}
|
||||
className={`flex-1 py-1.5 text-xs font-bold rounded-md transition-all ${selectedTestAssistant.type === TestType.FIXED ? 'bg-primary text-primary-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground hover:bg-white/5'}`}
|
||||
>
|
||||
固定流程
|
||||
</button>
|
||||
<button
|
||||
onClick={() => updateAssistant('type', TestType.INTELLIGENT)}
|
||||
className={`flex-1 py-1.5 text-xs font-bold rounded-md transition-all ${selectedTestAssistant.type === TestType.INTELLIGENT ? 'bg-primary text-primary-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground hover:bg-white/5'}`}
|
||||
>
|
||||
智能测试
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<label className="text-[10px] font-black text-muted-foreground uppercase tracking-[0.2em] flex items-center">
|
||||
<ListFilter className="h-3 w-3 mr-2 text-primary" /> 对话方式
|
||||
</label>
|
||||
<div className="flex p-1 bg-white/5 rounded-lg">
|
||||
<button
|
||||
onClick={() => updateAssistant('method', TestMethod.TEXT)}
|
||||
className={`flex-1 py-1.5 text-xs font-bold rounded-md transition-all ${selectedTestAssistant.method === TestMethod.TEXT ? 'bg-primary text-primary-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground hover:bg-white/5'}`}
|
||||
>
|
||||
文本
|
||||
</button>
|
||||
<button
|
||||
onClick={() => updateAssistant('method', TestMethod.AUDIO)}
|
||||
className={`flex-1 py-1.5 text-xs font-bold rounded-md transition-all ${selectedTestAssistant.method === TestMethod.AUDIO ? 'bg-primary text-primary-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground hover:bg-white/5'}`}
|
||||
>
|
||||
音频
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<label className="text-[10px] font-black text-muted-foreground uppercase tracking-[0.2em]">待测试小助手 (TARGET ASSISTANT)</label>
|
||||
<div className="max-w-3xl mx-auto space-y-8 animate-in slide-in-from-bottom-2">
|
||||
{/* Basic Config */}
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-white">目标小助手</label>
|
||||
<select
|
||||
className="flex h-10 w-full rounded-lg 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 [&>option]:bg-card text-foreground"
|
||||
value={selectedTestAssistant.targetAssistantId}
|
||||
onChange={(e) => updateAssistant('targetAssistantId', e.target.value)}
|
||||
className="flex h-10 w-full rounded-md border border-white/10 bg-white/5 px-3 py-1 text-sm text-foreground focus:ring-1 focus:ring-primary/50 [&>option]:bg-card"
|
||||
value={selectedTest.targetAssistantId}
|
||||
onChange={(e) => updateTest('targetAssistantId', e.target.value)}
|
||||
>
|
||||
{mockAssistants.map(a => (
|
||||
<option key={a.id} value={a.id}>{a.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-white">测试方式</label>
|
||||
<div className="flex bg-white/5 p-1 rounded-lg">
|
||||
<button
|
||||
onClick={() => updateTest('method', TestMethod.TEXT)}
|
||||
className={`flex-1 flex items-center justify-center py-1.5 text-xs rounded-md transition-all ${selectedTest.method === TestMethod.TEXT ? 'bg-primary text-primary-foreground' : 'text-muted-foreground hover:bg-white/5'}`}
|
||||
>
|
||||
<MessageSquare className="w-3 h-3 mr-1.5" /> 文本
|
||||
</button>
|
||||
<button
|
||||
onClick={() => updateTest('method', TestMethod.AUDIO)}
|
||||
className={`flex-1 flex items-center justify-center py-1.5 text-xs rounded-md transition-all ${selectedTest.method === TestMethod.AUDIO ? 'bg-primary text-primary-foreground' : 'text-muted-foreground hover:bg-white/5'}`}
|
||||
>
|
||||
<Mic className="w-3 h-3 mr-1.5" /> 语音
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Conditional Settings */}
|
||||
<div className="pt-6 border-t border-white/5">
|
||||
{selectedTestAssistant.type === TestType.FIXED ? (
|
||||
<div className="space-y-4 animate-in fade-in duration-300">
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="text-[10px] font-black text-muted-foreground uppercase tracking-[0.2em] flex items-center">
|
||||
<Braces className="h-3 w-3 mr-2 text-primary" /> 固定流程设置 (Steps)
|
||||
</label>
|
||||
<Button variant="outline" size="sm" onClick={handleAddStep} className="h-7 text-[10px]">
|
||||
<Plus className="w-3 h-3 mr-1" /> 添加步骤
|
||||
</Button>
|
||||
{/* Test Logic Type */}
|
||||
<div className="space-y-4">
|
||||
<label className="text-sm font-medium text-white">测试逻辑类型</label>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Card
|
||||
className={`p-4 cursor-pointer transition-all border-2 ${selectedTest.type === TestType.FIXED ? 'border-primary bg-primary/5' : 'border-white/5'}`}
|
||||
onClick={() => updateTest('type', TestType.FIXED)}
|
||||
>
|
||||
<div className="flex items-center space-x-3 mb-2">
|
||||
<Badge variant={selectedTest.type === TestType.FIXED ? 'default' : 'outline'}>固定流程</Badge>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
{selectedTestAssistant.fixedWorkflowSteps.map((step, idx) => (
|
||||
<div key={idx} className="flex items-center gap-3 group">
|
||||
<div className="flex items-center justify-center w-6 h-6 rounded bg-primary/20 text-[10px] font-mono text-primary font-bold shrink-0">
|
||||
{idx + 1}
|
||||
<p className="text-[10px] text-muted-foreground text-white/70">按照预设的消息序列依次发送给目标小助手,验证其回复是否符合预期。</p>
|
||||
</Card>
|
||||
<Card
|
||||
className={`p-4 cursor-pointer transition-all border-2 ${selectedTest.type === TestType.INTELLIGENT ? 'border-primary bg-primary/5' : 'border-white/5'}`}
|
||||
onClick={() => updateTest('type', TestType.INTELLIGENT)}
|
||||
>
|
||||
<div className="flex items-center space-x-3 mb-2">
|
||||
<Badge variant={selectedTest.type === TestType.INTELLIGENT ? 'default' : 'outline'}>智能提示词</Badge>
|
||||
</div>
|
||||
<p className="text-[10px] text-muted-foreground text-white/70">由 AI 扮演用户,根据设定的角色和场景与目标小助手进行动态对话。</p>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content Config */}
|
||||
{selectedTest.type === TestType.FIXED ? (
|
||||
<div className="space-y-4">
|
||||
<label className="text-sm font-medium text-white">对话步骤列表</label>
|
||||
<div className="space-y-2">
|
||||
{selectedTest.fixedWorkflowSteps.map((step, idx) => (
|
||||
<div key={idx} className="flex gap-2 animate-in slide-in-from-left-2" style={{ animationDelay: `${idx * 50}ms` }}>
|
||||
<div className="h-9 w-9 shrink-0 flex items-center justify-center bg-white/5 rounded-lg text-xs font-mono text-muted-foreground">{idx + 1}</div>
|
||||
<Input
|
||||
value={step}
|
||||
onChange={(e) => updateStep(idx, e.target.value)}
|
||||
placeholder={`步骤 ${idx + 1} 的测试输入...`}
|
||||
className="bg-white/5"
|
||||
onChange={(e) => {
|
||||
const newSteps = [...selectedTest.fixedWorkflowSteps];
|
||||
newSteps[idx] = e.target.value;
|
||||
updateTest('fixedWorkflowSteps', newSteps);
|
||||
}}
|
||||
placeholder="输入用户消息..."
|
||||
/>
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8 text-muted-foreground hover:text-destructive opacity-0 group-hover:opacity-100" onClick={() => removeStep(idx)}>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
<Button variant="ghost" size="icon" className="shrink-0" onClick={() => {
|
||||
updateTest('fixedWorkflowSteps', selectedTest.fixedWorkflowSteps.filter((_, i) => i !== idx));
|
||||
}}>
|
||||
<X size={14} className="text-destructive" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
{selectedTestAssistant.fixedWorkflowSteps.length === 0 && (
|
||||
<div className="text-center py-8 border border-dashed border-white/5 rounded-xl text-xs text-muted-foreground">
|
||||
点击右上角按钮添加测试步骤
|
||||
</div>
|
||||
)}
|
||||
<Button variant="outline" className="w-full border-dashed" onClick={() => {
|
||||
updateTest('fixedWorkflowSteps', [...selectedTest.fixedWorkflowSteps, '']);
|
||||
}}>
|
||||
<Plus size={14} className="mr-2" /> 添加步骤
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4 animate-in fade-in duration-300">
|
||||
<label className="text-[10px] font-black text-muted-foreground uppercase tracking-[0.2em] flex items-center">
|
||||
<Zap className="h-3 w-3 mr-2 text-primary" /> 智能测试提示词 (System Prompt)
|
||||
</label>
|
||||
<div className="space-y-4">
|
||||
<label className="text-sm font-medium text-white">测试助手人设 (Intelligent Prompt)</label>
|
||||
<textarea
|
||||
className="flex min-h-[250px] w-full rounded-xl border border-white/10 bg-white/5 px-4 py-3 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary/50 resize-y"
|
||||
value={selectedTestAssistant.intelligentPrompt}
|
||||
onChange={(e) => updateAssistant('intelligentPrompt', e.target.value)}
|
||||
placeholder="在此设定测试助手的身份、测试目标和预期行为..."
|
||||
className="w-full h-48 bg-white/5 border border-white/10 rounded-xl p-4 text-sm focus:outline-none focus:ring-1 focus:ring-primary/50 text-white placeholder:text-muted-foreground/30 leading-relaxed"
|
||||
value={selectedTest.intelligentPrompt}
|
||||
onChange={(e) => updateTest('intelligentPrompt', e.target.value)}
|
||||
placeholder="描述扮演的角色背景、性格特点以及本次测试需要达成的目标..."
|
||||
/>
|
||||
<p className="text-[10px] text-muted-foreground leading-relaxed opacity-60">
|
||||
提示:在智能测试模式下,测试助手将作为一名真实的终端用户,基于设定的 Prompt 逻辑与目标助手进行多轮对话测试。
|
||||
</p>
|
||||
<div className="p-3 bg-primary/5 border border-primary/20 rounded-lg text-[10px] text-muted-foreground flex items-start gap-2">
|
||||
<Zap className="w-3.5 h-3.5 text-primary shrink-0 mt-0.5" />
|
||||
<p>智能测试将使用另一个 AI 模型来模拟真实用户,它可以更好地检测目标小助手的异常处理能力和话术柔顺度。</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="flex-1 flex flex-col items-center justify-center text-muted-foreground">
|
||||
<div className="w-16 h-16 rounded-2xl bg-white/5 flex items-center justify-center mb-4">
|
||||
<Zap className="h-8 w-8 opacity-50" />
|
||||
</div>
|
||||
<p className="text-lg font-medium">请选择一个测试助手</p>
|
||||
<p className="text-sm opacity-60">配置自动化测试流程以提高小助手稳定性</p>
|
||||
<p className="text-lg font-medium">请选择或创建一个测试任务</p>
|
||||
<p className="text-sm opacity-60">用于自动化验证小助手的逻辑表现</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Delete Confirmation Dialog */}
|
||||
{/* Delete Confirmation */}
|
||||
<Dialog
|
||||
isOpen={!!deleteId}
|
||||
onClose={() => setDeleteId(null)}
|
||||
isOpen={isDeleteModalOpen}
|
||||
onClose={() => setIsDeleteModalOpen(false)}
|
||||
title="确认删除"
|
||||
footer={
|
||||
<>
|
||||
<Button variant="ghost" onClick={() => setDeleteId(null)}>取消</Button>
|
||||
<Button variant="ghost" onClick={() => setIsDeleteModalOpen(false)}>取消</Button>
|
||||
<Button variant="destructive" onClick={confirmDelete}>确认删除</Button>
|
||||
</>
|
||||
}
|
||||
@@ -315,8 +302,9 @@ export const AutoTestPage: React.FC = () => {
|
||||
<AlertTriangle className="h-6 w-6 text-destructive" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-foreground">
|
||||
您确定要删除此测试助手吗?
|
||||
<p className="text-sm text-white">您确定要删除此测试任务吗?此操作无法撤销。</p>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
测试任务: {testAssistants.find(t => t.id === deleteId)?.name}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { Download, Search, Calendar, Filter } from 'lucide-react';
|
||||
import { Button, Input, TableHeader, TableRow, TableHead, TableCell, Badge } from '../components/UI';
|
||||
@@ -27,7 +28,7 @@ export const CallLogsPage: React.FC = () => {
|
||||
log.startTime,
|
||||
log.duration
|
||||
].join(','));
|
||||
const csvContent = "data:text/csv;charset=utf-8," + [headers.join(','), ...rows].join('\n');
|
||||
const csvContent = "data:text/csv;charset=utf-8," + [headers.join(','), ...headers.join(',')].join('\n');
|
||||
const encodedUri = encodeURI(csvContent);
|
||||
const link = document.createElement("a");
|
||||
link.setAttribute("href", encodedUri);
|
||||
@@ -38,7 +39,7 @@ export const CallLogsPage: React.FC = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6 animate-in fade-in">
|
||||
<div className="space-y-6 animate-in fade-in py-2 pb-10">
|
||||
<div className="flex items-center justify-between">
|
||||
<h1 className="text-2xl font-bold tracking-tight">视频通话记录</h1>
|
||||
<Button variant="outline" onClick={handleExport}>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import { Phone, CheckCircle, Clock, UserCheck, Activity, Filter, ChevronDown, BarChart3, HelpCircle, Mail } from 'lucide-react';
|
||||
import { Phone, CheckCircle, Clock, UserCheck, Activity, Filter, ChevronDown, BarChart3, HelpCircle, Mail, Sparkles, Terminal, Box, Zap, ShieldCheck } from 'lucide-react';
|
||||
import { Card, Button } from '../components/UI';
|
||||
import { mockAssistants, getDashboardStats } from '../services/mockData';
|
||||
|
||||
@@ -13,35 +13,37 @@ export const DashboardPage: React.FC = () => {
|
||||
}, [timeRange, selectedAssistantId]);
|
||||
|
||||
return (
|
||||
<div className="min-h-full flex flex-col justify-center animate-in fade-in py-1">
|
||||
<div className="w-full max-w-5xl mx-auto space-y-4 px-2">
|
||||
<div className="min-h-full flex flex-col animate-in fade-in py-1">
|
||||
<div className="w-full max-w-[1600px] mx-auto space-y-4 px-2 lg:px-6">
|
||||
|
||||
{/* 1. Utility Row (Top Navigation Actions) */}
|
||||
<div className="flex justify-end items-center gap-2 border-b border-white/[0.03] pb-2">
|
||||
<Button variant="ghost" size="sm" className="h-7 px-2.5 text-[10px] font-bold border border-white/5 hover:bg-primary/10 hover:text-primary transition-all">
|
||||
<Button variant="ghost" size="sm" className="h-7 px-2.5 text-[10px] font-bold border border-white/5 hover:bg-primary/10 hover:text-primary transition-all text-white/70">
|
||||
<HelpCircle className="w-3 h-3 mr-1.5 opacity-70" /> 文档
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" className="h-7 px-2.5 text-[10px] font-bold border border-white/5 hover:bg-primary/10 hover:text-primary transition-all">
|
||||
<Button variant="ghost" size="sm" className="h-7 px-2.5 text-[10px] font-bold border border-white/5 hover:bg-primary/10 hover:text-primary transition-all text-white/70">
|
||||
<Mail className="w-3 h-3 mr-1.5 opacity-70" /> 联系方式
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 2. Welcome Row */}
|
||||
<div className="flex flex-col space-y-0.5 text-center md:text-left pt-1">
|
||||
{/* 2. Welcome Header */}
|
||||
<div className="flex flex-col space-y-4 text-center md:text-left pt-1">
|
||||
<div className="space-y-0.5">
|
||||
<h1 className="text-2xl font-bold tracking-tight text-white">
|
||||
欢迎, <span className="text-primary">Admin User</span>
|
||||
</h1>
|
||||
<p className="text-muted-foreground flex items-center justify-center md:justify-start text-[11px]">
|
||||
系统状态:
|
||||
系统当前运行环境:
|
||||
<span className="flex items-center ml-2 text-green-400 text-[10px] font-mono bg-green-400/10 px-1.5 py-0.5 rounded-full border border-green-400/20">
|
||||
<span className="w-1 h-1 rounded-full bg-green-400 animate-pulse mr-1"></span>
|
||||
HEALTHY
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 3. Section Header: Title + Filters aligned perfectly */}
|
||||
<div className="flex flex-col md:flex-row items-center justify-between border-b border-white/5 pb-3 pt-1 gap-3">
|
||||
{/* 3. Section Header: Title + Filters */}
|
||||
<div className="flex flex-col md:flex-row items-center justify-between border-b border-white/5 pb-3 pt-2 gap-3">
|
||||
<div className="flex items-center space-x-2.5">
|
||||
<div className="p-1 bg-primary/10 rounded-lg">
|
||||
<BarChart3 className="h-3.5 w-3.5 text-primary" />
|
||||
@@ -52,9 +54,7 @@ export const DashboardPage: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Filters Group (Aligned Right) */}
|
||||
<div className="flex items-center gap-1.5 bg-black/20 p-0.5 rounded-lg border border-white/5 shadow-inner scale-95 origin-right">
|
||||
{/* Assistant Selector */}
|
||||
<div className="relative group min-w-[130px]">
|
||||
<div className="absolute left-2.5 top-1/2 -translate-y-1/2 pointer-events-none text-muted-foreground group-focus-within:text-primary transition-colors">
|
||||
<Filter className="h-2.5 w-2.5" />
|
||||
@@ -74,7 +74,6 @@ export const DashboardPage: React.FC = () => {
|
||||
|
||||
<div className="h-3 w-px bg-white/10 mx-0.5"></div>
|
||||
|
||||
{/* Time Range Selector */}
|
||||
<div className="flex gap-0.5">
|
||||
{(['week', 'month', 'year'] as const).map((r) => (
|
||||
<button
|
||||
@@ -135,11 +134,73 @@ export const DashboardPage: React.FC = () => {
|
||||
<span className="text-[9px] font-mono text-primary animate-pulse tracking-widest uppercase">Streaming</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-[250px] w-full">
|
||||
<div className="h-[300px] w-full">
|
||||
<SimpleAreaChart data={stats.trend} />
|
||||
</div>
|
||||
</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">
|
||||
<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">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<div className="p-1.5 bg-primary/20 rounded-lg">
|
||||
<Sparkles className="w-4 h-4 text-primary" />
|
||||
</div>
|
||||
<h3 className="text-sm font-bold text-white tracking-wide">关于平台</h3>
|
||||
</div>
|
||||
|
||||
<div className="mb-6">
|
||||
<p className="text-sm text-white/80 leading-relaxed font-medium">
|
||||
AI视频助手是一个领先的多模态智能体管理平台,致力于通过先进的 AI 技术为企业和个人提供高效、低延迟、拟人化的音视频通话解决方案。🚀
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
<div className="space-y-2 group/item">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-lg">🤖</span>
|
||||
<h4 className="text-xs font-bold text-primary uppercase tracking-wider">多模态智能体</h4>
|
||||
</div>
|
||||
<p className="text-[11px] text-muted-foreground leading-relaxed group-hover:text-white/70 transition-colors">
|
||||
支持构建具备文本对话、高保真语音输出以及双向实时视频通话能力的智能助手,覆盖 7x24h 智能客服场景。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 group/item">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-lg">📚</span>
|
||||
<h4 className="text-xs font-bold text-primary uppercase tracking-wider">动态知识检索</h4>
|
||||
</div>
|
||||
<p className="text-[11px] text-muted-foreground leading-relaxed group-hover:text-white/70 transition-colors">
|
||||
深度集成 RAG 技术,允许上传私有 PDF/DOCX 文档,让智能体在通话中基于企业私域知识库提供精准、权威的回复。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 group/item">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-lg">🎙️</span>
|
||||
<h4 className="text-xs font-bold text-primary uppercase tracking-wider">音色库与克隆</h4>
|
||||
</div>
|
||||
<p className="text-[11px] text-muted-foreground leading-relaxed group-hover:text-white/70 transition-colors">
|
||||
集成多家主流 TTS 引擎,支持极致的声音克隆与微调,为您的品牌定制专属的、富有情感表现力的真人音色。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 group/item">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-lg">🛡️</span>
|
||||
<h4 className="text-xs font-bold text-primary uppercase tracking-wider">端到端测试</h4>
|
||||
</div>
|
||||
<p className="text-[11px] text-muted-foreground leading-relaxed group-hover:text-white/70 transition-colors">
|
||||
内置自动化测试助手,可通过固定流程或 AI 智能模拟用户进行压力测试与逻辑验证,确保发布前的服务稳定性。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -169,9 +230,9 @@ const StatCard: React.FC<{ title: string; value: string; icon: React.ReactNode;
|
||||
const SimpleAreaChart: React.FC<{ data: { label: string, value: number }[] }> = ({ data }) => {
|
||||
if (!data || data.length === 0) return null;
|
||||
|
||||
const height = 250;
|
||||
const width = 1000;
|
||||
const padding = 20;
|
||||
const height = 300;
|
||||
const width = 1400;
|
||||
const padding = 30;
|
||||
|
||||
const maxValue = Math.max(...data.map(d => d.value)) * 1.2;
|
||||
const points = data.map((d, i) => {
|
||||
|
||||
128
pages/History.tsx
Normal file
128
pages/History.tsx
Normal file
@@ -0,0 +1,128 @@
|
||||
|
||||
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 { mockCallLogs } from '../services/mockData';
|
||||
|
||||
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 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 handleExport = () => {
|
||||
// Generate CSV content
|
||||
const headers = ['ID', 'Agent', 'Source', 'Status', 'Start Time', 'Duration'];
|
||||
const rows = filteredLogs.map(log => [
|
||||
log.id,
|
||||
log.agentName,
|
||||
log.source,
|
||||
log.status,
|
||||
log.startTime,
|
||||
log.duration
|
||||
].join(','));
|
||||
const csvContent = "data:text/csv;charset=utf-8," + [headers.join(','), ...headers.join(',')].join('\n');
|
||||
const encodedUri = encodeURI(csvContent);
|
||||
const link = document.createElement("a");
|
||||
link.setAttribute("href", encodedUri);
|
||||
link.setAttribute("download", "history_logs.csv");
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
};
|
||||
|
||||
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 variant="outline" onClick={handleExport}>
|
||||
<Download 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">
|
||||
<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"
|
||||
value={sourceFilter}
|
||||
onChange={(e) => setSourceFilter(e.target.value as any)}
|
||||
>
|
||||
<option value="all">所有来源</option>
|
||||
<option value="debug">调试 (Debug)</option>
|
||||
<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={statusFilter}
|
||||
onChange={(e) => setStatusFilter(e.target.value as any)}
|
||||
>
|
||||
<option value="all">所有状态</option>
|
||||
<option value="connected">已接通</option>
|
||||
<option value="missed">未接通</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<Calendar className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||
<Input type="date" className="pl-9 border-0 bg-white/5" />
|
||||
</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>接听状态</TableHead>
|
||||
<TableHead>通话接通时间</TableHead>
|
||||
<TableHead>通话时长</TableHead>
|
||||
</TableRow>
|
||||
</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>
|
||||
<TableCell>
|
||||
<Badge variant="outline">{log.source === 'debug' ? '调试' : '外部'}</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant={log.status === 'connected' ? 'success' : 'warning'}>
|
||||
{log.status === 'connected' ? '已接通' : '未接通'}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-muted-foreground">{log.startTime}</TableCell>
|
||||
<TableCell className="text-muted-foreground">{log.duration}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
{filteredLogs.length === 0 && (
|
||||
<TableRow>
|
||||
<TableCell colSpan={6} className="text-center py-6 text-muted-foreground">暂无记录</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,3 +1,4 @@
|
||||
|
||||
import React, { useState, useRef } from 'react';
|
||||
import { Search, Plus, FileText, Upload, ArrowLeft, CloudUpload, File as FileIcon, X } from 'lucide-react';
|
||||
import { Button, Input, TableHeader, TableRow, TableHead, TableCell, Card, Dialog } from '../components/UI';
|
||||
@@ -10,6 +11,8 @@ export const KnowledgeBasePage: React.FC = () => {
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [kbs, setKbs] = useState(mockKnowledgeBases);
|
||||
const [isUploadOpen, setIsUploadOpen] = useState(false);
|
||||
const [isCreateKbOpen, setIsCreateKbOpen] = useState(false);
|
||||
const [newKbName, setNewKbName] = useState('');
|
||||
|
||||
const filteredKbs = kbs.filter(kb => kb.name.toLowerCase().includes(searchTerm.toLowerCase()));
|
||||
|
||||
@@ -22,34 +25,53 @@ export const KnowledgeBasePage: React.FC = () => {
|
||||
setIsUploadOpen(true);
|
||||
};
|
||||
|
||||
const handleCreateKb = () => {
|
||||
if (!newKbName.trim()) return;
|
||||
|
||||
const newKb: KnowledgeBase = {
|
||||
id: `kb_${Date.now()}`,
|
||||
name: newKbName.trim(),
|
||||
creator: 'Admin User',
|
||||
createdAt: new Date().toISOString().split('T')[0],
|
||||
documents: []
|
||||
};
|
||||
|
||||
setKbs([newKb, ...kbs]);
|
||||
setIsCreateKbOpen(false);
|
||||
setNewKbName('');
|
||||
};
|
||||
|
||||
if (view === 'detail' && selectedKb) {
|
||||
return (
|
||||
<>
|
||||
<div className="py-4 pb-10">
|
||||
<KnowledgeBaseDetail
|
||||
kb={selectedKb}
|
||||
onBack={() => setView('list')}
|
||||
onImport={handleImportClick}
|
||||
/>
|
||||
<UploadModal isOpen={isUploadOpen} onClose={() => setIsUploadOpen(false)} />
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6 animate-in fade-in">
|
||||
<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">知识库</h1>
|
||||
<h1 className="text-2xl font-bold tracking-tight text-white">知识库</h1>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2 bg-card/50 p-2 rounded-lg border border-white/5 shadow-sm w-full md:w-1/3">
|
||||
<Search className="h-4 w-4 text-muted-foreground ml-2" />
|
||||
{/* Search Bar - Layout aligned with History Page and width filled */}
|
||||
<div className="bg-card/50 p-4 rounded-lg border border-white/5 shadow-sm">
|
||||
<div className="relative w-full">
|
||||
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="搜索知识库名称..."
|
||||
className="border-0 shadow-none bg-transparent focus-visible:ring-0"
|
||||
className="pl-9 border-0 bg-white/5 w-full"
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{filteredKbs.map(kb => (
|
||||
@@ -63,7 +85,7 @@ export const KnowledgeBasePage: React.FC = () => {
|
||||
<FileText className="h-6 w-6" />
|
||||
</div>
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold group-hover:text-primary transition-colors">{kb.name}</h3>
|
||||
<h3 className="text-lg font-semibold group-hover:text-primary transition-colors text-white">{kb.name}</h3>
|
||||
<div className="mt-4 space-y-1 text-sm text-muted-foreground">
|
||||
<p>文档数量: {kb.documents.length}</p>
|
||||
<p>创建人: {kb.creator}</p>
|
||||
@@ -73,12 +95,44 @@ export const KnowledgeBasePage: React.FC = () => {
|
||||
</Card>
|
||||
))}
|
||||
|
||||
{/* Add New Placeholer */}
|
||||
<div className="border border-dashed border-white/10 rounded-xl p-6 flex flex-col items-center justify-center text-muted-foreground hover:bg-white/5 hover:border-primary/30 transition-all cursor-pointer min-h-[200px]">
|
||||
{/* Add New Placeholder */}
|
||||
<div
|
||||
onClick={() => setIsCreateKbOpen(true)}
|
||||
className="border border-dashed border-white/10 rounded-xl p-6 flex flex-col items-center justify-center text-muted-foreground hover:bg-white/5 hover:border-primary/30 transition-all cursor-pointer min-h-[200px]"
|
||||
>
|
||||
<Plus className="h-8 w-8 mb-2 opacity-50" />
|
||||
<span>新建知识库</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* New Knowledge Base Dialog */}
|
||||
<Dialog
|
||||
isOpen={isCreateKbOpen}
|
||||
onClose={() => setIsCreateKbOpen(false)}
|
||||
title="新建知识库"
|
||||
footer={
|
||||
<>
|
||||
<Button variant="ghost" onClick={() => setIsCreateKbOpen(false)}>取消</Button>
|
||||
<Button onClick={handleCreateKb} disabled={!newKbName.trim()}>确认创建</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>
|
||||
<Input
|
||||
value={newKbName}
|
||||
onChange={(e) => setNewKbName(e.target.value)}
|
||||
placeholder="请输入知识库名称..."
|
||||
autoFocus
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleCreateKb()}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
知识库用于存储私域文档,AI 小助手在回答问题时会优先检索绑定的知识库内容。
|
||||
</p>
|
||||
</div>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -99,7 +153,7 @@ const KnowledgeBaseDetail: React.FC<{
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
</Button>
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold">{kb.name}</h1>
|
||||
<h1 className="text-2xl font-bold text-white">{kb.name}</h1>
|
||||
<p className="text-sm text-muted-foreground">创建于 {kb.createdAt} · by {kb.creator}</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -110,7 +164,7 @@ const KnowledgeBaseDetail: React.FC<{
|
||||
|
||||
<Card className="overflow-hidden border-white/5">
|
||||
<div className="p-4 border-b border-white/5 flex justify-between items-center bg-white/5">
|
||||
<h3 className="font-medium">文档列表</h3>
|
||||
<h3 className="font-medium text-white">文档列表</h3>
|
||||
<div className="w-64">
|
||||
<Input
|
||||
placeholder="搜索文档..."
|
||||
@@ -132,21 +186,18 @@ const KnowledgeBaseDetail: React.FC<{
|
||||
<tbody>
|
||||
{filteredDocs.length > 0 ? filteredDocs.map(doc => (
|
||||
<TableRow key={doc.id}>
|
||||
<TableCell className="font-medium flex items-center">
|
||||
<TableCell className="font-medium flex items-center text-white">
|
||||
<FileText className="h-4 w-4 mr-2 text-primary"/> {doc.name}
|
||||
</TableCell>
|
||||
<TableCell>{doc.size}</TableCell>
|
||||
<TableCell>{doc.uploadDate}</TableCell>
|
||||
<TableCell className="text-muted-foreground">{doc.size}</TableCell>
|
||||
<TableCell className="text-muted-foreground">{doc.uploadDate}</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<Button variant="ghost" size="sm" className="text-destructive hover:text-destructive/80">删除</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)) : (
|
||||
<TableRow>
|
||||
<TableCell className="text-center py-8 text-muted-foreground">暂无文档</TableCell>
|
||||
<TableCell> </TableCell>
|
||||
<TableCell> </TableCell>
|
||||
<TableCell> </TableCell>
|
||||
<TableCell colSpan={4} className="text-center py-8 text-muted-foreground">暂无文档</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</tbody>
|
||||
@@ -176,7 +227,6 @@ const UploadModal: React.FC<{ isOpen: boolean; onClose: () => void }> = ({ isOpe
|
||||
e.stopPropagation();
|
||||
setDragActive(false);
|
||||
if (e.dataTransfer.files && e.dataTransfer.files[0]) {
|
||||
// Add new files to existing state
|
||||
setFiles(prev => [...prev, ...Array.from(e.dataTransfer.files)]);
|
||||
}
|
||||
};
|
||||
@@ -224,7 +274,7 @@ const UploadModal: React.FC<{ isOpen: boolean; onClose: () => void }> = ({ isOpe
|
||||
<p className="text-sm text-muted-foreground text-center">
|
||||
<span className="font-semibold text-primary">点击上传</span> 或将文件拖拽到此处
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground mt-1">支持 PDF, DOCX, TXT (Max 10MB)</p>
|
||||
<p className="text-xs text-muted-foreground mt-1 text-white/50">支持 PDF, DOCX, TXT (Max 10MB)</p>
|
||||
</div>
|
||||
|
||||
{files.length > 0 && (
|
||||
@@ -233,10 +283,10 @@ const UploadModal: React.FC<{ isOpen: boolean; onClose: () => void }> = ({ isOpe
|
||||
<div key={idx} className="flex items-center justify-between p-2 rounded-md bg-white/5 border border-white/5">
|
||||
<div className="flex items-center space-x-2 overflow-hidden">
|
||||
<FileIcon className="h-4 w-4 text-primary shrink-0" />
|
||||
<span className="text-sm truncate max-w-[200px]">{file.name}</span>
|
||||
<span className="text-sm truncate max-w-[200px] text-white">{file.name}</span>
|
||||
<span className="text-xs text-muted-foreground">({(file.size / 1024).toFixed(1)} KB)</span>
|
||||
</div>
|
||||
<button onClick={() => removeFile(idx)} className="text-muted-foreground hover:text-destructive">
|
||||
<button onClick={() => removeFile(idx)} className="text-muted-foreground hover:text-destructive transition-colors">
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -42,9 +42,9 @@ export const VoiceLibraryPage: React.FC = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6 animate-in fade-in">
|
||||
<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">声音库</h1>
|
||||
<h1 className="text-2xl font-bold tracking-tight text-white">声音库</h1>
|
||||
<div className="flex space-x-3">
|
||||
<Button variant="primary" onClick={() => setIsAddModalOpen(true)} className="shadow-[0_0_15px_rgba(6,182,212,0.4)]">
|
||||
<Plus className="mr-2 h-4 w-4" /> 添加声音
|
||||
@@ -69,7 +69,7 @@ export const VoiceLibraryPage: React.FC = () => {
|
||||
<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"
|
||||
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 as any)}
|
||||
>
|
||||
@@ -82,7 +82,7 @@ export const VoiceLibraryPage: React.FC = () => {
|
||||
</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"
|
||||
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={genderFilter}
|
||||
onChange={(e) => setGenderFilter(e.target.value as any)}
|
||||
>
|
||||
@@ -93,7 +93,7 @@ export const VoiceLibraryPage: React.FC = () => {
|
||||
</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"
|
||||
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 as any)}
|
||||
>
|
||||
@@ -120,7 +120,7 @@ export const VoiceLibraryPage: React.FC = () => {
|
||||
<TableRow key={voice.id}>
|
||||
<TableCell className="font-medium">
|
||||
<div className="flex flex-col">
|
||||
<span className="flex items-center">
|
||||
<span className="flex items-center text-white">
|
||||
{voice.vendor === '硅基流动' && <Sparkles className="w-3 h-3 text-primary mr-1.5" />}
|
||||
{voice.name}
|
||||
</span>
|
||||
@@ -130,8 +130,8 @@ export const VoiceLibraryPage: React.FC = () => {
|
||||
<TableCell>
|
||||
<Badge variant={voice.vendor === '硅基流动' ? 'default' : 'outline'}>{voice.vendor}</Badge>
|
||||
</TableCell>
|
||||
<TableCell>{voice.gender === 'Male' ? '男' : '女'}</TableCell>
|
||||
<TableCell>{voice.language === 'zh' ? '中文' : 'English'}</TableCell>
|
||||
<TableCell className="text-muted-foreground">{voice.gender === 'Male' ? '男' : '女'}</TableCell>
|
||||
<TableCell className="text-muted-foreground">{voice.language === 'zh' ? '中文' : 'English'}</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<Button
|
||||
variant="ghost"
|
||||
@@ -177,13 +177,11 @@ const AddVoiceModal: React.FC<{
|
||||
const [vendor, setVendor] = useState<'硅基流动' | 'Ali' | 'Volcano' | 'Minimax'>('硅基流动');
|
||||
const [name, setName] = useState('');
|
||||
|
||||
// SiliconFlow specific state
|
||||
const [sfModel, setSfModel] = useState('fishaudio/fish-speech-1.5');
|
||||
const [sfVoiceId, setSfVoiceId] = useState('fishaudio:amy');
|
||||
const [sfSpeed, setSfSpeed] = useState(1);
|
||||
const [sfGain, setSfGain] = useState(0);
|
||||
|
||||
// Common/Other state
|
||||
const [model, setModel] = useState('');
|
||||
const [voiceKey, setVoiceKey] = useState('');
|
||||
const [gender, setGender] = useState('Female');
|
||||
@@ -196,7 +194,6 @@ const AddVoiceModal: React.FC<{
|
||||
const handleAudition = () => {
|
||||
if (!testInput.trim()) return;
|
||||
setIsAuditioning(true);
|
||||
// Mocking API Call
|
||||
setTimeout(() => setIsAuditioning(false), 2000);
|
||||
};
|
||||
|
||||
@@ -213,7 +210,6 @@ const AddVoiceModal: React.FC<{
|
||||
};
|
||||
|
||||
onSuccess(newVoice);
|
||||
// Reset
|
||||
setName('');
|
||||
setVendor('硅基流动');
|
||||
setDescription('');
|
||||
@@ -232,7 +228,6 @@ const AddVoiceModal: React.FC<{
|
||||
}
|
||||
>
|
||||
<div className="space-y-4 max-h-[75vh] overflow-y-auto px-1 custom-scrollbar">
|
||||
{/* 1. Vendor Selection (Dropdown) */}
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-[10px] font-black text-muted-foreground uppercase tracking-widest block">厂商 (Vendor)</label>
|
||||
<div className="relative">
|
||||
@@ -252,13 +247,11 @@ const AddVoiceModal: React.FC<{
|
||||
|
||||
<div className="h-px bg-white/5"></div>
|
||||
|
||||
{/* 2. Basic Info */}
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-[10px] font-black text-muted-foreground uppercase tracking-widest block">声音名称</label>
|
||||
<Input value={name} onChange={e => setName(e.target.value)} placeholder="例如: 客服小美" />
|
||||
</div>
|
||||
|
||||
{/* 3. Dynamic Parameters based on Vendor */}
|
||||
{vendor === '硅基流动' ? (
|
||||
<div className="space-y-4 animate-in fade-in slide-in-from-top-1 duration-200">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
@@ -347,7 +340,6 @@ const AddVoiceModal: React.FC<{
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Audition Section */}
|
||||
<div className="p-4 rounded-xl border border-primary/20 bg-primary/5 space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="text-[10px] font-black text-primary flex items-center tracking-widest uppercase">
|
||||
@@ -429,7 +421,7 @@ const CloneVoiceModal: React.FC<{
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">语音名称</label>
|
||||
<label className="text-sm font-medium text-white">语音名称</label>
|
||||
<Input
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
@@ -438,7 +430,7 @@ const CloneVoiceModal: React.FC<{
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">上传音频文件 (参考音频)</label>
|
||||
<label className="text-sm font-medium text-white">上传音频文件 (参考音频)</label>
|
||||
<div
|
||||
className="flex flex-col items-center justify-center w-full h-32 rounded-lg border-2 border-dashed border-white/10 bg-white/5 hover:bg-white/10 transition-colors cursor-pointer"
|
||||
onClick={() => inputRef.current?.click()}
|
||||
@@ -465,7 +457,7 @@ const CloneVoiceModal: React.FC<{
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">语音描述</label>
|
||||
<label className="text-sm font-medium text-white">语音描述</label>
|
||||
<textarea
|
||||
className="flex min-h-[80px] w-full rounded-md border-0 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={description}
|
||||
|
||||
@@ -13,7 +13,6 @@ export const WorkflowsPage: React.FC = () => {
|
||||
const [isCreateOpen, setIsCreateOpen] = useState(false);
|
||||
const [activeMenu, setActiveMenu] = useState<string | null>(null);
|
||||
|
||||
// New Workflow State
|
||||
const [newWfName, setNewWfName] = useState('');
|
||||
const [selectedTemplate, setSelectedTemplate] = useState<'blank' | 'lead'>('blank');
|
||||
|
||||
@@ -27,7 +26,6 @@ export const WorkflowsPage: React.FC = () => {
|
||||
return;
|
||||
}
|
||||
setIsCreateOpen(false);
|
||||
// Navigate to the editor with the template name and type as query params
|
||||
navigate(`/workflows/new?name=${encodeURIComponent(newWfName)}&template=${selectedTemplate}`);
|
||||
};
|
||||
|
||||
@@ -39,7 +37,7 @@ export const WorkflowsPage: React.FC = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6 animate-in fade-in">
|
||||
<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>
|
||||
<div className="flex space-x-3">
|
||||
@@ -64,7 +62,7 @@ export const WorkflowsPage: React.FC = () => {
|
||||
</div>
|
||||
<div className="flex items-center space-x-2 bg-white/5 rounded-md px-3 border border-white/10 group focus-within:border-primary/50 transition-colors">
|
||||
<Calendar className="h-4 w-4 text-muted-foreground group-hover:text-primary transition-colors" />
|
||||
<select className="bg-transparent text-sm h-9 focus:outline-none border-none text-foreground cursor-pointer [&>option]:bg-background">
|
||||
<select className="bg-transparent text-sm h-9 focus:outline-none border-none text-foreground cursor-pointer [&>option]:bg-background text-white">
|
||||
<option value="all">所有时间</option>
|
||||
<option value="today">今天</option>
|
||||
<option value="week">近一周</option>
|
||||
@@ -95,7 +93,7 @@ export const WorkflowsPage: React.FC = () => {
|
||||
{wf.name}
|
||||
</button>
|
||||
</TableCell>
|
||||
<TableCell>{wf.nodeCount} 个节点</TableCell>
|
||||
<TableCell className="text-muted-foreground">{wf.nodeCount} 个节点</TableCell>
|
||||
<TableCell className="text-muted-foreground">{wf.createdAt}</TableCell>
|
||||
<TableCell className="text-muted-foreground">{wf.updatedAt}</TableCell>
|
||||
<TableCell className="text-right relative">
|
||||
@@ -109,13 +107,13 @@ export const WorkflowsPage: React.FC = () => {
|
||||
|
||||
{activeMenu === wf.id && (
|
||||
<div className="absolute right-0 top-12 z-50 w-48 bg-background border border-white/10 rounded-lg shadow-xl py-1 animate-in zoom-in-95">
|
||||
<button className="flex items-center w-full px-4 py-2 text-xs hover:bg-white/5 text-left" onClick={() => { alert('JSON copied!'); setActiveMenu(null); }}>
|
||||
<button className="flex items-center w-full px-4 py-2 text-xs hover:bg-white/5 text-left text-white" onClick={() => { alert('JSON copied!'); setActiveMenu(null); }}>
|
||||
<Code className="w-3.5 h-3.5 mr-2 opacity-70" /> 复制 JSON 代码
|
||||
</button>
|
||||
<button className="flex items-center w-full px-4 py-2 text-xs hover:bg-white/5 text-left" onClick={() => navigate(`/workflows/edit/${wf.id}`)}>
|
||||
<button className="flex items-center w-full px-4 py-2 text-xs hover:bg-white/5 text-left text-white" onClick={() => navigate(`/workflows/edit/${wf.id}`)}>
|
||||
<Edit2 className="w-3.5 h-3.5 mr-2 opacity-70" /> 编辑工作流
|
||||
</button>
|
||||
<button className="flex items-center w-full px-4 py-2 text-xs hover:bg-white/5 text-left" onClick={() => setActiveMenu(null)}>
|
||||
<button className="flex items-center w-full px-4 py-2 text-xs hover:bg-white/5 text-left text-white" onClick={() => setActiveMenu(null)}>
|
||||
<Copy className="w-3.5 h-3.5 mr-2 opacity-70" /> 复制
|
||||
</button>
|
||||
<div className="h-px bg-white/10 my-1" />
|
||||
@@ -138,7 +136,6 @@ export const WorkflowsPage: React.FC = () => {
|
||||
|
||||
<UploadJsonModal isOpen={isUploadOpen} onClose={() => setIsUploadOpen(false)} />
|
||||
|
||||
{/* Create Workflow Modal */}
|
||||
<Dialog
|
||||
isOpen={isCreateOpen}
|
||||
onClose={() => setIsCreateOpen(false)}
|
||||
@@ -226,9 +223,9 @@ const UploadJsonModal: React.FC<{ isOpen: boolean; onClose: () => void }> = ({ i
|
||||
<input ref={inputRef} type="file" className="hidden" accept=".json" onChange={e => e.target.files?.[0] && setFile(e.target.files[0])} />
|
||||
<CloudUpload className={`h-10 w-10 mb-3 ${dragActive ? 'text-primary' : 'text-muted-foreground'}`} />
|
||||
<p className="text-sm text-muted-foreground text-center">
|
||||
{file ? <span className="text-primary font-medium">{file.name}</span> : <span><span className="font-semibold text-primary">点击上传</span> 或将 JSON 文件拖拽到此处</span>}
|
||||
{file ? <span className="text-primary font-medium">{file.name}</span> : <span className="text-white/70"><span className="font-semibold text-primary">点击上传</span> 或将 JSON 文件拖拽到此处</span>}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground mt-1">仅支持 .json 格式的工作流配置文件</p>
|
||||
<p className="text-xs text-muted-foreground mt-1 text-white/40">仅支持 .json 格式的工作流配置文件</p>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
|
||||
import { Assistant, CallLog, KnowledgeBase, Voice, Workflow, AutoTestAssistant, TestType, TestMethod } from '../types';
|
||||
|
||||
export const mockVoices: Voice[] = [
|
||||
{ id: 'v1', name: 'Xiaoyun', vendor: 'Ali', gender: 'Female', language: 'zh', description: 'Gentle and professional.' },
|
||||
{ id: 'v2', name: 'Kevin', vendor: 'Volcano', gender: 'Male', language: 'en', description: 'Deep and authoritative.' },
|
||||
{ id: 'v3', name: 'Abby', vendor: 'Minimax', gender: 'Female', language: 'en', description: 'Cheerful and lively.' },
|
||||
{ id: 'v4', name: 'Guang', vendor: 'Ali', gender: 'Male', language: 'zh', description: 'Standard newscast style.' },
|
||||
{ id: 'v5', name: 'Doubao', vendor: 'Volcano', gender: 'Female', language: 'zh', description: 'Cute and young.' },
|
||||
];
|
||||
|
||||
export const mockAssistants: Assistant[] = [
|
||||
{
|
||||
id: '1',
|
||||
@@ -10,9 +18,10 @@ export const mockAssistants: Assistant[] = [
|
||||
prompt: 'You are a helpful customer service agent.',
|
||||
knowledgeBaseId: 'kb1',
|
||||
language: 'en',
|
||||
voice: 'alloy',
|
||||
voice: 'v3',
|
||||
speed: 1.0,
|
||||
hotwords: ['refund', 'order'],
|
||||
interruptionSensitivity: 500,
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
@@ -22,9 +31,10 @@ export const mockAssistants: Assistant[] = [
|
||||
prompt: 'You are an energetic sales representative.',
|
||||
knowledgeBaseId: 'kb2',
|
||||
language: 'zh',
|
||||
voice: 'echo',
|
||||
voice: 'v1',
|
||||
speed: 1.1,
|
||||
hotwords: ['price', 'discount'],
|
||||
interruptionSensitivity: 300,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -121,14 +131,6 @@ export const mockCallLogs: CallLog[] = [
|
||||
},
|
||||
];
|
||||
|
||||
export const mockVoices: Voice[] = [
|
||||
{ id: 'v1', name: 'Xiaoyun', vendor: 'Ali', gender: 'Female', language: 'zh', description: 'Gentle and professional.' },
|
||||
{ id: 'v2', name: 'Kevin', vendor: 'Volcano', gender: 'Male', language: 'en', description: 'Deep and authoritative.' },
|
||||
{ id: 'v3', name: 'Abby', vendor: 'Minimax', gender: 'Female', language: 'en', description: 'Cheerful and lively.' },
|
||||
{ id: 'v4', name: 'Guang', vendor: 'Ali', gender: 'Male', language: 'zh', description: 'Standard newscast style.' },
|
||||
{ id: 'v5', name: 'Doubao', vendor: 'Volcano', gender: 'Female', language: 'zh', description: 'Cute and young.' },
|
||||
];
|
||||
|
||||
export const mockAutoTestAssistants: AutoTestAssistant[] = [
|
||||
{
|
||||
id: 'at1',
|
||||
|
||||
9
types.ts
9
types.ts
@@ -7,10 +7,14 @@ export interface Assistant {
|
||||
prompt: string;
|
||||
knowledgeBaseId: string;
|
||||
language: 'zh' | 'en';
|
||||
voice: string;
|
||||
voice: string; // This will now store the ID of the voice from Voice Library
|
||||
speed: number;
|
||||
hotwords: string[];
|
||||
tools?: string[]; // IDs of enabled tools
|
||||
interruptionSensitivity?: number; // In ms
|
||||
configMode?: 'platform' | 'dify' | 'fastgpt' | 'none';
|
||||
apiUrl?: string;
|
||||
apiKey?: string;
|
||||
}
|
||||
|
||||
export interface Voice {
|
||||
@@ -99,7 +103,8 @@ export interface WorkflowEdge {
|
||||
export enum TabValue {
|
||||
GLOBAL = 'global',
|
||||
VOICE = 'voice',
|
||||
TOOLS = 'tools'
|
||||
TOOLS = 'tools',
|
||||
LINK = 'link'
|
||||
}
|
||||
|
||||
export enum TestType {
|
||||
|
||||
Reference in New Issue
Block a user