Files
ai-videoassistant-frontend/pages/AutoTest.tsx

327 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, ListFilter, Braces, Rocket } 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 [deleteId, setDeleteId] = useState<string | null>(null);
const selectedTestAssistant = testAssistants.find(a => a.id === selectedId) || null;
const filteredTests = testAssistants.filter(a =>
a.name.toLowerCase().includes(searchTerm.toLowerCase())
);
const handleCreate = () => {
const newId = `at_${Date.now()}`;
const newAssistant: AutoTestAssistant = {
id: newId,
name: '新测试助手',
type: TestType.INTELLIGENT,
method: TestMethod.TEXT,
targetAssistantId: mockAssistants[0]?.id || '',
fixedWorkflowSteps: [],
intelligentPrompt: '你是一个普通的测试用户,试图了解产品信息。',
createdAt: new Date().toISOString().replace('T', ' ').substring(0, 16)
};
setTestAssistants([...testAssistants, newAssistant]);
setSelectedId(newId);
};
const handleCopy = (e: React.MouseEvent, assistant: AutoTestAssistant) => {
e.stopPropagation();
const newAssistant = { ...assistant, id: `at_${Date.now()}`, name: `${assistant.name} (Copy)` };
setTestAssistants([...testAssistants, newAssistant]);
};
const handleDeleteClick = (e: React.MouseEvent, id: string) => {
e.stopPropagation();
setDeleteId(id);
};
const confirmDelete = () => {
if (deleteId) {
setTestAssistants(prev => prev.filter(a => a.id !== deleteId));
if (selectedId === deleteId) setSelectedId(null);
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));
}
};
return (
<div className="flex h-[calc(100vh-6rem)] gap-6 animate-in fade-in">
{/* LEFT COLUMN: Test Assistants 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>
<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 justify-between items-start mb-2">
<span className={`font-semibold truncate pr-6 ${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 ? '固定流程' : '智能测试'}
</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>
{/* 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="复制">
<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 font-mono opacity-50">
[ NO TESTERS FOUND ]
</div>
)}
</div>
</div>
{/* RIGHT COLUMN: 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">
<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>
<Input
value={selectedTestAssistant.name}
onChange={(e) => updateAssistant('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>
</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>
<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)}
>
{mockAssistants.map(a => (
<option key={a.id} value={a.id}>{a.name}</option>
))}
</select>
</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>
</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}
</div>
<Input
value={step}
onChange={(e) => updateStep(idx, e.target.value)}
placeholder={`步骤 ${idx + 1} 的测试输入...`}
className="bg-white/5"
/>
<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>
</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>
)}
</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>
<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="在此设定测试助手的身份、测试目标和预期行为..."
/>
<p className="text-[10px] text-muted-foreground leading-relaxed opacity-60">
Prompt
</p>
</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 */}
<Dialog
isOpen={!!deleteId}
onClose={() => setDeleteId(null)}
title="确认删除"
footer={
<>
<Button variant="ghost" onClick={() => setDeleteId(null)}></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-foreground">
</p>
</div>
</div>
</Dialog>
</div>
);
};