+ {/* 6. Platform Feature Intro - Updated Background */}
+
diff --git a/web/pages/History.tsx b/web/pages/History.tsx
index 818d503..51cc056 100644
--- a/web/pages/History.tsx
+++ b/web/pages/History.tsx
@@ -1,29 +1,35 @@
import React, { useState } from 'react';
-import { Download, Search, Calendar, Filter } from 'lucide-react';
-import { Button, Input, TableHeader, TableRow, TableHead, TableCell, Badge } from '../components/UI';
+import { Download, Search, Calendar, Filter, MessageSquare, Mic, Video, Eye, X, Play } from 'lucide-react';
+import { Button, Input, TableHeader, TableRow, TableHead, TableCell, Badge, Drawer } from '../components/UI';
import { mockCallLogs } from '../services/mockData';
+import { CallLog, InteractionType } from '../types';
export const HistoryPage: React.FC = () => {
const [logs] = useState(mockCallLogs);
const [searchTerm, setSearchTerm] = useState('');
const [statusFilter, setStatusFilter] = useState<'all' | 'connected' | 'missed'>('all');
const [sourceFilter, setSourceFilter] = useState<'all' | 'debug' | 'external'>('all');
+ const [typeFilter, setTypeFilter] = useState<'all' | InteractionType>('all');
+
+ const [selectedLog, setSelectedLog] = useState(null);
const filteredLogs = logs.filter(log => {
const matchesSearch = log.agentName.toLowerCase().includes(searchTerm.toLowerCase());
const matchesStatus = statusFilter === 'all' || log.status === statusFilter;
const matchesSource = sourceFilter === 'all' || log.source === sourceFilter;
- return matchesSearch && matchesStatus && matchesSource;
+ const matchesType = typeFilter === 'all' || log.type === typeFilter;
+ return matchesSearch && matchesStatus && matchesSource && matchesType;
});
const handleExport = () => {
// Generate CSV content
- const headers = ['ID', 'Agent', 'Source', 'Status', 'Start Time', 'Duration'];
+ const headers = ['ID', 'Agent', 'Source', 'Type', 'Status', 'Start Time', 'Duration'];
const rows = filteredLogs.map(log => [
log.id,
log.agentName,
- log.source,
+ log.source,
+ log.type,
log.status,
log.startTime,
log.duration
@@ -47,7 +53,7 @@ export const HistoryPage: React.FC = () => {
-
+
{
+
+
+
+
+ {selectedLog && (
+
setSelectedLog(null)}
+ title="历史记录详情"
+ >
+
+
+
+
{selectedLog.agentName}
+ {selectedLog.type} Record
+
+
+
ID: #{selectedLog.id}
+
时间: {selectedLog.startTime}
+
时长: {selectedLog.duration}
+
状态: {selectedLog.status}
+
+
+
+
+ {(selectedLog.details && selectedLog.details.length > 0) ? (
+ selectedLog.details.map((detail, index) => (
+
+
+
+
+ {detail.role === 'user' ? 'User' : 'AI Assistant'}
+
+ {detail.timestamp}
+
+
+ {/* Video Frames */}
+ {selectedLog.type === 'video' && detail.role === 'user' && detail.imageUrls && detail.imageUrls.length > 0 && (
+
+ {detail.imageUrls.map((url, i) => (
+
+

+
+
+
+
+ ))}
+
+ )}
+
+ {/* Content / Transcript */}
+
+ {selectedLog.type !== 'text' && (
+
+
+ {selectedLog.type === 'audio' ? : }
+
+
Transcript
+
+ )}
+ {detail.content}
+
+
+ {/* Audio Player Placeholder for Audio/Video types */}
+ {selectedLog.type !== 'text' && (
+
+ )}
+
+
+ ))
+ ) : (
+
+ )}
+
+
+
+ )}
);
};
diff --git a/web/pages/LLMLibrary.tsx b/web/pages/LLMLibrary.tsx
new file mode 100644
index 0000000..60d8d29
--- /dev/null
+++ b/web/pages/LLMLibrary.tsx
@@ -0,0 +1,248 @@
+
+import React, { useState } from 'react';
+import { Search, Filter, Plus, BrainCircuit, Trash2, Key, Settings2, Server, Thermometer } from 'lucide-react';
+import { Button, Input, TableHeader, TableRow, TableHead, TableCell, Dialog, Badge } from '../components/UI';
+import { mockLLMModels } from '../services/mockData';
+import { LLMModel } from '../types';
+
+export const LLMLibraryPage: React.FC = () => {
+ const [models, setModels] = useState
(mockLLMModels);
+ const [searchTerm, setSearchTerm] = useState('');
+ const [vendorFilter, setVendorFilter] = useState('all');
+ const [typeFilter, setTypeFilter] = useState('all');
+ const [isAddModalOpen, setIsAddModalOpen] = useState(false);
+
+ // Form State
+ const [newModel, setNewModel] = useState>({
+ vendor: 'OpenAI Compatible',
+ type: 'text',
+ temperature: 0.7
+ });
+
+ const filteredModels = models.filter(m => {
+ const matchesSearch = m.name.toLowerCase().includes(searchTerm.toLowerCase());
+ const matchesVendor = vendorFilter === 'all' || m.vendor === vendorFilter;
+ const matchesType = typeFilter === 'all' || m.type === typeFilter;
+ return matchesSearch && matchesVendor && matchesType;
+ });
+
+ const handleAddModel = () => {
+ if (!newModel.name || !newModel.baseUrl || !newModel.apiKey) {
+ alert("请填写完整信息");
+ return;
+ }
+
+ const model: LLMModel = {
+ id: `m_${Date.now()}`,
+ name: newModel.name,
+ vendor: newModel.vendor as string,
+ type: newModel.type as 'text' | 'embedding' | 'rerank',
+ baseUrl: newModel.baseUrl,
+ apiKey: newModel.apiKey,
+ temperature: newModel.type === 'text' ? newModel.temperature : undefined
+ };
+
+ setModels([model, ...models]);
+ setIsAddModalOpen(false);
+ setNewModel({ vendor: 'OpenAI Compatible', type: 'text', temperature: 0.7, name: '', baseUrl: '', apiKey: '' });
+ };
+
+ const handleDeleteModel = (id: string) => {
+ if (confirm('确认删除该模型配置吗?')) {
+ setModels(prev => prev.filter(m => m.id !== id));
+ }
+ };
+
+ return (
+
+
+
大模型库
+
+
+
+
+
+
+ setSearchTerm(e.target.value)}
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 模型名称
+ 厂商
+ 类型
+ Base URL
+ 操作
+
+
+
+ {filteredModels.map(model => (
+
+
+
+ {model.name}
+
+
+ {model.vendor}
+
+
+
+ {model.type.toUpperCase()}
+
+
+
+ {model.baseUrl}
+
+
+
+
+
+ ))}
+ {filteredModels.length === 0 && (
+
+ 暂无模型数据
+
+ )}
+
+
+
+
+
+
+ );
+};
diff --git a/web/pages/ToolLibrary.tsx b/web/pages/ToolLibrary.tsx
new file mode 100644
index 0000000..77c1720
--- /dev/null
+++ b/web/pages/ToolLibrary.tsx
@@ -0,0 +1,191 @@
+
+import React, { useState } from 'react';
+import { Search, Filter, Plus, Wrench, Terminal, Globe, Camera, CameraOff, Image, Images, CloudSun, Calendar, TrendingUp, Coins, Trash2, Edit2, X, Box } from 'lucide-react';
+import { Button, Input, Badge, Dialog } from '../components/UI';
+import { mockTools } from '../services/mockData';
+import { Tool } from '../types';
+
+// Map icon strings to React Nodes
+const iconMap: Record = {
+ Camera: ,
+ CameraOff: ,
+ Image: ,
+ Images: ,
+ CloudSun: ,
+ Calendar: ,
+ TrendingUp: ,
+ Coins: ,
+ Terminal: ,
+ Globe: ,
+ Wrench: ,
+};
+
+export const ToolLibraryPage: React.FC = () => {
+ const [tools, setTools] = useState(mockTools);
+ const [searchTerm, setSearchTerm] = useState('');
+ const [categoryFilter, setCategoryFilter] = useState<'all' | 'system' | 'query'>('all');
+ const [isAddModalOpen, setIsAddModalOpen] = useState(false);
+
+ // New Tool Form
+ const [newToolName, setNewToolName] = useState('');
+ const [newToolDesc, setNewToolDesc] = useState('');
+ const [newToolCategory, setNewToolCategory] = useState<'system' | 'query'>('system');
+
+ const filteredTools = tools.filter(tool => {
+ const matchesSearch = tool.name.toLowerCase().includes(searchTerm.toLowerCase());
+ const matchesCategory = categoryFilter === 'all' || tool.category === categoryFilter;
+ return matchesSearch && matchesCategory;
+ });
+
+ const handleAddTool = () => {
+ if (!newToolName.trim()) return;
+ const newTool: Tool = {
+ id: `custom_${Date.now()}`,
+ name: newToolName,
+ description: newToolDesc,
+ category: newToolCategory,
+ icon: newToolCategory === 'system' ? 'Terminal' : 'Globe',
+ isCustom: true
+ };
+ setTools([...tools, newTool]);
+ setIsAddModalOpen(false);
+ setNewToolName('');
+ setNewToolDesc('');
+ };
+
+ const handleDeleteTool = (e: React.MouseEvent, id: string) => {
+ e.stopPropagation();
+ if (confirm('确认删除该工具吗?')) {
+ setTools(prev => prev.filter(t => t.id !== id));
+ }
+ };
+
+ return (
+
+
+
工具库
+
+
+
+
+
+
+ setSearchTerm(e.target.value)}
+ />
+
+
+
+
+
+
+
+
+ {filteredTools.map(tool => (
+
+
+ {iconMap[tool.icon] || }
+
+
+
+ {tool.name}
+ {tool.isCustom && CUSTOM}
+
+
+
+ {tool.category === 'system' ? 'SYSTEM' : 'QUERY'}
+
+ ID: {tool.id}
+
+
{tool.description}
+
+
+ {tool.isCustom && (
+
+
+
+ )}
+
+ ))}
+ {filteredTools.length === 0 && (
+
+ )}
+
+
+
+
+ );
+};
\ No newline at end of file
diff --git a/web/services/mockData.ts b/web/services/mockData.ts
index b651110..ddeea1b 100644
--- a/web/services/mockData.ts
+++ b/web/services/mockData.ts
@@ -1,5 +1,5 @@
-import { Assistant, CallLog, KnowledgeBase, Voice, Workflow, AutoTestAssistant, TestType, TestMethod } from '../types';
+import { Assistant, CallLog, KnowledgeBase, Voice, Workflow, AutoTestAssistant, TestType, TestMethod, Tool, LLMModel, ASRModel } from '../types';
export const mockVoices: Voice[] = [
{ id: 'v1', name: 'Xiaoyun', vendor: 'Ali', gender: 'Female', language: 'zh', description: 'Gentle and professional.' },
@@ -112,6 +112,13 @@ export const mockCallLogs: CallLog[] = [
startTime: '2023-11-20 10:30:00',
duration: '5m 23s',
agentName: 'Customer Support Bot',
+ type: 'video',
+ details: [
+ { role: 'user', content: 'Can you see this product?', imageUrls: ['https://images.unsplash.com/photo-1523275335684-37898b6baf30?w=150&h=150&fit=crop'], timestamp: '10:30:05' },
+ { role: 'assistant', content: 'Yes, I can see the white watch. It looks like a minimalist design.', timestamp: '10:30:08' },
+ { role: 'user', content: 'How much is it?', imageUrls: [], timestamp: '10:30:15' },
+ { role: 'assistant', content: 'Based on the database, this model retails for $199.', timestamp: '10:30:18' }
+ ]
},
{
id: 'c2',
@@ -120,6 +127,11 @@ export const mockCallLogs: CallLog[] = [
startTime: '2023-11-20 11:15:00',
duration: '1m 10s',
agentName: 'Sales Agent',
+ type: 'audio',
+ details: [
+ { role: 'user', content: 'I am interested in the premium plan.', timestamp: '11:15:02' },
+ { role: 'assistant', content: 'That is a great choice. The premium plan includes...', timestamp: '11:15:05' }
+ ]
},
{
id: 'c3',
@@ -128,7 +140,24 @@ export const mockCallLogs: CallLog[] = [
startTime: '2023-11-20 12:00:00',
duration: '0s',
agentName: 'Customer Support Bot',
+ type: 'text',
+ details: []
},
+ {
+ id: 'c4',
+ source: 'debug',
+ status: 'connected',
+ startTime: '2023-11-21 09:30:00',
+ duration: '45s',
+ agentName: 'Refund Bot',
+ type: 'text',
+ details: [
+ { role: 'user', content: '我想申请退款', timestamp: '09:30:01' },
+ { role: 'assistant', content: '好的,请提供您的订单号。', timestamp: '09:30:02' },
+ { role: 'user', content: 'ORDER-2024-888', timestamp: '09:30:10' },
+ { role: 'assistant', content: '收到,正在为您处理...', timestamp: '09:30:12' }
+ ]
+ }
];
export const mockAutoTestAssistants: AutoTestAssistant[] = [
@@ -154,6 +183,29 @@ export const mockAutoTestAssistants: AutoTestAssistant[] = [
}
];
+export const mockTools: Tool[] = [
+ { id: 'cam_open', name: '打开相机', description: '允许 AI 开启摄像头流', category: 'system', icon: 'Camera' },
+ { id: 'cam_close', name: '关闭相机', description: '允许 AI 停止摄像头流', category: 'system', icon: 'CameraOff' },
+ { id: 'take_photo', name: '拍照', description: 'AI 触发单张拍摄', category: 'system', icon: 'Image' },
+ { id: 'burst_3', name: '连拍三张', description: 'AI 触发快速连拍', category: 'system', icon: 'Images' },
+ { id: 'q_weather', name: '天气查询', description: '查询实时及未来天气', category: 'query', icon: 'CloudSun' },
+ { id: 'q_calendar', name: '日历查询', description: '查询日程及节假日信息', category: 'query', icon: 'Calendar' },
+ { id: 'q_stock', name: '股价查询', description: '查询股票实时行情', category: 'query', icon: 'TrendingUp' },
+ { id: 'q_exchange', name: '汇率查询', description: '查询多国货币汇率', category: 'query', icon: 'Coins' },
+ { id: 'custom_1', name: '智能家居控制', description: '控制灯光、窗帘等设备', category: 'system', icon: 'Terminal', isCustom: true },
+];
+
+export const mockLLMModels: LLMModel[] = [
+ { id: 'm1', name: 'GPT-4o', vendor: 'OpenAI Compatible', type: 'text', baseUrl: 'https://api.openai.com/v1', apiKey: 'sk-***', temperature: 0.7 },
+ { id: 'm2', name: 'DeepSeek-V3', vendor: 'OpenAI Compatible', type: 'text', baseUrl: 'https://api.deepseek.com', apiKey: 'sk-***', temperature: 0.5 },
+ { id: 'm3', name: 'text-embedding-3-small', vendor: 'OpenAI Compatible', type: 'embedding', baseUrl: 'https://api.openai.com/v1', apiKey: 'sk-***' },
+];
+
+export const mockASRModels: ASRModel[] = [
+ { id: 'asr1', name: 'Whisper-1', vendor: 'OpenAI Compatible', language: 'Multi-lingual', baseUrl: 'https://api.openai.com/v1', apiKey: 'sk-***' },
+ { id: 'asr2', name: 'SenseVoiceSmall', vendor: 'OpenAI Compatible', language: 'zh', baseUrl: 'https://api.siliconflow.cn/v1', apiKey: 'sk-***' },
+];
+
export interface DashboardStats {
totalCalls: number;
answerRate: number;
@@ -191,4 +243,4 @@ export const getDashboardStats = (timeRange: 'week' | 'month' | 'year', assistan
humanTransferCount: transfers,
trend
};
-};
+};
\ No newline at end of file
diff --git a/web/types.ts b/web/types.ts
index 4ee4ba2..6142bfd 100644
--- a/web/types.ts
+++ b/web/types.ts
@@ -41,6 +41,16 @@ export interface KnowledgeDocument {
uploadDate: string;
}
+export type InteractionType = 'text' | 'audio' | 'video';
+
+export interface InteractionDetail {
+ role: 'user' | 'assistant';
+ content: string; // Text content or transcript
+ audioUrl?: string; // Placeholder for audio url
+ imageUrls?: string[]; // For video frames
+ timestamp: string;
+}
+
export interface CallLog {
id: string;
source: 'debug' | 'external';
@@ -48,6 +58,8 @@ export interface CallLog {
startTime: string;
duration: string;
agentName: string;
+ type: InteractionType;
+ details?: InteractionDetail[];
}
export interface Workflow {
@@ -127,3 +139,31 @@ export interface AutoTestAssistant {
intelligentPrompt: string;
createdAt: string;
}
+
+export interface Tool {
+ id: string;
+ name: string;
+ description: string;
+ category: 'system' | 'query';
+ icon: string;
+ isCustom?: boolean;
+}
+
+export interface LLMModel {
+ id: string;
+ name: string;
+ vendor: string;
+ type: 'text' | 'embedding' | 'rerank';
+ baseUrl: string;
+ apiKey: string;
+ temperature?: number;
+}
+
+export interface ASRModel {
+ id: string;
+ name: string;
+ vendor: 'OpenAI Compatible';
+ language: string;
+ baseUrl: string;
+ apiKey: string;
+}
\ No newline at end of file