247 lines
14 KiB
TypeScript
247 lines
14 KiB
TypeScript
|
|
import React, { useState } from 'react';
|
|
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<CallLog | null>(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;
|
|
const matchesType = typeFilter === 'all' || log.type === typeFilter;
|
|
return matchesSearch && matchesStatus && matchesSource && matchesType;
|
|
});
|
|
|
|
const handleExport = () => {
|
|
// Generate CSV content
|
|
const headers = ['ID', 'Agent', 'Source', 'Type', 'Status', 'Start Time', 'Duration'];
|
|
const rows = filteredLogs.map(log => [
|
|
log.id,
|
|
log.agentName,
|
|
log.source,
|
|
log.type,
|
|
log.status,
|
|
log.startTime,
|
|
log.duration
|
|
].join(','));
|
|
const csvContent = "data:text/csv;charset=utf-8," + [headers.join(','), ...headers.join(',')].join('\n');
|
|
const encodedUri = encodeURI(csvContent);
|
|
const link = document.createElement("a");
|
|
link.setAttribute("href", encodedUri);
|
|
link.setAttribute("download", "history_logs.csv");
|
|
document.body.appendChild(link);
|
|
link.click();
|
|
document.body.removeChild(link);
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-6 animate-in fade-in py-4 pb-10">
|
|
<div className="flex items-center justify-between">
|
|
<h1 className="text-2xl font-bold tracking-tight text-white">历史记录</h1>
|
|
<Button variant="outline" onClick={handleExport}>
|
|
<Download className="mr-2 h-4 w-4" /> 导出记录
|
|
</Button>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-5 gap-4 bg-card/50 p-4 rounded-lg border border-white/5 shadow-sm">
|
|
<div className="relative">
|
|
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
|
<Input
|
|
placeholder="搜索代理小助手..."
|
|
className="pl-9 border-0 bg-white/5"
|
|
value={searchTerm}
|
|
onChange={e => setSearchTerm(e.target.value)}
|
|
/>
|
|
</div>
|
|
<div className="flex items-center space-x-2">
|
|
<Filter className="h-4 w-4 text-muted-foreground" />
|
|
<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 [&>option]:bg-card"
|
|
value={sourceFilter}
|
|
onChange={(e) => setSourceFilter(e.target.value as any)}
|
|
>
|
|
<option value="all">所有来源</option>
|
|
<option value="debug">调试 (Debug)</option>
|
|
<option value="external">外部测试 (External)</option>
|
|
</select>
|
|
</div>
|
|
<div className="flex items-center space-x-2">
|
|
<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 [&>option]:bg-card"
|
|
value={typeFilter}
|
|
onChange={(e) => setTypeFilter(e.target.value as any)}
|
|
>
|
|
<option value="all">所有类型</option>
|
|
<option value="text">文本 (Text)</option>
|
|
<option value="audio">语音 (Audio)</option>
|
|
<option value="video">视频 (Video)</option>
|
|
</select>
|
|
</div>
|
|
<div className="flex items-center space-x-2">
|
|
<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 [&>option]:bg-card"
|
|
value={statusFilter}
|
|
onChange={(e) => setStatusFilter(e.target.value as any)}
|
|
>
|
|
<option value="all">所有状态</option>
|
|
<option value="connected">已接通</option>
|
|
<option value="missed">未接通</option>
|
|
</select>
|
|
</div>
|
|
<div className="relative">
|
|
<Calendar className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
|
<Input type="date" className="pl-9 border-0 bg-white/5" />
|
|
</div>
|
|
</div>
|
|
|
|
<div className="rounded-md border border-white/5 bg-card/40 backdrop-blur-md overflow-hidden">
|
|
<table className="w-full text-sm">
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead>编号</TableHead>
|
|
<TableHead>代理小助手</TableHead>
|
|
<TableHead>类型</TableHead>
|
|
<TableHead>来源</TableHead>
|
|
<TableHead>接听状态</TableHead>
|
|
<TableHead>通话接通时间</TableHead>
|
|
<TableHead>通话时长</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<tbody>
|
|
{filteredLogs.map(log => (
|
|
<TableRow key={log.id} className="cursor-pointer hover:bg-white/5 group" onClick={() => setSelectedLog(log)}>
|
|
<TableCell className="font-mono text-xs text-muted-foreground group-hover:text-primary transition-colors">#{log.id}</TableCell>
|
|
<TableCell className="font-medium text-white group-hover:text-primary transition-colors flex items-center gap-2">
|
|
{log.agentName}
|
|
</TableCell>
|
|
<TableCell>
|
|
<div className="flex items-center gap-1.5">
|
|
{log.type === 'text' && <MessageSquare className="w-3.5 h-3.5 text-blue-400" />}
|
|
{log.type === 'audio' && <Mic className="w-3.5 h-3.5 text-orange-400" />}
|
|
{log.type === 'video' && <Video className="w-3.5 h-3.5 text-green-400" />}
|
|
<span className="capitalize text-xs">{log.type}</span>
|
|
</div>
|
|
</TableCell>
|
|
<TableCell>
|
|
<Badge variant="outline">{log.source === 'debug' ? '调试' : '外部'}</Badge>
|
|
</TableCell>
|
|
<TableCell>
|
|
<Badge variant={log.status === 'connected' ? 'success' : 'warning'}>
|
|
{log.status === 'connected' ? '已接通' : '未接通'}
|
|
</Badge>
|
|
</TableCell>
|
|
<TableCell className="text-muted-foreground">{log.startTime}</TableCell>
|
|
<TableCell className="text-muted-foreground">{log.duration}</TableCell>
|
|
</TableRow>
|
|
))}
|
|
{filteredLogs.length === 0 && (
|
|
<TableRow>
|
|
<TableCell colSpan={7} className="text-center py-6 text-muted-foreground">暂无记录</TableCell>
|
|
</TableRow>
|
|
)}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
{selectedLog && (
|
|
<Drawer
|
|
isOpen={!!selectedLog}
|
|
onClose={() => setSelectedLog(null)}
|
|
title="历史记录详情"
|
|
>
|
|
<div className="flex flex-col h-full overflow-hidden">
|
|
<div className="shrink-0 mb-4 p-4 bg-white/5 rounded-xl border border-white/10 space-y-3">
|
|
<div className="flex items-center justify-between">
|
|
<h3 className="font-bold text-lg text-white">{selectedLog.agentName}</h3>
|
|
<Badge variant="outline" className="uppercase">{selectedLog.type} Record</Badge>
|
|
</div>
|
|
<div className="grid grid-cols-2 gap-4 text-xs text-muted-foreground">
|
|
<div>ID: <span className="font-mono text-white/70">#{selectedLog.id}</span></div>
|
|
<div>时间: <span className="text-white/70">{selectedLog.startTime}</span></div>
|
|
<div>时长: <span className="text-white/70">{selectedLog.duration}</span></div>
|
|
<div>状态: <span className={selectedLog.status === 'connected' ? 'text-green-400' : 'text-yellow-400'}>{selectedLog.status}</span></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex-1 overflow-y-auto space-y-4 pr-1 custom-scrollbar pb-6">
|
|
{(selectedLog.details && selectedLog.details.length > 0) ? (
|
|
selectedLog.details.map((detail, index) => (
|
|
<div key={index} className={`flex flex-col gap-1 ${detail.role === 'user' ? 'items-end' : 'items-start'}`}>
|
|
<div className={`max-w-[85%] rounded-2xl p-4 shadow-sm border ${
|
|
detail.role === 'user'
|
|
? 'bg-primary/10 border-primary/20 rounded-tr-none'
|
|
: 'bg-card border-white/10 rounded-tl-none'
|
|
}`}>
|
|
<div className="flex items-center gap-2 mb-2 opacity-70">
|
|
<span className="text-[10px] font-bold uppercase tracking-wider text-primary">
|
|
{detail.role === 'user' ? 'User' : 'AI Assistant'}
|
|
</span>
|
|
<span className="text-[10px] text-muted-foreground">{detail.timestamp}</span>
|
|
</div>
|
|
|
|
{/* Video Frames */}
|
|
{selectedLog.type === 'video' && detail.role === 'user' && detail.imageUrls && detail.imageUrls.length > 0 && (
|
|
<div className="flex gap-2 overflow-x-auto mb-3 pb-2">
|
|
{detail.imageUrls.map((url, i) => (
|
|
<div key={i} className="relative h-20 w-32 rounded-lg overflow-hidden border border-white/10 bg-black/50 shrink-0 group">
|
|
<img src={url} alt={`Frame ${i}`} className="h-full w-full object-cover" />
|
|
<div className="absolute inset-0 flex items-center justify-center bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity">
|
|
<Eye className="w-5 h-5 text-white" />
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{/* Content / Transcript */}
|
|
<div className="text-sm leading-relaxed text-white/90">
|
|
{selectedLog.type !== 'text' && (
|
|
<div className="flex items-center gap-2 mb-1.5">
|
|
<div className={`p-1.5 rounded-full ${detail.role === 'user' ? 'bg-primary/20' : 'bg-white/10'}`}>
|
|
{selectedLog.type === 'audio' ? <Mic size={12} /> : <Video size={12} />}
|
|
</div>
|
|
<span className="text-[10px] uppercase font-mono text-muted-foreground">Transcript</span>
|
|
</div>
|
|
)}
|
|
{detail.content}
|
|
</div>
|
|
|
|
{/* Audio Player Placeholder for Audio/Video types */}
|
|
{selectedLog.type !== 'text' && (
|
|
<div className="mt-3 flex items-center gap-2 p-2 rounded-lg bg-black/20 border border-white/5">
|
|
<button className="w-6 h-6 rounded-full bg-white/10 hover:bg-primary hover:text-white flex items-center justify-center transition-colors">
|
|
<Play size={10} className="ml-0.5" />
|
|
</button>
|
|
<div className="h-1 flex-1 bg-white/10 rounded-full overflow-hidden">
|
|
<div className="h-full w-1/3 bg-primary/50"></div>
|
|
</div>
|
|
<span className="text-[10px] font-mono text-muted-foreground">00:05</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
))
|
|
) : (
|
|
<div className="h-full flex flex-col items-center justify-center text-muted-foreground opacity-50 space-y-2">
|
|
<MessageSquare className="w-10 h-10" />
|
|
<p className="text-sm">暂无对话详情数据</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</Drawer>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|