边栏缩略图,小助手添加工具配置,添加自动测试大类
This commit is contained in:
60
App.tsx
60
App.tsx
@@ -1,7 +1,7 @@
|
||||
|
||||
import React from 'react';
|
||||
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 } from 'lucide-react';
|
||||
import { Bot, Phone, Book, User, LayoutDashboard, Mic2, Video, GitBranch, Zap, PanelLeftClose, PanelLeftOpen } from 'lucide-react';
|
||||
|
||||
import { AssistantsPage } from './pages/Assistants';
|
||||
import { KnowledgeBasePage } from './pages/KnowledgeBase';
|
||||
@@ -11,19 +11,22 @@ import { DashboardPage } from './pages/Dashboard';
|
||||
import { VoiceLibraryPage } from './pages/VoiceLibrary';
|
||||
import { WorkflowsPage } from './pages/Workflows';
|
||||
import { WorkflowEditorPage } from './pages/WorkflowEditor';
|
||||
import { AutoTestPage } from './pages/AutoTest';
|
||||
|
||||
const SidebarItem: React.FC<{ to: string; icon: React.ReactNode; label: string; active: boolean }> = ({ to, icon, label, active }) => (
|
||||
const SidebarItem: React.FC<{ to: string; icon: React.ReactNode; label: string; active: boolean; isCollapsed: boolean }> = ({ to, icon, label, active, isCollapsed }) => (
|
||||
<Link
|
||||
to={to}
|
||||
className={`flex items-center space-x-3 px-4 py-3 rounded-md transition-all duration-200 ${active ? 'bg-primary/20 text-primary border-r-2 border-primary' : 'text-muted-foreground hover:bg-muted/50 hover:text-foreground'}`}
|
||||
title={isCollapsed ? label : undefined}
|
||||
className={`flex items-center space-x-3 px-4 py-3 rounded-md transition-all duration-300 ${active ? 'bg-primary/20 text-primary border-r-2 border-primary' : 'text-muted-foreground hover:bg-muted/50 hover:text-foreground'} ${isCollapsed ? 'justify-center space-x-0 px-2' : ''}`}
|
||||
>
|
||||
{icon}
|
||||
<span className="font-medium text-sm">{label}</span>
|
||||
<div className="shrink-0">{icon}</div>
|
||||
{!isCollapsed && <span className="font-medium text-sm animate-in fade-in duration-300 whitespace-nowrap overflow-hidden">{label}</span>}
|
||||
</Link>
|
||||
);
|
||||
|
||||
const AppLayout: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
const location = useLocation();
|
||||
const [isCollapsed, setIsCollapsed] = useState(false);
|
||||
|
||||
const navItems = [
|
||||
{ path: '/', label: '首页', icon: <LayoutDashboard className="h-5 w-5" /> },
|
||||
@@ -32,46 +35,66 @@ const AppLayout: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
{ path: '/call-logs', label: '视频通话记录', icon: <Phone 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: '/profile', label: '个人中心', icon: <User className="h-5 w-5" /> },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="flex h-screen overflow-hidden">
|
||||
{/* Sidebar with Glass effect */}
|
||||
<aside className="w-64 border-r border-border/40 bg-card/30 backdrop-blur-md hidden md:flex flex-col">
|
||||
<div className="p-6 flex items-center space-x-3 border-b border-border/40 overflow-hidden">
|
||||
{/* Sidebar with Glass effect and collapse logic */}
|
||||
<aside
|
||||
className={`border-r border-border/40 bg-card/30 backdrop-blur-md hidden md:flex flex-col transition-all duration-300 ease-in-out relative group ${isCollapsed ? 'w-20' : 'w-64'}`}
|
||||
>
|
||||
<div className={`p-6 flex items-center border-b border-border/40 overflow-hidden transition-all duration-300 ${isCollapsed ? 'justify-center px-4' : 'space-x-3'}`}>
|
||||
<div className="h-10 w-10 shrink-0 bg-gradient-to-br from-cyan-400 to-blue-600 rounded-xl flex items-center justify-center shadow-[0_0_20px_rgba(6,182,212,0.5)] border border-white/10">
|
||||
<Video className="h-6 w-6 text-white drop-shadow-md" />
|
||||
</div>
|
||||
<span className="text-lg font-bold tracking-wide whitespace-nowrap bg-clip-text text-transparent bg-gradient-to-r from-white to-white/80">
|
||||
AI VideoAssistant
|
||||
</span>
|
||||
{!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
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<nav className="flex-1 p-4 space-y-2">
|
||||
|
||||
<nav className="flex-1 p-4 space-y-2 overflow-y-auto overflow-x-hidden custom-scrollbar">
|
||||
{navItems.map(item => (
|
||||
<SidebarItem
|
||||
key={item.path}
|
||||
to={item.path}
|
||||
icon={item.icon}
|
||||
label={item.label}
|
||||
isCollapsed={isCollapsed}
|
||||
active={item.path === '/' ? location.pathname === '/' : location.pathname.startsWith(item.path)}
|
||||
/>
|
||||
))}
|
||||
</nav>
|
||||
<div className="p-4 border-t border-border/40 text-xs text-muted-foreground text-center font-mono">
|
||||
SYSTEM v2.0
|
||||
|
||||
{/* Footer with Version and Collapse Button */}
|
||||
<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
|
||||
</span>
|
||||
)}
|
||||
<button
|
||||
onClick={() => setIsCollapsed(!isCollapsed)}
|
||||
title={isCollapsed ? "展开边栏" : "收起边栏"}
|
||||
className={`p-1.5 rounded-md hover:bg-white/5 text-muted-foreground hover:text-primary transition-all ${isCollapsed ? '' : 'ml-2'}`}
|
||||
>
|
||||
{isCollapsed ? <PanelLeftOpen className="h-4 w-4" /> : <PanelLeftClose className="h-4 w-4" />}
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{/* Main Content */}
|
||||
<main className="flex-1 flex flex-col overflow-hidden relative">
|
||||
<main className="flex-1 flex flex-col overflow-hidden relative bg-background">
|
||||
<header className="h-16 border-b border-border/40 flex items-center px-6 bg-card/30 backdrop-blur-md md:hidden space-x-3">
|
||||
<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">AI VideoAssistant</span>
|
||||
<span className="font-bold text-lg whitespace-nowrap text-white">AI VideoAssistant</span>
|
||||
</header>
|
||||
<div className="flex-1 overflow-auto p-4 md:p-6">
|
||||
<div className="flex-1 overflow-auto p-4 md:p-6 transition-all duration-300">
|
||||
{children}
|
||||
</div>
|
||||
</main>
|
||||
@@ -92,6 +115,7 @@ const App: React.FC = () => {
|
||||
<Route path="/workflows" element={<WorkflowsPage />} />
|
||||
<Route path="/workflows/new" element={<WorkflowEditorPage />} />
|
||||
<Route path="/workflows/edit/:id" element={<WorkflowEditorPage />} />
|
||||
<Route path="/auto-test" element={<AutoTestPage />} />
|
||||
<Route path="/profile" element={<ProfilePage />} />
|
||||
</Routes>
|
||||
</AppLayout>
|
||||
|
||||
@@ -1,11 +1,20 @@
|
||||
|
||||
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 } 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 } from 'lucide-react';
|
||||
import { Button, Input, Card, Badge, Drawer, Dialog } from '../components/UI';
|
||||
import { mockAssistants, mockKnowledgeBases } from '../services/mockData';
|
||||
import { Assistant, TabValue } from '../types';
|
||||
import { GoogleGenAI } from "@google/genai";
|
||||
|
||||
interface ToolItem {
|
||||
id: string;
|
||||
name: string;
|
||||
icon: React.ReactNode;
|
||||
desc: string;
|
||||
category: 'system' | 'query';
|
||||
isCustom?: boolean;
|
||||
}
|
||||
|
||||
export const AssistantsPage: React.FC = () => {
|
||||
const [assistants, setAssistants] = useState<Assistant[]>(mockAssistants);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
@@ -14,6 +23,16 @@ export const AssistantsPage: React.FC = () => {
|
||||
const [debugOpen, setDebugOpen] = useState(false);
|
||||
const [hotwordInput, setHotwordInput] = useState('');
|
||||
|
||||
// Custom Tools State
|
||||
const [customTools, setCustomTools] = useState<ToolItem[]>([]);
|
||||
const [hiddenToolIds, setHiddenToolIds] = useState<string[]>([]); // Track deleted/hidden base tools
|
||||
const [isAddToolModalOpen, setIsAddToolModalOpen] = useState(false);
|
||||
const [addingToCategory, setAddingToCategory] = useState<'system' | 'query'>('system');
|
||||
|
||||
// New Tool Form State
|
||||
const [newToolName, setNewToolName] = useState('');
|
||||
const [newToolDesc, setNewToolDesc] = useState('');
|
||||
|
||||
// State for delete confirmation dialog
|
||||
const [deleteId, setDeleteId] = useState<string | null>(null);
|
||||
|
||||
@@ -35,7 +54,8 @@ export const AssistantsPage: React.FC = () => {
|
||||
language: 'zh',
|
||||
voice: 'default',
|
||||
speed: 1,
|
||||
hotwords: []
|
||||
hotwords: [],
|
||||
tools: []
|
||||
};
|
||||
setAssistants([...assistants, newAssistant]);
|
||||
setSelectedId(newId);
|
||||
@@ -65,6 +85,31 @@ export const AssistantsPage: React.FC = () => {
|
||||
setAssistants(prev => prev.map(a => a.id === selectedId ? { ...a, [field]: value } : a));
|
||||
};
|
||||
|
||||
const toggleTool = (toolId: string) => {
|
||||
if (!selectedAssistant) return;
|
||||
const currentTools = selectedAssistant.tools || [];
|
||||
const newTools = currentTools.includes(toolId)
|
||||
? currentTools.filter(id => id !== toolId)
|
||||
: [...currentTools, toolId];
|
||||
updateAssistant('tools', newTools);
|
||||
};
|
||||
|
||||
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 {
|
||||
setHiddenToolIds(prev => [...prev, toolId]);
|
||||
}
|
||||
};
|
||||
|
||||
const addHotword = () => {
|
||||
if (hotwordInput.trim() && selectedAssistant) {
|
||||
updateAssistant('hotwords', [...selectedAssistant.hotwords, hotwordInput.trim()]);
|
||||
@@ -78,12 +123,52 @@ export const AssistantsPage: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddCustomTool = () => {
|
||||
if (!newToolName.trim()) return;
|
||||
const newTool: ToolItem = {
|
||||
id: `custom_${Date.now()}`,
|
||||
name: newToolName,
|
||||
desc: newToolDesc,
|
||||
category: addingToCategory,
|
||||
icon: addingToCategory === 'system' ? <Terminal className="w-4 h-4" /> : <Globe className="w-4 h-4" />,
|
||||
isCustom: true
|
||||
};
|
||||
setCustomTools([...customTools, newTool]);
|
||||
setIsAddToolModalOpen(false);
|
||||
setNewToolName('');
|
||||
setNewToolDesc('');
|
||||
};
|
||||
|
||||
const openAddToolModal = (e: React.MouseEvent, cat: 'system' | 'query') => {
|
||||
e.stopPropagation();
|
||||
setAddingToCategory(cat);
|
||||
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' },
|
||||
{ id: 'take_photo', name: '拍照', icon: <Image className="w-4 h-4" />, desc: 'AI 触发单张拍摄', category: 'system' },
|
||||
{ id: 'burst_3', name: '连拍三张', icon: <Images className="w-4 h-4" />, desc: 'AI 触发快速连拍', category: 'system' },
|
||||
];
|
||||
|
||||
const baseQueryTools: ToolItem[] = [
|
||||
{ id: 'q_weather', name: '天气查询', icon: <CloudSun className="w-4 h-4" />, desc: '查询实时及未来天气', category: 'query' },
|
||||
{ id: 'q_calendar', name: '日历查询', icon: <Calendar className="w-4 h-4" />, desc: '查询日程及节假日信息', category: 'query' },
|
||||
{ id: 'q_stock', name: '股价查询', icon: <TrendingUp className="w-4 h-4" />, desc: '查询股票实时行情', category: 'query' },
|
||||
{ id: 'q_exchange', name: '汇率查询', icon: <Coins className="w-4 h-4" />, desc: '查询多国货币汇率', category: 'query' },
|
||||
];
|
||||
|
||||
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));
|
||||
|
||||
return (
|
||||
<div className="flex h-[calc(100vh-6rem)] gap-6 animate-in fade-in">
|
||||
{/* LEFT COLUMN: 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>
|
||||
<h2 className="text-xl font-bold tracking-tight text-white">小助手列表</h2>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
@@ -148,10 +233,10 @@ export const AssistantsPage: React.FC = () => {
|
||||
<>
|
||||
{/* Header Area */}
|
||||
<div className="p-6 border-b border-white/5 bg-white/[0.02] space-y-4">
|
||||
{/* Row 1: Name and Actions - Aligned with items-end */}
|
||||
{/* Row 1: Name and Actions */}
|
||||
<div className="flex items-end justify-between gap-4">
|
||||
<div className="flex-1">
|
||||
<label className="text-xs text-muted-foreground font-mono mb-2 block ml-1">ASSISTANT NAME</label>
|
||||
<label className="text-[10px] text-muted-foreground font-black tracking-widest uppercase mb-2 block ml-1">ASSISTANT NAME</label>
|
||||
<Input
|
||||
value={selectedAssistant.name}
|
||||
onChange={(e) => updateAssistant('name', e.target.value)}
|
||||
@@ -189,16 +274,22 @@ export const AssistantsPage: React.FC = () => {
|
||||
>
|
||||
语音配置
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab(TabValue.TOOLS)}
|
||||
className={`px-6 py-1.5 text-sm font-medium rounded-md transition-all ${activeTab === TabValue.TOOLS ? 'bg-primary text-primary-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground hover:bg-white/5'}`}
|
||||
>
|
||||
工具配置
|
||||
</button>
|
||||
</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 ? (
|
||||
{activeTab === TabValue.GLOBAL && (
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-foreground flex items-center">
|
||||
<label className="text-sm font-medium text-white flex items-center">
|
||||
<MessageSquare className="w-4 h-4 mr-2 text-primary"/> 开场白 (Opener)
|
||||
</label>
|
||||
<Input
|
||||
@@ -211,11 +302,11 @@ export const AssistantsPage: React.FC = () => {
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-foreground flex items-center">
|
||||
<label className="text-sm font-medium text-white flex items-center">
|
||||
<BotIcon className="w-4 h-4 mr-2 text-primary"/> 提示词 (Prompt)
|
||||
</label>
|
||||
<textarea
|
||||
className="flex min-h-[200px] 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"
|
||||
className="flex min-h-[200px] 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 text-white"
|
||||
value={selectedAssistant.prompt}
|
||||
onChange={(e) => updateAssistant('prompt', e.target.value)}
|
||||
placeholder="设定小助手的人设、语气、行为规范以及业务逻辑..."
|
||||
@@ -223,7 +314,7 @@ export const AssistantsPage: React.FC = () => {
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-foreground">知识库绑定</label>
|
||||
<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 shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary/50 [&>option]:bg-card text-foreground"
|
||||
value={selectedAssistant.knowledgeBaseId}
|
||||
@@ -236,11 +327,13 @@ export const AssistantsPage: React.FC = () => {
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
)}
|
||||
|
||||
{activeTab === TabValue.VOICE && (
|
||||
<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-foreground">语言 (Language)</label>
|
||||
<label className="text-sm font-medium text-white">语言 (Language)</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"
|
||||
value={selectedAssistant.language}
|
||||
@@ -251,7 +344,7 @@ export const AssistantsPage: React.FC = () => {
|
||||
</select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-foreground">音色 (Voice)</label>
|
||||
<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"
|
||||
value={selectedAssistant.voice}
|
||||
@@ -270,7 +363,7 @@ export const AssistantsPage: React.FC = () => {
|
||||
|
||||
<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-foreground">语速 (Speed)</label>
|
||||
<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>
|
||||
<input
|
||||
@@ -290,7 +383,7 @@ export const AssistantsPage: React.FC = () => {
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<label className="text-sm font-medium text-foreground flex items-center">
|
||||
<label className="text-sm font-medium text-white flex items-center">
|
||||
<Mic className="w-4 h-4 mr-2 text-primary"/> ASR 热词优化 (Hotwords)
|
||||
</label>
|
||||
<div className="flex space-x-2">
|
||||
@@ -318,6 +411,102 @@ export const AssistantsPage: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === TabValue.TOOLS && (
|
||||
<div className="space-y-8 animate-in fade-in">
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-[10px] font-black flex items-center text-primary uppercase tracking-[0.2em]">
|
||||
<Wrench className="w-3.5 h-3.5 mr-2" /> 系统指令
|
||||
</h3>
|
||||
<button
|
||||
onClick={(e) => openAddToolModal(e, 'system')}
|
||||
className="p-1 rounded-full bg-primary/10 text-primary hover:bg-primary/20 transition-colors shadow-sm"
|
||||
>
|
||||
<Plus className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
{systemTools.map(tool => (
|
||||
<div
|
||||
key={tool.id}
|
||||
onClick={() => toggleTool(tool.id)}
|
||||
className={`p-4 rounded-xl border transition-all cursor-pointer group relative flex items-start space-x-3 ${selectedAssistant.tools?.includes(tool.id) ? 'bg-primary/10 border-primary/40 shadow-[0_0_15px_rgba(6,182,212,0.1)]' : 'bg-card/30 border-white/5 hover:bg-white/5 hover:border-white/10'}`}
|
||||
>
|
||||
<div className={`p-2 rounded-lg shrink-0 transition-colors ${selectedAssistant.tools?.includes(tool.id) ? 'bg-primary text-primary-foreground' : 'bg-white/5 text-muted-foreground'}`}>
|
||||
{tool.icon}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center justify-between mb-0.5">
|
||||
<span className="text-sm font-bold text-white">{tool.name}</span>
|
||||
<div className={`w-4 h-4 rounded-full border-2 flex items-center justify-center transition-all ${selectedAssistant.tools?.includes(tool.id) ? 'border-primary bg-primary' : 'border-white/10'}`}>
|
||||
{selectedAssistant.tools?.includes(tool.id) && <div className="w-1.5 h-1.5 bg-white rounded-full"></div>}
|
||||
</div>
|
||||
</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"
|
||||
title="删除工具"
|
||||
>
|
||||
<X className="w-3 h-3" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-[10px] font-black flex items-center text-blue-400 uppercase tracking-[0.2em]">
|
||||
<TrendingUp className="w-3.5 h-3.5 mr-2" /> 信息查询
|
||||
</h3>
|
||||
<button
|
||||
onClick={(e) => openAddToolModal(e, 'query')}
|
||||
className="p-1 rounded-full bg-blue-500/10 text-blue-400 hover:bg-blue-500/20 transition-colors shadow-sm"
|
||||
>
|
||||
<Plus className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
{queryTools.map(tool => (
|
||||
<div
|
||||
key={tool.id}
|
||||
onClick={() => toggleTool(tool.id)}
|
||||
className={`p-4 rounded-xl border transition-all cursor-pointer group relative flex items-start space-x-3 ${selectedAssistant.tools?.includes(tool.id) ? 'bg-blue-500/10 border-blue-500/40 shadow-[0_0_15px_rgba(59,130,246,0.1)]' : 'bg-card/30 border-white/5 hover:bg-white/5 hover:border-white/10'}`}
|
||||
>
|
||||
<div className={`p-2 rounded-lg shrink-0 transition-colors ${selectedAssistant.tools?.includes(tool.id) ? 'bg-blue-500 text-white' : 'bg-white/5 text-muted-foreground'}`}>
|
||||
{tool.icon}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center justify-between mb-0.5">
|
||||
<span className="text-sm font-bold text-white">{tool.name}</span>
|
||||
<div className={`w-4 h-4 rounded-full border-2 flex items-center justify-center transition-all ${selectedAssistant.tools?.includes(tool.id) ? 'border-blue-500 bg-blue-500' : 'border-white/10'}`}>
|
||||
{selectedAssistant.tools?.includes(tool.id) && <div className="w-1.5 h-1.5 bg-white rounded-full"></div>}
|
||||
</div>
|
||||
</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"
|
||||
title="删除工具"
|
||||
>
|
||||
<X className="w-3 h-3" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-4 bg-white/5 border border-white/5 rounded-xl text-[10px] text-muted-foreground flex items-center gap-3">
|
||||
<Rocket className="w-4 h-4 text-primary shrink-0" />
|
||||
<span>提示:启用工具后,AI 将能在对话中自动识别并调用相关功能以协助用户。</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
@@ -340,6 +529,44 @@ export const AssistantsPage: React.FC = () => {
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Add Custom Tool Modal */}
|
||||
<Dialog
|
||||
isOpen={isAddToolModalOpen}
|
||||
onClose={() => setIsAddToolModalOpen(false)}
|
||||
title={addingToCategory === 'system' ? '添加自定义系统指令' : '添加自定义信息查询'}
|
||||
footer={
|
||||
<>
|
||||
<Button variant="ghost" onClick={() => setIsAddToolModalOpen(false)}>取消</Button>
|
||||
<Button onClick={handleAddCustomTool}>确认添加</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={newToolName}
|
||||
onChange={e => setNewToolName(e.target.value)}
|
||||
placeholder="例如: 智能家居控制"
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-[10px] font-black text-muted-foreground uppercase tracking-widest block">工具描述 (给 AI 的说明)</label>
|
||||
<textarea
|
||||
className="flex min-h-[100px] w-full rounded-md border border-white/10 bg-white/5 px-3 py-2 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary/50 text-white"
|
||||
value={newToolDesc}
|
||||
onChange={e => setNewToolDesc(e.target.value)}
|
||||
placeholder="描述该工具的功能,以及 AI 应该在什么情况下调用它..."
|
||||
/>
|
||||
</div>
|
||||
<div 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>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
|
||||
{/* Delete Confirmation Dialog */}
|
||||
<Dialog
|
||||
isOpen={!!deleteId}
|
||||
@@ -357,7 +584,7 @@ export const AssistantsPage: React.FC = () => {
|
||||
<AlertTriangle className="h-6 w-6 text-destructive" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-foreground">
|
||||
<p className="text-sm text-foreground text-white">
|
||||
您确定要删除此小助手吗?此操作无法撤销。
|
||||
</p>
|
||||
{deleteId && (
|
||||
|
||||
326
pages/AutoTest.tsx
Normal file
326
pages/AutoTest.tsx
Normal file
@@ -0,0 +1,326 @@
|
||||
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
|
||||
import React, { useState, useRef } from 'react';
|
||||
import { Search, Mic2, Play, Pause, Upload, X, Filter } from 'lucide-react';
|
||||
import { Search, Mic2, Play, Pause, Upload, X, Filter, Plus, Volume2, Sparkles, Wand2, ChevronDown } from 'lucide-react';
|
||||
import { Button, Input, TableHeader, TableRow, TableHead, TableCell, Dialog, Badge } from '../components/UI';
|
||||
import { mockVoices } from '../services/mockData';
|
||||
import { Voice } from '../types';
|
||||
@@ -8,12 +8,13 @@ import { Voice } from '../types';
|
||||
export const VoiceLibraryPage: React.FC = () => {
|
||||
const [voices, setVoices] = useState<Voice[]>(mockVoices);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [vendorFilter, setVendorFilter] = useState<'all' | 'Ali' | 'Volcano' | 'Minimax'>('all');
|
||||
const [vendorFilter, setVendorFilter] = useState<'all' | 'Ali' | 'Volcano' | 'Minimax' | '硅基流动'>('all');
|
||||
const [genderFilter, setGenderFilter] = useState<'all' | 'Male' | 'Female'>('all');
|
||||
const [langFilter, setLangFilter] = useState<'all' | 'zh' | 'en'>('all');
|
||||
|
||||
const [playingVoiceId, setPlayingVoiceId] = useState<string | null>(null);
|
||||
const [isCloneModalOpen, setIsCloneModalOpen] = useState(false);
|
||||
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
|
||||
|
||||
const filteredVoices = voices.filter(voice => {
|
||||
const matchesSearch = voice.name.toLowerCase().includes(searchTerm.toLowerCase());
|
||||
@@ -28,15 +29,15 @@ export const VoiceLibraryPage: React.FC = () => {
|
||||
setPlayingVoiceId(null);
|
||||
} else {
|
||||
setPlayingVoiceId(id);
|
||||
// Mock auto-stop after 3 seconds
|
||||
setTimeout(() => {
|
||||
setPlayingVoiceId((current) => current === id ? null : current);
|
||||
}, 3000);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCloneSuccess = (newVoice: Voice) => {
|
||||
const handleAddSuccess = (newVoice: Voice) => {
|
||||
setVoices([newVoice, ...voices]);
|
||||
setIsAddModalOpen(false);
|
||||
setIsCloneModalOpen(false);
|
||||
};
|
||||
|
||||
@@ -44,9 +45,14 @@ export const VoiceLibraryPage: React.FC = () => {
|
||||
<div className="space-y-6 animate-in fade-in">
|
||||
<div className="flex items-center justify-between">
|
||||
<h1 className="text-2xl font-bold tracking-tight">声音库</h1>
|
||||
<Button onClick={() => setIsCloneModalOpen(true)}>
|
||||
<Mic2 className="mr-2 h-4 w-4" /> 克隆声音
|
||||
</Button>
|
||||
<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" /> 添加声音
|
||||
</Button>
|
||||
<Button variant="primary" onClick={() => setIsCloneModalOpen(true)} className="shadow-[0_0_15px_rgba(6,182,212,0.4)]">
|
||||
<Mic2 className="mr-2 h-4 w-4" /> 克隆声音
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Filter Bar */}
|
||||
@@ -68,6 +74,7 @@ export const VoiceLibraryPage: React.FC = () => {
|
||||
onChange={(e) => setVendorFilter(e.target.value as any)}
|
||||
>
|
||||
<option value="all">所有厂商</option>
|
||||
<option value="硅基流动">硅基流动 (SiliconFlow)</option>
|
||||
<option value="Ali">阿里 (Ali)</option>
|
||||
<option value="Volcano">火山 (Volcano)</option>
|
||||
<option value="Minimax">Minimax</option>
|
||||
@@ -97,7 +104,7 @@ export const VoiceLibraryPage: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-md border border-white/5 bg-card/40 backdrop-blur-md">
|
||||
<div className="rounded-md border border-white/5 bg-card/40 backdrop-blur-md overflow-hidden">
|
||||
<table className="w-full text-sm">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
@@ -113,12 +120,15 @@ export const VoiceLibraryPage: React.FC = () => {
|
||||
<TableRow key={voice.id}>
|
||||
<TableCell className="font-medium">
|
||||
<div className="flex flex-col">
|
||||
<span>{voice.name}</span>
|
||||
<span className="flex items-center">
|
||||
{voice.vendor === '硅基流动' && <Sparkles className="w-3 h-3 text-primary mr-1.5" />}
|
||||
{voice.name}
|
||||
</span>
|
||||
{voice.description && <span className="text-xs text-muted-foreground">{voice.description}</span>}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant="outline">{voice.vendor}</Badge>
|
||||
<Badge variant={voice.vendor === '硅基流动' ? 'default' : 'outline'}>{voice.vendor}</Badge>
|
||||
</TableCell>
|
||||
<TableCell>{voice.gender === 'Male' ? '男' : '女'}</TableCell>
|
||||
<TableCell>{voice.language === 'zh' ? '中文' : 'English'}</TableCell>
|
||||
@@ -136,26 +146,238 @@ export const VoiceLibraryPage: React.FC = () => {
|
||||
))}
|
||||
{filteredVoices.length === 0 && (
|
||||
<TableRow>
|
||||
<TableCell className="text-center py-6 text-muted-foreground">暂无声音数据</TableCell>
|
||||
<TableCell> </TableCell>
|
||||
<TableCell> </TableCell>
|
||||
<TableCell> </TableCell>
|
||||
<TableCell> </TableCell>
|
||||
<TableCell colSpan={5} className="text-center py-6 text-muted-foreground">暂无声音数据</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<AddVoiceModal
|
||||
isOpen={isAddModalOpen}
|
||||
onClose={() => setIsAddModalOpen(false)}
|
||||
onSuccess={handleAddSuccess}
|
||||
/>
|
||||
|
||||
<CloneVoiceModal
|
||||
isOpen={isCloneModalOpen}
|
||||
onClose={() => setIsCloneModalOpen(false)}
|
||||
onSuccess={handleCloneSuccess}
|
||||
onSuccess={handleAddSuccess}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// --- Unified Add Voice Modal ---
|
||||
const AddVoiceModal: React.FC<{
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onSuccess: (voice: Voice) => void;
|
||||
}> = ({ isOpen, onClose, onSuccess }) => {
|
||||
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');
|
||||
const [language, setLanguage] = useState('zh');
|
||||
const [description, setDescription] = useState('');
|
||||
|
||||
const [testInput, setTestInput] = useState('你好,正在测试语音合成效果。');
|
||||
const [isAuditioning, setIsAuditioning] = useState(false);
|
||||
|
||||
const handleAudition = () => {
|
||||
if (!testInput.trim()) return;
|
||||
setIsAuditioning(true);
|
||||
// Mocking API Call
|
||||
setTimeout(() => setIsAuditioning(false), 2000);
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (!name) { alert("请填写声音显示名称"); return; }
|
||||
|
||||
let newVoice: Voice = {
|
||||
id: `${vendor === '硅基流动' ? 'sf' : 'gen'}-${Date.now()}`,
|
||||
name: name,
|
||||
vendor: vendor,
|
||||
gender: gender,
|
||||
language: language,
|
||||
description: description || (vendor === '硅基流动' ? `Model: ${sfModel}` : `Model: ${model}`)
|
||||
};
|
||||
|
||||
onSuccess(newVoice);
|
||||
// Reset
|
||||
setName('');
|
||||
setVendor('硅基流动');
|
||||
setDescription('');
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
title="添加声音"
|
||||
footer={
|
||||
<>
|
||||
<Button variant="ghost" onClick={onClose}>取消</Button>
|
||||
<Button onClick={handleSubmit} className="bg-primary hover:bg-primary/90">确认添加</Button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<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">
|
||||
<select
|
||||
className="flex h-10 w-full rounded-md border border-white/10 bg-white/5 px-3 py-1 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary/50 text-foreground appearance-none cursor-pointer [&>option]:bg-card"
|
||||
value={vendor}
|
||||
onChange={(e) => setVendor(e.target.value as any)}
|
||||
>
|
||||
<option value="硅基流动">硅基流动 (SiliconFlow)</option>
|
||||
<option value="Ali">阿里 (Ali)</option>
|
||||
<option value="Volcano">火山 (Volcano)</option>
|
||||
<option value="Minimax">Minimax</option>
|
||||
</select>
|
||||
<ChevronDown className="absolute right-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground pointer-events-none" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-[10px] font-black text-muted-foreground uppercase tracking-widest block">模型 (Model)</label>
|
||||
<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 text-foreground [&>option]:bg-card"
|
||||
value={sfModel}
|
||||
onChange={e => setSfModel(e.target.value)}
|
||||
>
|
||||
<option value="fishaudio/fish-speech-1.5">fishaudio/fish-speech-1.5</option>
|
||||
<option value="fishaudio/fish-speech-1.4">fishaudio/fish-speech-1.4</option>
|
||||
<option value="ByteDance/SA-Speech">ByteDance/SA-Speech</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-[10px] font-black text-muted-foreground uppercase tracking-widest block">声音 ID (Voice)</label>
|
||||
<Input value={sfVoiceId} onChange={e => setSfVoiceId(e.target.value)} placeholder="fishaudio:amy" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-[10px] font-black text-muted-foreground uppercase tracking-widest block">语速 (Speed)</label>
|
||||
<div className="flex items-center space-x-2">
|
||||
<input type="range" min="0.5" max="2" step="0.1" value={sfSpeed} onChange={e => setSfSpeed(parseFloat(e.target.value))} className="flex-1 accent-primary" />
|
||||
<span className="text-[10px] font-mono text-primary bg-primary/10 px-1.5 py-0.5 rounded">{sfSpeed}x</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-[10px] font-black text-muted-foreground uppercase tracking-widest block">增益 (Gain)</label>
|
||||
<div className="flex items-center space-x-2">
|
||||
<input type="range" min="-10" max="10" step="1" value={sfGain} onChange={e => setSfGain(parseInt(e.target.value))} className="flex-1 accent-primary" />
|
||||
<span className="text-[10px] font-mono text-primary bg-primary/10 px-1.5 py-0.5 rounded">{sfGain}dB</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4 animate-in fade-in slide-in-from-top-1 duration-200">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-[10px] font-black text-muted-foreground uppercase tracking-widest block">模型标识</label>
|
||||
<Input value={model} onChange={e => setModel(e.target.value)} placeholder="API Model Key" />
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-[10px] font-black text-muted-foreground uppercase tracking-widest block">发音人标识</label>
|
||||
<Input value={voiceKey} onChange={e => setVoiceKey(e.target.value)} placeholder="Voice Key" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-[10px] font-black text-muted-foreground uppercase tracking-widest block">性别</label>
|
||||
<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 text-foreground [&>option]:bg-card"
|
||||
value={gender}
|
||||
onChange={e => setGender(e.target.value)}
|
||||
>
|
||||
<option value="Female">女 (Female)</option>
|
||||
<option value="Male">男 (Male)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-[10px] font-black text-muted-foreground uppercase tracking-widest block">语言</label>
|
||||
<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 text-foreground [&>option]:bg-card"
|
||||
value={language}
|
||||
onChange={e => setLanguage(e.target.value)}
|
||||
>
|
||||
<option value="zh">中文 (Chinese)</option>
|
||||
<option value="en">英文 (English)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-[10px] font-black text-muted-foreground uppercase tracking-widest block">备注</label>
|
||||
<textarea
|
||||
className="flex min-h-[60px] 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}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
placeholder="记录该声音的特点..."
|
||||
/>
|
||||
</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">
|
||||
<Volume2 className="w-3.5 h-3.5 mr-1.5" /> 参数试听 (Preview)
|
||||
</h4>
|
||||
{vendor === '硅基流动' && <Badge variant="outline" className="text-[8px] border-primary/20 text-primary/70">SiliconFlow Audio API</Badge>}
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
value={testInput}
|
||||
onChange={e => setTestInput(e.target.value)}
|
||||
placeholder="输入测试文本..."
|
||||
className="text-xs bg-black/20"
|
||||
/>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
onClick={handleAudition}
|
||||
disabled={isAuditioning}
|
||||
className="shrink-0 h-9"
|
||||
>
|
||||
{isAuditioning ? <Pause className="h-3.5 w-3.5 animate-pulse" /> : <Play className="h-3.5 w-3.5" />}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
const CloneVoiceModal: React.FC<{
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
@@ -178,19 +400,16 @@ const CloneVoiceModal: React.FC<{
|
||||
return;
|
||||
}
|
||||
|
||||
// Mock creation
|
||||
const newVoice: Voice = {
|
||||
id: `v-${Date.now()}`,
|
||||
name: name,
|
||||
vendor: 'Volcano', // Default for cloned voices
|
||||
gender: 'Female', // Mock default
|
||||
vendor: 'Volcano',
|
||||
gender: 'Female',
|
||||
language: 'zh',
|
||||
description: description || 'User cloned voice'
|
||||
};
|
||||
|
||||
onSuccess(newVoice);
|
||||
|
||||
// Reset
|
||||
setName('');
|
||||
setDescription('');
|
||||
setFile(null);
|
||||
@@ -248,7 +467,7 @@ const CloneVoiceModal: React.FC<{
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">语音描述</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"
|
||||
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}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
placeholder="描述声音特点(如:年轻、沉稳...)"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
|
||||
import { Assistant, CallLog, KnowledgeBase, Voice, Workflow } from '../types';
|
||||
import { Assistant, CallLog, KnowledgeBase, Voice, Workflow, AutoTestAssistant, TestType, TestMethod } from '../types';
|
||||
|
||||
export const mockAssistants: Assistant[] = [
|
||||
{
|
||||
@@ -129,6 +129,29 @@ export const mockVoices: Voice[] = [
|
||||
{ id: 'v5', name: 'Doubao', vendor: 'Volcano', gender: 'Female', language: 'zh', description: 'Cute and young.' },
|
||||
];
|
||||
|
||||
export const mockAutoTestAssistants: AutoTestAssistant[] = [
|
||||
{
|
||||
id: 'at1',
|
||||
name: '退款流程压力测试',
|
||||
type: TestType.FIXED,
|
||||
method: TestMethod.TEXT,
|
||||
targetAssistantId: '1',
|
||||
fixedWorkflowSteps: ['你好,我要退款', '订单号是123456', '谢谢'],
|
||||
intelligentPrompt: '',
|
||||
createdAt: '2024-03-10 09:00'
|
||||
},
|
||||
{
|
||||
id: 'at2',
|
||||
name: '愤怒的客户模拟',
|
||||
type: TestType.INTELLIGENT,
|
||||
method: TestMethod.AUDIO,
|
||||
targetAssistantId: '1',
|
||||
fixedWorkflowSteps: [],
|
||||
intelligentPrompt: '你是一个非常愤怒的客户,因为订单延迟了一周。你需要表达你的不满,并要求立即解决。你的语气必须很冲,不接受简单的道歉。',
|
||||
createdAt: '2024-03-11 14:20'
|
||||
}
|
||||
];
|
||||
|
||||
export interface DashboardStats {
|
||||
totalCalls: number;
|
||||
answerRate: number;
|
||||
|
||||
25
types.ts
25
types.ts
@@ -10,6 +10,7 @@ export interface Assistant {
|
||||
voice: string;
|
||||
speed: number;
|
||||
hotwords: string[];
|
||||
tools?: string[]; // IDs of enabled tools
|
||||
}
|
||||
|
||||
export interface Voice {
|
||||
@@ -97,5 +98,27 @@ export interface WorkflowEdge {
|
||||
|
||||
export enum TabValue {
|
||||
GLOBAL = 'global',
|
||||
VOICE = 'voice'
|
||||
VOICE = 'voice',
|
||||
TOOLS = 'tools'
|
||||
}
|
||||
|
||||
export enum TestType {
|
||||
FIXED = 'fixed',
|
||||
INTELLIGENT = 'intelligent'
|
||||
}
|
||||
|
||||
export enum TestMethod {
|
||||
TEXT = 'text',
|
||||
AUDIO = 'audio'
|
||||
}
|
||||
|
||||
export interface AutoTestAssistant {
|
||||
id: string;
|
||||
name: string;
|
||||
type: TestType;
|
||||
method: TestMethod;
|
||||
targetAssistantId: string;
|
||||
fixedWorkflowSteps: string[];
|
||||
intelligentPrompt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user