修改前端
This commit is contained in:
351
frontend/src/pages/InstructionChat.tsx
Normal file
351
frontend/src/pages/InstructionChat.tsx
Normal file
@@ -0,0 +1,351 @@
|
|||||||
|
import React, { useState, useRef, useEffect } from 'react';
|
||||||
|
import {
|
||||||
|
Send,
|
||||||
|
Bot,
|
||||||
|
User,
|
||||||
|
Sparkles,
|
||||||
|
Trash2,
|
||||||
|
RefreshCcw,
|
||||||
|
FileText,
|
||||||
|
TableProperties,
|
||||||
|
ChevronRight,
|
||||||
|
ArrowRight,
|
||||||
|
Loader2
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { backendApi } from '@/db/backend-api';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
type ChatMessage = {
|
||||||
|
id: string;
|
||||||
|
role: 'user' | 'assistant';
|
||||||
|
content: string;
|
||||||
|
created_at: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const InstructionChat: React.FC = () => {
|
||||||
|
const [messages, setMessages] = useState<ChatMessage[]>([]);
|
||||||
|
const [input, setInput] = useState('');
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const scrollAreaRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Initial welcome message
|
||||||
|
if (messages.length === 0) {
|
||||||
|
setMessages([
|
||||||
|
{
|
||||||
|
id: 'welcome',
|
||||||
|
role: 'assistant',
|
||||||
|
content: `您好!我是智联文档 AI 助手。
|
||||||
|
|
||||||
|
我可以帮您完成以下操作:
|
||||||
|
|
||||||
|
📄 **文档管理**
|
||||||
|
- "帮我列出最近上传的所有文档"
|
||||||
|
- "删除三天前的 docx 文档"
|
||||||
|
|
||||||
|
📊 **Excel 分析**
|
||||||
|
- "分析一下最近上传的 Excel 文件"
|
||||||
|
- "帮我统计销售报表中的数据"
|
||||||
|
|
||||||
|
📝 **智能填表**
|
||||||
|
- "根据员工信息表创建一个考勤汇总表"
|
||||||
|
- "用财务文档填充报销模板"
|
||||||
|
|
||||||
|
请告诉我您想做什么?`,
|
||||||
|
created_at: new Date().toISOString()
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Scroll to bottom
|
||||||
|
if (scrollAreaRef.current) {
|
||||||
|
const scrollElement = scrollAreaRef.current.querySelector('[data-radix-scroll-area-viewport]');
|
||||||
|
if (scrollElement) {
|
||||||
|
scrollElement.scrollTop = scrollElement.scrollHeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [messages]);
|
||||||
|
|
||||||
|
const handleSend = async () => {
|
||||||
|
if (!input.trim()) return;
|
||||||
|
|
||||||
|
const userMessage: ChatMessage = {
|
||||||
|
id: Math.random().toString(36).substring(7),
|
||||||
|
role: 'user',
|
||||||
|
content: input.trim(),
|
||||||
|
created_at: new Date().toISOString()
|
||||||
|
};
|
||||||
|
|
||||||
|
setMessages(prev => [...prev, userMessage]);
|
||||||
|
setInput('');
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// TODO: 后端对话接口,暂用模拟响应
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1500));
|
||||||
|
|
||||||
|
// 简单的命令解析演示
|
||||||
|
const userInput = userMessage.content.toLowerCase();
|
||||||
|
let response = '';
|
||||||
|
|
||||||
|
if (userInput.includes('列出') || userInput.includes('列表')) {
|
||||||
|
const result = await backendApi.getDocuments(undefined, 10);
|
||||||
|
if (result.success && result.documents && result.documents.length > 0) {
|
||||||
|
response = `已为您找到 ${result.documents.length} 个文档:\n\n`;
|
||||||
|
result.documents.slice(0, 5).forEach((doc: any, idx: number) => {
|
||||||
|
response += `${idx + 1}. **${doc.original_filename}** (${doc.doc_type.toUpperCase()})\n`;
|
||||||
|
response += ` - 大小: ${(doc.file_size / 1024).toFixed(1)} KB\n`;
|
||||||
|
response += ` - 时间: ${new Date(doc.created_at).toLocaleDateString()}\n\n`;
|
||||||
|
});
|
||||||
|
if (result.documents.length > 5) {
|
||||||
|
response += `...还有 ${result.documents.length - 5} 个文档`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
response = '暂未找到已上传的文档,您可以先上传一些文档试试。';
|
||||||
|
}
|
||||||
|
} else if (userInput.includes('分析') || userInput.includes('excel') || userInput.includes('报表')) {
|
||||||
|
response = `好的,我可以帮您分析 Excel 文件。
|
||||||
|
|
||||||
|
请告诉我:
|
||||||
|
1. 您想分析哪个 Excel 文件?
|
||||||
|
2. 需要什么样的分析?(数据摘要/统计分析/图表生成)
|
||||||
|
|
||||||
|
或者您可以直接告诉我您想从数据中了解什么,我来为您生成分析。`;
|
||||||
|
} else if (userInput.includes('填表') || userInput.includes('模板')) {
|
||||||
|
response = `好的,要进行智能填表,我需要:
|
||||||
|
|
||||||
|
1. **上传表格模板** - 您要填写的表格模板文件(Excel 或 Word 格式)
|
||||||
|
2. **选择数据源** - 包含要填写内容的源文档
|
||||||
|
|
||||||
|
您可以去【智能填表】页面完成这些操作,或者告诉我您具体想填什么类型的表格,我来指导您操作。`;
|
||||||
|
} else if (userInput.includes('删除')) {
|
||||||
|
response = `要删除文档,请告诉我:
|
||||||
|
|
||||||
|
- 要删除的文件名是什么?
|
||||||
|
- 或者您可以到【文档中心】页面手动选择并删除文档
|
||||||
|
|
||||||
|
⚠️ 删除操作不可恢复,请确认后再操作。`;
|
||||||
|
} else if (userInput.includes('帮助') || userInput.includes('help')) {
|
||||||
|
response = `**我可以帮您完成以下操作:**
|
||||||
|
|
||||||
|
📄 **文档管理**
|
||||||
|
- 列出/搜索已上传的文档
|
||||||
|
- 查看文档详情和元数据
|
||||||
|
- 删除不需要的文档
|
||||||
|
|
||||||
|
📊 **Excel 处理**
|
||||||
|
- 分析 Excel 文件内容
|
||||||
|
- 生成数据统计和图表
|
||||||
|
- 导出处理后的数据
|
||||||
|
|
||||||
|
📝 **智能填表**
|
||||||
|
- 上传表格模板
|
||||||
|
- 从文档中提取信息填入模板
|
||||||
|
- 导出填写完成的表格
|
||||||
|
|
||||||
|
📋 **任务历史**
|
||||||
|
- 查看历史处理任务
|
||||||
|
- 重新执行或导出结果
|
||||||
|
|
||||||
|
请直接告诉我您想做什么!`;
|
||||||
|
} else {
|
||||||
|
response = `我理解您想要: "${input.trim()}"
|
||||||
|
|
||||||
|
目前我还在学习如何更好地理解您的需求。您可以尝试:
|
||||||
|
|
||||||
|
1. **上传文档** - 去【文档中心】上传 docx/md/txt 文件
|
||||||
|
2. **分析 Excel** - 去【Excel解析】上传并分析 Excel 文件
|
||||||
|
3. **智能填表** - 去【智能填表】创建填表任务
|
||||||
|
|
||||||
|
或者您可以更具体地描述您想做的事情,我会尽力帮助您!`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const assistantMessage: ChatMessage = {
|
||||||
|
id: Math.random().toString(36).substring(7),
|
||||||
|
role: 'assistant',
|
||||||
|
content: response,
|
||||||
|
created_at: new Date().toISOString()
|
||||||
|
};
|
||||||
|
|
||||||
|
setMessages(prev => [...prev, assistantMessage]);
|
||||||
|
} catch (err: any) {
|
||||||
|
toast.error('请求失败,请重试');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearChat = () => {
|
||||||
|
setMessages([messages[0]]);
|
||||||
|
toast.success('对话已清空');
|
||||||
|
};
|
||||||
|
|
||||||
|
const quickActions = [
|
||||||
|
{ label: '列出所有文档', icon: FileText, action: () => setInput('列出所有已上传的文档') },
|
||||||
|
{ label: '分析 Excel 数据', icon: TableProperties, action: () => setInput('分析一下 Excel 文件') },
|
||||||
|
{ label: '智能填表', icon: Sparkles, action: () => setInput('我想进行智能填表') },
|
||||||
|
{ label: '帮助', icon: Sparkles, action: () => setInput('帮助') }
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="h-[calc(100vh-8rem)] flex flex-col gap-6 animate-fade-in relative">
|
||||||
|
<section className="flex flex-col md:flex-row md:items-center justify-between gap-4">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<h1 className="text-3xl font-extrabold tracking-tight flex items-center gap-3">
|
||||||
|
<Sparkles className="text-primary animate-pulse" />
|
||||||
|
智能助手
|
||||||
|
</h1>
|
||||||
|
<p className="text-muted-foreground">通过自然语言指令,极速操控您的整个文档数据库。</p>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="rounded-xl gap-2 h-10 border-none bg-card shadow-sm hover:bg-destructive/10 hover:text-destructive"
|
||||||
|
onClick={clearChat}
|
||||||
|
>
|
||||||
|
<Trash2 size={16} />
|
||||||
|
<span>清除历史</span>
|
||||||
|
</Button>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div className="flex-1 flex flex-col lg:flex-row gap-6 min-h-0">
|
||||||
|
{/* Chat Area */}
|
||||||
|
<Card className="flex-1 flex flex-col border-none shadow-xl overflow-hidden rounded-3xl bg-card/50 backdrop-blur-sm">
|
||||||
|
<ScrollArea className="flex-1 p-6" ref={scrollAreaRef}>
|
||||||
|
<div className="space-y-8 pb-4">
|
||||||
|
{messages.map((m) => (
|
||||||
|
<div
|
||||||
|
key={m.id}
|
||||||
|
className={cn(
|
||||||
|
"flex gap-4 max-w-[85%]",
|
||||||
|
m.role === 'user' ? "ml-auto flex-row-reverse" : "mr-auto"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className={cn(
|
||||||
|
"w-10 h-10 rounded-2xl flex items-center justify-center shrink-0 shadow-lg",
|
||||||
|
m.role === 'user' ? "bg-primary text-primary-foreground" : "bg-white text-primary border border-primary/20"
|
||||||
|
)}>
|
||||||
|
{m.role === 'user' ? <User size={20} /> : <Bot size={22} />}
|
||||||
|
</div>
|
||||||
|
<div className={cn(
|
||||||
|
"space-y-2 p-5 rounded-3xl",
|
||||||
|
m.role === 'user'
|
||||||
|
? "bg-primary text-primary-foreground shadow-xl shadow-primary/20 rounded-tr-none"
|
||||||
|
: "bg-white border border-border/50 shadow-md rounded-tl-none"
|
||||||
|
)}>
|
||||||
|
<p className="text-sm leading-relaxed whitespace-pre-wrap font-medium">
|
||||||
|
{m.content}
|
||||||
|
</p>
|
||||||
|
<span className={cn(
|
||||||
|
"text-[10px] block opacity-50 font-bold tracking-widest",
|
||||||
|
m.role === 'user' ? "text-right" : "text-left"
|
||||||
|
)}>
|
||||||
|
{new Date(m.created_at).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{loading && (
|
||||||
|
<div className="flex gap-4 mr-auto max-w-[85%] animate-pulse">
|
||||||
|
<div className="w-10 h-10 rounded-2xl bg-muted flex items-center justify-center shrink-0 border border-border/50">
|
||||||
|
<Bot size={22} className="text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
<div className="p-5 rounded-3xl rounded-tl-none bg-muted/50 border border-border/50">
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<div className="w-2 h-2 rounded-full bg-primary/40 animate-bounce [animation-delay:-0.3s]" />
|
||||||
|
<div className="w-2 h-2 rounded-full bg-primary/40 animate-bounce [animation-delay:-0.15s]" />
|
||||||
|
<div className="w-2 h-2 rounded-full bg-primary/40 animate-bounce" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
|
|
||||||
|
<CardContent className="p-6 bg-white/50 backdrop-blur-xl border-t border-border/50">
|
||||||
|
<form
|
||||||
|
onSubmit={(e) => { e.preventDefault(); handleSend(); }}
|
||||||
|
className="w-full flex gap-3 bg-muted/30 p-2 rounded-2xl border border-border/50 focus-within:border-primary/50 transition-all shadow-inner"
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
placeholder="尝试输入:帮我分析最近上传的 Excel 文件..."
|
||||||
|
className="flex-1 bg-transparent border-none focus-visible:ring-0 shadow-none h-12 text-base font-medium"
|
||||||
|
value={input}
|
||||||
|
onChange={(e) => setInput(e.target.value)}
|
||||||
|
disabled={loading}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
size="icon"
|
||||||
|
className="w-12 h-12 rounded-xl bg-primary hover:scale-105 transition-all shadow-lg shadow-primary/20"
|
||||||
|
disabled={loading || !input.trim()}
|
||||||
|
>
|
||||||
|
<Send size={20} />
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Quick Actions Panel */}
|
||||||
|
<aside className="w-full lg:w-80 space-y-6">
|
||||||
|
<Card className="border-none shadow-lg rounded-3xl bg-gradient-to-br from-primary/5 via-background to-background">
|
||||||
|
<CardHeader className="p-6">
|
||||||
|
<CardTitle className="text-sm font-bold uppercase tracking-widest text-primary flex items-center gap-2">
|
||||||
|
<Sparkles size={16} />
|
||||||
|
快捷操作
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="p-6 pt-0 space-y-3">
|
||||||
|
{quickActions.map((action, i) => (
|
||||||
|
<button
|
||||||
|
key={i}
|
||||||
|
className="w-full flex items-center gap-3 p-3 rounded-2xl hover:bg-white hover:shadow-md transition-all group text-left border border-transparent hover:border-primary/10"
|
||||||
|
onClick={action.action}
|
||||||
|
>
|
||||||
|
<div className="w-8 h-8 rounded-lg bg-primary/10 text-primary flex items-center justify-center shrink-0">
|
||||||
|
<action.icon size={16} />
|
||||||
|
</div>
|
||||||
|
<span className="text-sm font-semibold truncate group-hover:text-primary transition-colors">{action.label}</span>
|
||||||
|
<ArrowRight size={14} className="ml-auto opacity-0 group-hover:opacity-100 -translate-x-2 group-hover:translate-x-0 transition-all" />
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="border-none shadow-lg rounded-3xl bg-gradient-to-br from-indigo-500/10 to-blue-500/10 overflow-hidden relative">
|
||||||
|
<div className="absolute top-0 right-0 p-4 opacity-20">
|
||||||
|
<Sparkles size={100} />
|
||||||
|
</div>
|
||||||
|
<CardHeader className="p-6 relative z-10">
|
||||||
|
<CardTitle className="text-lg font-bold">功能说明</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="p-6 pt-0 relative z-10 space-y-4 text-sm text-muted-foreground">
|
||||||
|
<div className="flex items-start gap-2">
|
||||||
|
<FileText size={16} className="mt-0.5 text-blue-500 shrink-0" />
|
||||||
|
<span>上传 docx/md/txt 文档到 MongoDB</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-start gap-2">
|
||||||
|
<TableProperties size={16} className="mt-0.5 text-emerald-500 shrink-0" />
|
||||||
|
<span>上传 xlsx 文档到 MySQL</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-start gap-2">
|
||||||
|
<Sparkles size={16} className="mt-0.5 text-indigo-500 shrink-0" />
|
||||||
|
<span>使用 RAG 智能检索和填表</span>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default InstructionChat;
|
||||||
307
frontend/src/pages/TaskHistory.tsx
Normal file
307
frontend/src/pages/TaskHistory.tsx
Normal file
@@ -0,0 +1,307 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import {
|
||||||
|
Clock,
|
||||||
|
CheckCircle2,
|
||||||
|
XCircle,
|
||||||
|
RefreshCcw,
|
||||||
|
Download,
|
||||||
|
FileText,
|
||||||
|
FileSpreadsheet,
|
||||||
|
Loader2,
|
||||||
|
ChevronDown,
|
||||||
|
ChevronUp,
|
||||||
|
Trash2,
|
||||||
|
AlertCircle
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { backendApi } from '@/db/backend-api';
|
||||||
|
import { format } from 'date-fns';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import { Skeleton } from '@/components/ui/skeleton';
|
||||||
|
|
||||||
|
type Task = {
|
||||||
|
task_id: string;
|
||||||
|
status: 'pending' | 'processing' | 'success' | 'failure';
|
||||||
|
created_at: string;
|
||||||
|
completed_at?: string;
|
||||||
|
message?: string;
|
||||||
|
result?: any;
|
||||||
|
error?: string;
|
||||||
|
task_type?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const TaskHistory: React.FC = () => {
|
||||||
|
const [tasks, setTasks] = useState<Task[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [expandedTask, setExpandedTask] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// Mock data for demonstration
|
||||||
|
useEffect(() => {
|
||||||
|
// 模拟任务数据,实际应该从后端获取
|
||||||
|
setTasks([
|
||||||
|
{
|
||||||
|
task_id: 'task-001',
|
||||||
|
status: 'success',
|
||||||
|
created_at: new Date(Date.now() - 3600000).toISOString(),
|
||||||
|
completed_at: new Date(Date.now() - 3500000).toISOString(),
|
||||||
|
task_type: 'document_parse',
|
||||||
|
message: '文档解析完成',
|
||||||
|
result: {
|
||||||
|
doc_id: 'doc-001',
|
||||||
|
filename: 'report_q1_2026.docx',
|
||||||
|
extracted_fields: ['标题', '作者', '日期', '金额']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
task_id: 'task-002',
|
||||||
|
status: 'success',
|
||||||
|
created_at: new Date(Date.now() - 7200000).toISOString(),
|
||||||
|
completed_at: new Date(Date.now() - 7100000).toISOString(),
|
||||||
|
task_type: 'excel_analysis',
|
||||||
|
message: 'Excel 分析完成',
|
||||||
|
result: {
|
||||||
|
filename: 'sales_data.xlsx',
|
||||||
|
row_count: 1250,
|
||||||
|
charts_generated: 3
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
task_id: 'task-003',
|
||||||
|
status: 'processing',
|
||||||
|
created_at: new Date(Date.now() - 600000).toISOString(),
|
||||||
|
task_type: 'template_fill',
|
||||||
|
message: '正在填充表格...'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
task_id: 'task-004',
|
||||||
|
status: 'failure',
|
||||||
|
created_at: new Date(Date.now() - 86400000).toISOString(),
|
||||||
|
completed_at: new Date(Date.now() - 86390000).toISOString(),
|
||||||
|
task_type: 'document_parse',
|
||||||
|
message: '解析失败',
|
||||||
|
error: '文件格式不支持或文件已损坏'
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
setLoading(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const getStatusBadge = (status: string) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'success':
|
||||||
|
return <Badge className="bg-emerald-500 text-white text-[10px]"><CheckCircle2 size={12} className="mr-1" />成功</Badge>;
|
||||||
|
case 'failure':
|
||||||
|
return <Badge className="bg-destructive text-white text-[10px]"><XCircle size={12} className="mr-1" />失败</Badge>;
|
||||||
|
case 'processing':
|
||||||
|
return <Badge className="bg-amber-500 text-white text-[10px]"><Loader2 size={12} className="mr-1 animate-spin" />处理中</Badge>;
|
||||||
|
default:
|
||||||
|
return <Badge className="bg-gray-500 text-white text-[10px]"><Clock size={12} className="mr-1" />等待</Badge>;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTaskTypeLabel = (type?: string) => {
|
||||||
|
switch (type) {
|
||||||
|
case 'document_parse':
|
||||||
|
return '文档解析';
|
||||||
|
case 'excel_analysis':
|
||||||
|
return 'Excel 分析';
|
||||||
|
case 'template_fill':
|
||||||
|
return '智能填表';
|
||||||
|
case 'rag_index':
|
||||||
|
return 'RAG 索引';
|
||||||
|
default:
|
||||||
|
return '未知任务';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTaskIcon = (type?: string) => {
|
||||||
|
switch (type) {
|
||||||
|
case 'document_parse':
|
||||||
|
case 'rag_index':
|
||||||
|
return <FileText size={20} />;
|
||||||
|
case 'excel_analysis':
|
||||||
|
return <FileSpreadsheet size={20} />;
|
||||||
|
default:
|
||||||
|
return <Clock size={20} />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRetry = async (taskId: string) => {
|
||||||
|
toast.info('任务重试功能开发中...');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = async (taskId: string) => {
|
||||||
|
setTasks(prev => prev.filter(t => t.task_id !== taskId));
|
||||||
|
toast.success('任务已删除');
|
||||||
|
};
|
||||||
|
|
||||||
|
const stats = {
|
||||||
|
total: tasks.length,
|
||||||
|
success: tasks.filter(t => t.status === 'success').length,
|
||||||
|
processing: tasks.filter(t => t.status === 'processing').length,
|
||||||
|
failure: tasks.filter(t => t.status === 'failure').length
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-8 pb-10">
|
||||||
|
<section className="flex flex-col md:flex-row md:items-center justify-between gap-4">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<h1 className="text-3xl font-extrabold tracking-tight">任务历史</h1>
|
||||||
|
<p className="text-muted-foreground">查看和管理您所有的文档处理任务记录</p>
|
||||||
|
</div>
|
||||||
|
<Button variant="outline" className="rounded-xl gap-2" onClick={() => window.location.reload()}>
|
||||||
|
<RefreshCcw size={18} />
|
||||||
|
<span>刷新</span>
|
||||||
|
</Button>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Stats Cards */}
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||||
|
{[
|
||||||
|
{ label: '总任务数', value: stats.total, icon: Clock, color: 'text-blue-500', bg: 'bg-blue-500/10' },
|
||||||
|
{ label: '成功', value: stats.success, icon: CheckCircle2, color: 'text-emerald-500', bg: 'bg-emerald-500/10' },
|
||||||
|
{ label: '处理中', value: stats.processing, icon: Loader2, color: 'text-amber-500', bg: 'bg-amber-500/10' },
|
||||||
|
{ label: '失败', value: stats.failure, icon: XCircle, color: 'text-destructive', bg: 'bg-destructive/10' }
|
||||||
|
].map((stat, i) => (
|
||||||
|
<Card key={i} className="border-none shadow-sm">
|
||||||
|
<CardContent className="p-4 flex items-center gap-4">
|
||||||
|
<div className={cn("w-12 h-12 rounded-xl flex items-center justify-center", stat.bg)}>
|
||||||
|
<stat.icon size={24} className={stat.color} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-2xl font-bold">{stat.value}</p>
|
||||||
|
<p className="text-xs text-muted-foreground">{stat.label}</p>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Task List */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
{loading ? (
|
||||||
|
Array.from({ length: 3 }).map((_, i) => (
|
||||||
|
<Skeleton key={i} className="h-24 w-full rounded-2xl" />
|
||||||
|
))
|
||||||
|
) : tasks.length > 0 ? (
|
||||||
|
tasks.map((task) => (
|
||||||
|
<Card key={task.task_id} className="border-none shadow-sm overflow-hidden">
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div className="p-6 flex flex-col md:flex-row md:items-center gap-4">
|
||||||
|
<div className={cn(
|
||||||
|
"w-12 h-12 rounded-xl flex items-center justify-center shrink-0",
|
||||||
|
task.status === 'success' ? "bg-emerald-500/10 text-emerald-500" :
|
||||||
|
task.status === 'failure' ? "bg-destructive/10 text-destructive" :
|
||||||
|
"bg-amber-500/10 text-amber-500"
|
||||||
|
)}>
|
||||||
|
{task.status === 'processing' ? (
|
||||||
|
<Loader2 size={24} className="animate-spin" />
|
||||||
|
) : (
|
||||||
|
getTaskIcon(task.task_type)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1 min-w-0 space-y-1">
|
||||||
|
<div className="flex items-center gap-3 flex-wrap">
|
||||||
|
<h3 className="font-bold">{getTaskTypeLabel(task.task_type)}</h3>
|
||||||
|
{getStatusBadge(task.status)}
|
||||||
|
<Badge variant="outline" className="text-[10px]">
|
||||||
|
{task.task_id}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{task.message || '任务执行中...'}
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center gap-4 text-xs text-muted-foreground">
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<Clock size={12} />
|
||||||
|
{format(new Date(task.created_at), 'yyyy-MM-dd HH:mm:ss')}
|
||||||
|
</span>
|
||||||
|
{task.completed_at && (
|
||||||
|
<span>
|
||||||
|
耗时: {Math.round((new Date(task.completed_at).getTime() - new Date(task.created_at).getTime()) / 1000)} 秒
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{task.status === 'failure' && (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="rounded-xl gap-1 h-9"
|
||||||
|
onClick={() => handleRetry(task.task_id)}
|
||||||
|
>
|
||||||
|
<RefreshCcw size={14} />
|
||||||
|
<span>重试</span>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="rounded-lg text-destructive hover:bg-destructive/10"
|
||||||
|
onClick={() => handleDelete(task.task_id)}
|
||||||
|
>
|
||||||
|
<Trash2 size={18} />
|
||||||
|
</Button>
|
||||||
|
{task.result && (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="rounded-xl gap-1 h-9"
|
||||||
|
onClick={() => setExpandedTask(expandedTask === task.task_id ? null : task.task_id)}
|
||||||
|
>
|
||||||
|
{expandedTask === task.task_id ? <ChevronUp size={14} /> : <ChevronDown size={14} />}
|
||||||
|
<span>详情</span>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Expanded Details */}
|
||||||
|
{expandedTask === task.task_id && task.result && (
|
||||||
|
<div className="px-6 pb-6 pt-2 border-t border-dashed animate-in slide-in-from-top-2 duration-300">
|
||||||
|
<div className="p-4 bg-muted/30 rounded-xl">
|
||||||
|
<p className="text-xs font-bold uppercase tracking-widest text-muted-foreground mb-3">任务结果</p>
|
||||||
|
<pre className="text-sm whitespace-pre-wrap font-mono">
|
||||||
|
{JSON.stringify(task.result, null, 2)}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Error Details */}
|
||||||
|
{task.status === 'failure' && task.error && (
|
||||||
|
<div className="px-6 pb-6 pt-2 border-t border-dashed">
|
||||||
|
<div className="p-4 bg-destructive/5 rounded-xl border border-destructive/20">
|
||||||
|
<p className="text-xs font-bold uppercase tracking-widest text-destructive mb-3 flex items-center gap-2">
|
||||||
|
<AlertCircle size={14} />
|
||||||
|
错误详情
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-destructive">{task.error}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<div className="py-20 flex flex-col items-center justify-center text-center space-y-4">
|
||||||
|
<div className="w-24 h-24 rounded-full bg-muted flex items-center justify-center text-muted-foreground/30">
|
||||||
|
<Clock size={48} />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<p className="text-xl font-bold">暂无任务记录</p>
|
||||||
|
<p className="text-muted-foreground">上传文档或创建填表任务后,这里会显示处理记录</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TaskHistory;
|
||||||
Reference in New Issue
Block a user