Files
AI-VideoAssistant/web/pages/AutoTest.tsx
Xin Wang d96ffdeda4 Add web
2026-02-06 20:43:35 +08:00

315 lines
16 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useState } from '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';
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 filteredTests = testAssistants.filter(t =>
t.name.toLowerCase().includes(searchTerm.toLowerCase())
);
const selectedTest = testAssistants.find(t => t.id === selectedId) || null;
const handleCreate = () => {
const newId = crypto.randomUUID();
const newTest: AutoTestAssistant = {
id: newId,
name: '新测试任务',
type: TestType.FIXED,
method: TestMethod.TEXT,
targetAssistantId: mockAssistants[0]?.id || '',
fixedWorkflowSteps: [],
intelligentPrompt: '',
createdAt: new Date().toISOString().split('T')[0],
};
setTestAssistants([newTest, ...testAssistants]);
setSelectedId(newId);
};
const handleCopy = (e: React.MouseEvent, test: AutoTestAssistant) => {
e.stopPropagation();
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(t => t.id !== deleteId));
if (selectedId === deleteId) setSelectedId(null);
setIsDeleteModalOpen(false);
setDeleteId(null);
}
};
const handleCopyId = (id: string) => {
navigator.clipboard.writeText(id);
setCopySuccess(true);
setTimeout(() => setCopySuccess(false), 2000);
};
return (
<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">
<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="搜索测试..."
className="pl-9 bg-card/50 border-white/5"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
</div>
<Button size="icon" onClick={handleCreate} title="新建测试">
<Plus className="h-5 w-5" />
</Button>
</div>
<div className="flex-1 overflow-y-auto space-y-2 pr-1 custom-scrollbar">
{filteredTests.map(test => (
<div
key={test.id}
onClick={() => setSelectedId(test.id)}
className={`group relative flex flex-col p-4 rounded-xl border transition-all cursor-pointer ${
selectedId === test.id
? 'bg-primary/10 border-primary/40 shadow-[0_0_15px_rgba(6,182,212,0.15)]'
: 'bg-card/30 border-white/5 hover:bg-white/5 hover:border-white/10'
}`}
>
<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 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>
</div>
<div className="text-[10px] text-muted-foreground font-mono opacity-60">
: {test.createdAt}
</div>
{/* 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="删除测试任务">
<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">
</div>
)}
</div>
</div>
{/* 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">
{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">
<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={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("开始自动化测试脚本生成...")}>
<Zap className="mr-2 h-4 w-4" />
</Button>
</div>
</div>
<div className="flex-1 overflow-y-auto p-8 custom-scrollbar">
<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-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>
{/* 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>
<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) => {
const newSteps = [...selectedTest.fixedWorkflowSteps];
newSteps[idx] = e.target.value;
updateTest('fixedWorkflowSteps', newSteps);
}}
placeholder="输入用户消息..."
/>
<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>
))}
<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">
<label className="text-sm font-medium text-white"> (Intelligent Prompt)</label>
<textarea
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="描述扮演的角色背景、性格特点以及本次测试需要达成的目标..."
/>
<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>
</div>
)}
</div>
{/* Delete Confirmation */}
<Dialog
isOpen={isDeleteModalOpen}
onClose={() => setIsDeleteModalOpen(false)}
title="确认删除"
footer={
<>
<Button variant="ghost" onClick={() => setIsDeleteModalOpen(false)}></Button>
<Button variant="destructive" onClick={confirmDelete}></Button>
</>
}
>
<div className="flex items-center space-x-4">
<div className="p-3 bg-destructive/10 rounded-full">
<AlertTriangle className="h-6 w-6 text-destructive" />
</div>
<div>
<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>
</Dialog>
</div>
);
};