修改前端

This commit is contained in:
2026-03-27 02:02:15 +08:00
parent 091c9db0da
commit d494e78f70
2 changed files with 658 additions and 0 deletions

View 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;

View 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;