From d494e78f709291d2b964470dc57791f220ea7b7b Mon Sep 17 00:00:00 2001 From: KiriAky 107 Date: Fri, 27 Mar 2026 02:02:15 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E5=89=8D=E7=AB=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/pages/InstructionChat.tsx | 351 +++++++++++++++++++++++++ frontend/src/pages/TaskHistory.tsx | 307 +++++++++++++++++++++ 2 files changed, 658 insertions(+) create mode 100644 frontend/src/pages/InstructionChat.tsx create mode 100644 frontend/src/pages/TaskHistory.tsx diff --git a/frontend/src/pages/InstructionChat.tsx b/frontend/src/pages/InstructionChat.tsx new file mode 100644 index 0000000..cd6bdab --- /dev/null +++ b/frontend/src/pages/InstructionChat.tsx @@ -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([]); + const [input, setInput] = useState(''); + const [loading, setLoading] = useState(false); + const scrollAreaRef = useRef(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 ( +
+
+
+

+ + 智能助手 +

+

通过自然语言指令,极速操控您的整个文档数据库。

+
+ +
+ +
+ {/* Chat Area */} + + +
+ {messages.map((m) => ( +
+
+ {m.role === 'user' ? : } +
+
+

+ {m.content} +

+ + {new Date(m.created_at).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} + +
+
+ ))} + {loading && ( +
+
+ +
+
+
+
+
+
+
+
+
+ )} +
+ + + +
{ 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" + > + setInput(e.target.value)} + disabled={loading} + /> + +
+
+ + + {/* Quick Actions Panel */} + +
+
+ ); +}; + +export default InstructionChat; \ No newline at end of file diff --git a/frontend/src/pages/TaskHistory.tsx b/frontend/src/pages/TaskHistory.tsx new file mode 100644 index 0000000..91b162b --- /dev/null +++ b/frontend/src/pages/TaskHistory.tsx @@ -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([]); + const [loading, setLoading] = useState(true); + const [expandedTask, setExpandedTask] = useState(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 成功; + case 'failure': + return 失败; + case 'processing': + return 处理中; + default: + return 等待; + } + }; + + 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 ; + case 'excel_analysis': + return ; + default: + return ; + } + }; + + 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 ( +
+
+
+

任务历史

+

查看和管理您所有的文档处理任务记录

+
+ +
+ + {/* Stats Cards */} +
+ {[ + { 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) => ( + + +
+ +
+
+

{stat.value}

+

{stat.label}

+
+
+
+ ))} +
+ + {/* Task List */} +
+ {loading ? ( + Array.from({ length: 3 }).map((_, i) => ( + + )) + ) : tasks.length > 0 ? ( + tasks.map((task) => ( + +
+
+
+ {task.status === 'processing' ? ( + + ) : ( + getTaskIcon(task.task_type) + )} +
+ +
+
+

{getTaskTypeLabel(task.task_type)}

+ {getStatusBadge(task.status)} + + {task.task_id} + +
+

+ {task.message || '任务执行中...'} +

+
+ + + {format(new Date(task.created_at), 'yyyy-MM-dd HH:mm:ss')} + + {task.completed_at && ( + + 耗时: {Math.round((new Date(task.completed_at).getTime() - new Date(task.created_at).getTime()) / 1000)} 秒 + + )} +
+
+ +
+ {task.status === 'failure' && ( + + )} + + {task.result && ( + + )} +
+
+ + {/* Expanded Details */} + {expandedTask === task.task_id && task.result && ( +
+
+

任务结果

+
+                        {JSON.stringify(task.result, null, 2)}
+                      
+
+
+ )} + + {/* Error Details */} + {task.status === 'failure' && task.error && ( +
+
+

+ + 错误详情 +

+

{task.error}

+
+
+ )} +
+
+ )) + ) : ( +
+
+ +
+
+

暂无任务记录

+

上传文档或创建填表任务后,这里会显示处理记录

+
+
+ )} +
+
+ ); +}; + +export default TaskHistory; \ No newline at end of file