Files
FilesReadSystem/frontend/src/pages/Dashboard.tsx
dj e5d4724e82 【智能助手增强】
- 新增对话历史管理:MongoDB新增conversations集合,存储用户与AI的对话上下文,支持多轮对话意图延续
- 新增对话历史API(conversation.py):GET/DELETE conversation历史、列出所有会话
- 意图解析增强:支持基于对话历史的意图识别,上下文理解更准确
- 字段提取优化:支持"提取文档中的医院数量"等自然语言模式,智能去除"文档中的"前缀
- 文档对比优化:从指令中提取文件名并精确匹配source_docs,支持"对比A和B两个文档"
- 文档摘要优化:使用LLM生成真实AI摘要而非返回原始文档预览

【Word模板填表核心功能】
- Word模板字段生成:空白Word上传后,自动从源文档(Excel/Word/TXT/MD)内容AI生成字段名
- Word模板填表(_fill_docx):将提取数据写入Word模板表格,支持精确匹配、模糊匹配、追加新行
- 数据润色(_polish_word_filled_data):LLM对多行Excel数据进行统计归纳(合计/平均/极值),转化为专业自然语言描述
- 段落格式输出:使用📌字段名+值段落+分隔线(灰色横线)格式,提升可读性
- 导出链打通:fill_template返回filled_file_path,export直接返回已填好的Word文件

【其他修复】
- 修复Word导出Windows文件锁问题:NamedTemporaryFile改为mkstemp+close
- 修复Word方框非法字符:扩展clean_text移除\uFFFD、□等Unicode替代符和零宽字符
- 修复文档对比"需要至少2个文档":从指令提取具体文件名优先匹配而非取前2个
- 修复导出format硬编码:自动识别docx/xlsx格式
- Docx解析器增加备用解析方法和更完整的段落/表格/标题提取
- RAG服务新增MySQL数据源支持
2026-04-15 23:32:55 +08:00

286 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useEffect, useState } from 'react';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
import { Link } from 'react-router-dom';
import { Button } from '@/components/ui/button';
import {
FileText,
TableProperties,
MessageSquareCode,
TrendingUp,
Clock,
CheckCircle2,
ArrowRight,
UploadCloud,
Layers,
Sparkles,
Database,
FileSpreadsheet,
RefreshCcw,
Trash2
} from 'lucide-react';
import { backendApi } from '@/db/backend-api';
import { formatDistanceToNow } from 'date-fns';
import { zhCN } from 'date-fns/locale';
import { cn } from '@/lib/utils';
import { toast } from 'sonner';
type DocumentItem = {
doc_id: string;
filename: string;
original_filename: string;
doc_type: string;
file_size: number;
created_at: string;
metadata?: {
row_count?: number;
column_count?: number;
columns?: string[];
};
};
type TaskItem = {
task_id: string;
status: string;
created_at: string;
message?: string;
};
const Dashboard: React.FC = () => {
const [stats, setStats] = useState({ docs: 0, excelFiles: 0, tasks: 0 });
const [recentDocs, setRecentDocs] = useState<DocumentItem[]>([]);
const [recentTasks, setRecentTasks] = useState<TaskItem[]>([]);
const [loading, setLoading] = useState(true);
const loadData = async () => {
setLoading(true);
try {
// 获取文档列表
const docsResult = await backendApi.getDocuments(undefined, 50);
if (docsResult.success && docsResult.documents) {
setRecentDocs(docsResult.documents.slice(0, 5));
// 分类统计
const docxMdTxt = docsResult.documents.filter((d: DocumentItem) =>
['docx', 'md', 'txt'].includes(d.doc_type)
).length;
const xlsx = docsResult.documents.filter((d: DocumentItem) =>
d.doc_type === 'xlsx'
).length;
setStats({
docs: docxMdTxt,
excelFiles: xlsx,
tasks: 0 // TODO: 后端任务接口
});
}
} catch (err) {
console.error('加载数据失败:', err);
} finally {
setLoading(false);
}
};
useEffect(() => {
loadData();
}, []);
return (
<div className="space-y-8 animate-fade-in">
<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">
使 <span className="text-primary"></span> 👋
</h1>
<p className="text-muted-foreground"></p>
</div>
<div className="flex items-center gap-3">
<Button variant="outline" className="rounded-xl" asChild>
<Link to="/documents"></Link>
</Button>
<Button className="rounded-xl shadow-lg shadow-primary/20" asChild>
<Link to="/assistant" className="gap-2">
<Sparkles size={18} />
<span></span>
</Link>
</Button>
</div>
</section>
{/* Stats Grid */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{[
{ label: '已上传文档', value: stats.docs, icon: FileText, color: 'bg-blue-500', trend: '非结构化文档', link: '/documents' },
{ label: 'Excel 文件', value: stats.excelFiles, icon: FileSpreadsheet, color: 'bg-emerald-500', trend: '结构化数据', link: '/documents' },
{ label: '填表任务', value: stats.tasks, icon: TableProperties, color: 'bg-indigo-500', trend: '待实现', link: '/form-fill' }
].map((stat, i) => (
<Card key={i} className="border-none shadow-md overflow-hidden group hover:shadow-xl transition-all duration-300">
<CardContent className="p-0">
<div className="p-6 flex items-start justify-between">
<div className="space-y-2">
<p className="text-sm font-medium text-muted-foreground">{stat.label}</p>
<p className="text-3xl font-bold tracking-tight">{stat.value}</p>
<div className="flex items-center gap-1 text-xs text-muted-foreground bg-muted px-2 py-1 rounded-full w-fit">
<span>{stat.trend}</span>
</div>
</div>
<div className={cn("w-12 h-12 rounded-2xl flex items-center justify-center text-white shadow-lg", stat.color)}>
<stat.icon size={24} />
</div>
</div>
<div className="h-1 bg-muted group-hover:bg-primary transition-colors" />
</CardContent>
</Card>
))}
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
{/* Recent Documents */}
<Card className="border-none shadow-md">
<CardHeader className="flex flex-row items-center justify-between pb-2">
<div className="space-y-1">
<CardTitle className="text-xl flex items-center gap-2">
<Clock className="text-primary" size={20} />
</CardTitle>
<CardDescription></CardDescription>
</div>
<Button variant="ghost" size="sm" asChild className="text-primary hover:text-primary/80 hover:bg-primary/5">
<Link to="/documents"> <ArrowRight size={14} className="ml-1" /></Link>
</Button>
</CardHeader>
<CardContent>
{loading ? (
<div className="space-y-4 py-4">
{[1, 2, 3].map(i => <div key={i} className="h-12 bg-muted rounded-xl animate-pulse" />)}
</div>
) : recentDocs.length > 0 ? (
<div className="space-y-3">
{recentDocs.map(doc => (
<div key={doc.doc_id} className="flex items-center gap-4 p-3 rounded-xl border border-transparent hover:border-border hover:bg-muted/30 transition-all group">
<div className="w-10 h-10 rounded-lg bg-blue-500/10 text-blue-500 flex items-center justify-center">
<FileText size={20} />
</div>
<div className="flex-1 min-w-0">
<p className="font-semibold text-sm truncate">{doc.original_filename}</p>
<p className="text-xs text-muted-foreground">
{doc.doc_type.toUpperCase()} {formatDistanceToNow(new Date(doc.created_at), { addSuffix: true, locale: zhCN })}
</p>
</div>
<div className="flex items-center gap-2">
<div className="px-2 py-1 rounded-full text-[10px] font-bold uppercase tracking-wider bg-muted">
{doc.doc_type}
</div>
<Button
variant="ghost"
size="icon"
className="opacity-0 group-hover:opacity-100 text-destructive hover:bg-destructive/10 transition-opacity"
onClick={async (e) => {
e.stopPropagation();
if (!confirm(`确定要删除 "${doc.original_filename}" 吗?`)) return;
try {
const result = await backendApi.deleteDocument(doc.doc_id);
if (result.success) {
setRecentDocs(prev => prev.filter(d => d.doc_id !== doc.doc_id));
toast.success('文档已删除');
}
} catch (err: any) {
toast.error(err.message || '删除失败');
}
}}
>
<Trash2 size={16} />
</Button>
</div>
</div>
))}
</div>
) : (
<div className="flex flex-col items-center justify-center py-10 text-center space-y-3">
<UploadCloud size={48} className="text-muted-foreground/30" />
<p className="text-muted-foreground italic"></p>
<Button variant="outline" size="sm" asChild className="rounded-xl">
<Link to="/documents"></Link>
</Button>
</div>
)}
</CardContent>
</Card>
{/* Quick Actions */}
<Card className="border-none shadow-md">
<CardHeader className="flex flex-row items-center justify-between pb-2">
<div className="space-y-1">
<CardTitle className="text-xl flex items-center gap-2">
<Sparkles className="text-primary" size={20} />
</CardTitle>
<CardDescription>使</CardDescription>
</div>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
{[
{ title: '上传文档', desc: '支持 docx/md/txt', icon: FileText, link: '/documents', color: 'bg-blue-500' },
{ title: '解析 Excel', desc: '上传并分析数据', icon: FileSpreadsheet, link: '/documents', color: 'bg-emerald-500' },
{ title: '智能填表', desc: '自动填写表格模板', icon: TableProperties, link: '/form-fill', color: 'bg-indigo-500' },
{ title: 'AI 助手', desc: '自然语言交互', icon: MessageSquareCode, link: '/assistant', color: 'bg-amber-500' }
].map((item, i) => (
<Link
key={i}
to={item.link}
className="flex items-center gap-4 p-4 rounded-2xl border border-transparent hover:border-border hover:bg-muted/30 transition-all group"
>
<div className={cn("w-12 h-12 rounded-xl flex items-center justify-center text-white shadow-lg", item.color)}>
<item.icon size={24} />
</div>
<div>
<p className="font-semibold group-hover:text-primary transition-colors">{item.title}</p>
<p className="text-xs text-muted-foreground">{item.desc}</p>
</div>
<ArrowRight size={16} className="ml-auto opacity-0 group-hover:opacity-100 transition-opacity" />
</Link>
))}
</div>
</CardContent>
</Card>
</div>
{/* System Status */}
<Card className="border-none shadow-md">
<CardHeader className="pb-2">
<CardTitle className="text-xl flex items-center gap-2">
<Database className="text-primary" size={20} />
</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="flex items-center gap-3 p-4 rounded-xl bg-muted/30">
<div className="w-3 h-3 rounded-full bg-emerald-500" />
<div>
<p className="font-semibold text-sm">MySQL</p>
<p className="text-xs text-muted-foreground"></p>
</div>
</div>
<div className="flex items-center gap-3 p-4 rounded-xl bg-muted/30">
<div className="w-3 h-3 rounded-full bg-emerald-500" />
<div>
<p className="font-semibold text-sm">MongoDB</p>
<p className="text-xs text-muted-foreground"></p>
</div>
</div>
<div className="flex items-center gap-3 p-4 rounded-xl bg-muted/30">
<div className="w-3 h-3 rounded-full bg-emerald-500" />
<div>
<p className="font-semibold text-sm">Faiss + RAG</p>
<p className="text-xs text-muted-foreground"></p>
</div>
</div>
</div>
</CardContent>
</Card>
</div>
);
};
export default Dashboard;