- {activeTab === TabValue.GLOBAL ? (
+ {activeTab === TabValue.GLOBAL && (
-
-
+
提示词 (Prompt)
- 知识库绑定
+ 知识库绑定
- ) : (
+ )}
+
+ {activeTab === TabValue.VOICE && (
- 语言 (Language)
+ 语言 (Language)
-
音色 (Voice)
+
音色 (Voice)
>
@@ -340,6 +529,44 @@ export const AssistantsPage: React.FC = () => {
/>
)}
+ {/* Add Custom Tool Modal */}
+
+
{/* Delete Confirmation Dialog */}
-
+
您确定要删除此小助手吗?此操作无法撤销。
{deleteId && (
diff --git a/pages/AutoTest.tsx b/pages/AutoTest.tsx
new file mode 100644
index 0000000..3d4fb9d
--- /dev/null
+++ b/pages/AutoTest.tsx
@@ -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
(mockAutoTestAssistants);
+ const [searchTerm, setSearchTerm] = useState('');
+ const [selectedId, setSelectedId] = useState(null);
+ const [deleteId, setDeleteId] = useState(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 (
+
+ {/* LEFT COLUMN: Test Assistants List */}
+
+
+
测试助手列表
+
+
+
+
+
+ setSearchTerm(e.target.value)}
+ />
+
+
+
+
+
+ {filteredTests.map(test => (
+
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'
+ }`}
+ >
+
+
+ {test.name}
+
+
+
+
+
+ {test.type === TestType.FIXED ? '固定流程' : '智能测试'}
+
+
+ {test.method === TestMethod.TEXT ? : }
+ {test.method === TestMethod.TEXT ? '文本' : '音频'}
+
+
+
+ {/* Hover Actions */}
+
+
+
+
+
+ ))}
+ {filteredTests.length === 0 && (
+
+ [ NO TESTERS FOUND ]
+
+ )}
+
+
+
+ {/* RIGHT COLUMN: Config Panel */}
+
+ {selectedTestAssistant ? (
+ <>
+ {/* Header Area */}
+
+
+
+ TESTER NAME
+ updateAssistant('name', e.target.value)}
+ className="font-bold bg-white/5 border-white/10 focus:border-primary/50 text-base"
+ />
+
+
+
+
+
+ {/* Scroll Area */}
+
+
+
+ {/* Basic Config Grid */}
+
+
+
+ 测试类型
+
+
+
+
+
+
+
+
+
+ 对话方式
+
+
+
+
+
+
+
+
+
+ 待测试小助手 (TARGET ASSISTANT)
+
+
+
+ {/* Conditional Settings */}
+
+ {selectedTestAssistant.type === TestType.FIXED ? (
+
+
+
+ 固定流程设置 (Steps)
+
+
+
+
+ {selectedTestAssistant.fixedWorkflowSteps.map((step, idx) => (
+
+
+ {idx + 1}
+
+
updateStep(idx, e.target.value)}
+ placeholder={`步骤 ${idx + 1} 的测试输入...`}
+ className="bg-white/5"
+ />
+
+
+ ))}
+ {selectedTestAssistant.fixedWorkflowSteps.length === 0 && (
+
+ 点击右上角按钮添加测试步骤
+
+ )}
+
+
+ ) : (
+
+
+ 智能测试提示词 (System Prompt)
+
+
+ )}
+
+
+
+
+ >
+ ) : (
+
+
+
+
+
请选择一个测试助手
+
配置自动化测试流程以提高小助手稳定性
+
+ )}
+
+
+ {/* Delete Confirmation Dialog */}
+
+
+ );
+};
diff --git a/pages/VoiceLibrary.tsx b/pages/VoiceLibrary.tsx
index bd658df..3215d67 100644
--- a/pages/VoiceLibrary.tsx
+++ b/pages/VoiceLibrary.tsx
@@ -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(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(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 = () => {
声音库
-
+
+
+
+
{/* Filter Bar */}
@@ -68,6 +74,7 @@ export const VoiceLibraryPage: React.FC = () => {
onChange={(e) => setVendorFilter(e.target.value as any)}
>
+
@@ -97,7 +104,7 @@ export const VoiceLibraryPage: React.FC = () => {
-
+
@@ -113,12 +120,15 @@ export const VoiceLibraryPage: React.FC = () => {
- {voice.name}
+
+ {voice.vendor === '硅基流动' && }
+ {voice.name}
+
{voice.description && {voice.description}}
- {voice.vendor}
+ {voice.vendor}
{voice.gender === 'Male' ? '男' : '女'}
{voice.language === 'zh' ? '中文' : 'English'}
@@ -136,26 +146,238 @@ export const VoiceLibraryPage: React.FC = () => {
))}
{filteredVoices.length === 0 && (
- 暂无声音数据
-
-
-
-
+ 暂无声音数据
)}
+
setIsAddModalOpen(false)}
+ onSuccess={handleAddSuccess}
+ />
+
setIsCloneModalOpen(false)}
- onSuccess={handleCloneSuccess}
+ onSuccess={handleAddSuccess}
/>
);
};
+// --- 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 (
+
+ );
+};
+
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<{
语音描述