Files
ai-contract-platform/frontend/src/app/workspace/page.tsx
T

448 lines
25 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.
"use client";
import Link from "next/link";
import { useEffect, useMemo, useState } from "react";
import { Empty, Space, Spin, Tag, message } from "antd";
import { listGeneratedContracts, type GeneratedContractListItem } from "@/services/contract";
import { getReviews, type ReviewListItem } from "@/services/review";
import { useAuthStore } from "@/stores/auth.store";
import {
formatAutoReviewStatus,
formatContractType,
formatDateTime,
formatReviewAs,
formatRisk,
formatStatus,
getRiskColor,
getStatusColor,
} from "@/lib/format";
const workspaceUtilityLinks = [
{ href: "/files", label: "合同仓库", description: "查看文件、预览材料并关联合同。", eyebrow: "Files" },
{ href: "/search", label: "统一搜索", description: "按合同、审查、文件和模板快速定位。", eyebrow: "Search" },
{ href: "/reviews", label: "审查记录", description: "查看任务状态、风险等级,并进入详情页。", eyebrow: "Review History" },
{ href: "/chat", label: "法律问答", description: "围绕法条、风险点和修改建议继续追问。", eyebrow: "Legal Chat" },
{ href: "/contract-ledger", label: "合同台账", description: "集中查看生命周期、到期和逾期事项。", eyebrow: "Ledger" },
];
const draftingFlowItems = ["选择模板", "填写关键内容", "生成合同", "自动预审"];
type WorkspaceNotificationItem = {
id: string;
href: string;
title: string;
contract_title?: string | null;
description?: string | null;
priority?: "critical" | "high" | "medium" | "low" | string;
is_mine?: boolean;
};
function isCurrentMonth(value: string) {
const date = new Date(value);
const now = new Date();
return date.getFullYear() === now.getFullYear() && date.getMonth() === now.getMonth();
}
function buildStats(items: ReviewListItem[]) {
return {
totalReviews: items.length,
monthlyReviews: items.filter((item) => isCurrentMonth(item.created_at)).length,
highRisk: items.filter((item) => item.risk_level === "high").length,
mediumRisk: items.filter((item) => item.risk_level === "medium").length,
lowRisk: items.filter((item) => item.risk_level === "low").length,
pendingCount: items.filter((item) => item.status === "pending" || item.status === "processing").length,
failedCount: items.filter((item) => item.status === "failed" || item.status === "partial").length,
completedCount: items.filter((item) => item.status === "completed").length,
};
}
function buildContractStats(items: GeneratedContractListItem[]) {
return {
totalContracts: items.length,
monthlyContracts: items.filter((item) => isCurrentMonth(item.generated_at)).length,
autoReviewCompleted: items.filter((item) => item.auto_review?.status === "completed").length,
autoReviewProcessing: items.filter((item) => item.auto_review?.status === "processing" || item.auto_review?.status === "pending").length,
autoReviewFailed: items.filter((item) => item.auto_review?.status === "failed" || item.auto_review?.status === "partial").length,
highRiskContracts: items.filter((item) => item.auto_review?.risk_level === "high").length,
};
}
function buildPriorityTasks(items: ReviewListItem[]) {
const highRiskItems = items.filter((item) => item.risk_level === "high").slice(0, 2);
const blockedItems = items.filter((item) => item.status === "failed" || item.status === "partial").slice(0, 2);
const processingItems = items.filter((item) => item.status === "pending" || item.status === "processing").slice(0, 2);
return [
...highRiskItems.map((item) => ({ id: item.id, href: `/reviews/${item.id}`, label: "高风险优先处理", title: item.title, meta: `${formatContractType(item.contract_type)} · ${formatRisk(item.risk_level)}` })),
...blockedItems.map((item) => ({ id: item.id, href: `/reviews/${item.id}`, label: "异常任务待处理", title: item.title, meta: `${formatContractType(item.contract_type)} · ${formatStatus(item.status)}` })),
...processingItems.map((item) => ({ id: item.id, href: `/reviews/${item.id}`, label: "处理中任务跟进", title: item.title, meta: `${formatContractType(item.contract_type)} · ${formatStatus(item.status)}` })),
].slice(0, 4);
}
function buildContractPriorityTasks(items: GeneratedContractListItem[]) {
const highRiskItems = items.filter((item) => item.auto_review?.risk_level === "high").slice(0, 2);
const pendingItems = items
.filter((item) => item.auto_review?.status === "processing" || item.auto_review?.status === "pending")
.slice(0, 2);
const failedItems = items
.filter((item) => item.auto_review?.status === "failed" || item.auto_review?.status === "partial")
.slice(0, 2);
return [
...highRiskItems.map((item) => ({
id: item.id,
href: item.auto_review?.detail_url || (item.auto_review?.review_id ? `/reviews/${item.auto_review.review_id}` : `/contracts/${item.id}`),
label: "高风险合同待处理",
title: item.template_name || item.file_name,
meta: `${formatContractType(item.document_type_code || item.contract_type)} · ${formatRisk(item.auto_review?.risk_level)}`,
})),
...failedItems.map((item) => ({
id: item.id,
href: `/contracts/${item.id}`,
label: "预审异常待确认",
title: item.template_name || item.file_name,
meta: `${formatContractType(item.document_type_code || item.contract_type)} · ${formatAutoReviewStatus(item.auto_review?.status)}`,
})),
...pendingItems.map((item) => ({
id: item.id,
href: `/contracts/${item.id}`,
label: "自动预审处理中",
title: item.template_name || item.file_name,
meta: `${formatContractType(item.document_type_code || item.contract_type)} · ${formatAutoReviewStatus(item.auto_review?.status)}`,
})),
].slice(0, 4);
}
function buildWorkspaceInsights(items: ReviewListItem[]) {
const latestHighRisk = items.find((item) => item.risk_level === "high");
const latestFailed = items.find((item) => item.status === "failed" || item.status === "partial");
const latestProcessing = items.find((item) => item.status === "pending" || item.status === "processing");
return [
latestHighRisk ? { label: "最高优先级", title: latestHighRisk.title, description: "当前存在高风险任务,建议优先进入详情页处理。", href: `/reviews/${latestHighRisk.id}` } : null,
latestFailed ? { label: "异常任务", title: latestFailed.title, description: "当前存在失败/部分完成任务,建议先定位卡点。", href: `/reviews/${latestFailed.id}` } : null,
latestProcessing ? { label: "处理中任务", title: latestProcessing.title, description: "当前仍有任务处理中,可继续跟进阶段状态。", href: `/reviews/${latestProcessing.id}` } : null,
].filter((item): item is { label: string; title: string; description: string; href: string } => item !== null);
}
function buildGettingStartedLinks(role: string | undefined) {
if (role === "admin") return [
{ href: "/contracts", label: "先起草一份合同", description: "验证模板起草、生成和自动预审主链路。" },
{ href: "/review", label: "上传已有合同审查", description: "验证客户已有合同的审查与报告交付。" },
{ href: "/templates", label: "准备合同模板", description: "让生成链路进入可重复使用状态。" },
{ href: "/knowledge-bases", label: "上传首批知识文档", description: "为问答和审查补齐依据来源。" },
];
return [
{ href: "/contracts", label: "先起草一份合同", description: "选择模板、填写关键内容并生成合同。" },
{ href: "/review", label: "上传已有合同审查", description: "上传合同文件或文本,完成第一次审查。" },
{ href: "/reviews", label: "查看审查记录", description: "从结果详情进入后续处理工作流。" },
{ href: "/chat", label: "继续使用审查助手", description: "围绕风险项、条款和建议继续追问。" },
];
}
export default function WorkspacePage() {
const [messageApi, contextHolder] = message.useMessage();
const currentUser = useAuthStore((state) => state.currentUser);
const [loading, setLoading] = useState(true);
const [reviews, setReviews] = useState<ReviewListItem[]>([]);
const [contracts, setContracts] = useState<GeneratedContractListItem[]>([]);
useEffect(() => {
const timer = setTimeout(async () => {
if (!currentUser) { setLoading(false); return; }
try {
const [reviewResult, contractResult] = await Promise.all([
getReviews(),
listGeneratedContracts({ page: 1, page_size: 100 }),
]);
if (reviewResult.code !== 0) { messageApi.error(reviewResult.message || "审查任务加载失败"); return; }
if (contractResult.code !== 0) { messageApi.error(contractResult.message || "合同起草记录加载失败"); return; }
setReviews(reviewResult.data.items);
setContracts(contractResult.data.items);
} catch { messageApi.error("工作台数据加载失败,请稍后重试"); }
finally { setLoading(false); }
}, 0);
return () => clearTimeout(timer);
}, [currentUser, messageApi]);
const stats = useMemo(() => buildStats(reviews), [reviews]);
const contractStats = useMemo(() => buildContractStats(contracts), [contracts]);
const contractReminderItems = useMemo(() => {
return contracts
.filter((item) => item.ledger_attention?.is_mine || item.lifecycle?.due_status === "due_soon" || item.lifecycle?.due_status === "overdue")
.sort((a, b) => Number(Boolean(b.ledger_attention?.is_mine)) - Number(Boolean(a.ledger_attention?.is_mine)))
.slice(0, 4);
}, [contracts]);
const notifications: WorkspaceNotificationItem[] = [];
const myContractTodoCount = contracts.filter((item) => item.ledger_attention?.is_mine).length;
const recentReviews = useMemo(() => reviews.slice(0, 5), [reviews]);
const recentContracts = useMemo(() => contracts.slice(0, 5), [contracts]);
const latestReview = recentReviews[0];
const latestContract = recentContracts[0];
const hasReviews = reviews.length > 0;
const hasContracts = contracts.length > 0;
const priorityTasks = useMemo(() => buildPriorityTasks(reviews), [reviews]);
const contractPriorityTasks = useMemo(() => buildContractPriorityTasks(contracts), [contracts]);
const adminWorkbenchLinks = useMemo(() => {
if (currentUser?.role !== "admin") return [];
return [
{ href: "/admin", label: "管理工作台", description: "统一进入后台治理、知识库、规则、模板与试点交付模块。" },
{ href: "/governance", label: "治理中心", description: "查看复核、证据、上线批准与门禁汇总。" },
{ href: "/knowledge-bases", label: "知识库管理", description: "维护文档生命周期和问答资料。" },
{ href: "/model-settings", label: "大模型设置", description: "维护模型供应商、密钥和运行时部署参数。" },
{ href: "/rules", label: "审查规则管理", description: "维护规则与风险识别逻辑。" },
{ href: "/templates", label: "模板管理", description: "维护合同模板与默认模板集。" },
{ href: "/audit-logs", label: "审计日志", description: "查看关键后台动作留痕。" },
{ href: "/pilot-readiness", label: "试点交付准备", description: "复查默认账号、演示链路和验收门禁。" },
];
}, [currentUser?.role]);
const workspaceInsights = useMemo(() => buildWorkspaceInsights(reviews), [reviews]);
const gettingStartedLinks = useMemo(() => buildGettingStartedLinks(currentUser?.role), [currentUser?.role]);
const activeTask = contractPriorityTasks[0] ?? priorityTasks[0] ?? null;
const dashboardHighlights = useMemo(() => [
...contractPriorityTasks.map((item) => ({ href: item.href, eyebrow: item.label, title: item.title, description: item.meta })),
...priorityTasks.map((item) => ({ href: item.href, eyebrow: item.label, title: item.title, description: item.meta })),
...workspaceInsights.map((item) => ({ href: item.href, eyebrow: item.label, title: item.title, description: item.description })),
].slice(0, activeTask ? 3 : 4), [activeTask, contractPriorityTasks, priorityTasks, workspaceInsights]);
const workflowLinks = useMemo(() => {
if (hasContracts || hasReviews) {
return workspaceUtilityLinks.slice(0, 4);
}
return [
...gettingStartedLinks.slice(2).map((item) => ({ ...item, eyebrow: "Next Step" })),
...workspaceUtilityLinks.slice(0, 2),
].slice(0, 4);
}, [gettingStartedLinks, hasContracts, hasReviews]);
const primaryAction = { href: "/contracts", label: "起草合同", description: "从模板选择、关键内容填写开始,生成后自动预审。" };
const secondaryAction = hasReviews || hasContracts
? { href: "/review", label: "上传已有合同审查", description: "直接上传 PDF / DOCX / TXT 或粘贴正文,适合审核客户现有合同。" }
: { href: "/review", label: "上传已有合同审查", description: "直接上传 PDF / DOCX / TXT 或粘贴正文,先完成一次审查闭环。" };
const workspaceHeadline = activeTask
? `你好,${currentUser?.display_name}。先处理待办,再快速发起。`
: `你好,${currentUser?.display_name}。先起草一份合同并启动自动预审。`;
const workspaceSubtitle = activeTask
? `当前优先任务:${activeTask.title}。也可以直接新建合同,完成选择模板、生成、自动预审和报告交付。`
: "工作台优先引导合同起草;如果客户已经有合同原文,也可以直接上传 PDF / DOCX / TXT 完成审查。";
const workspacePrimaryActions = useMemo(() => [
{ href: "/contracts", label: "合同起草", description: "选择模板、填写关键内容并生成合同。" },
{ href: "/review", label: "发起审查", description: "上传已有合同或粘贴正文进入审查。" },
], []);
const workspaceFocusItems = activeTask
? [
`优先任务:${activeTask.title}`,
`最近起草:${latestContract?.template_name || latestContract?.file_name || "暂无"}`,
`处理中任务:${stats.pendingCount} 个`,
]
: [
"先选择模板起草合同,验证生成和自动预审是否顺畅",
"如已有客户合同,直接上传审查并下载报告",
"生成结果可进入合同详情、审查详情和问答复核",
];
return (
<main className="landing-page">
{contextHolder}
<section className="landing-hero-shell">
<div className="landing-hero">
<span className="landing-badge">Contract Workspace</span>
<h1>{workspaceHeadline}</h1>
<p>{workspaceSubtitle}</p>
<div className="landing-actions">
<Link className="landing-primary-action" href={primaryAction.href}>{primaryAction.label}</Link>
<Link className="landing-secondary-action" href={secondaryAction.href}>{secondaryAction.label}</Link>
</div>
<div className="landing-command-visual" aria-label="合同审查智能流程">
<div className="landing-command-header">
<span>Draft & Review Pipeline</span>
<strong>{contractStats.autoReviewCompleted}/{contractStats.totalContracts || 0}</strong>
</div>
<div className="landing-command-flow">
{draftingFlowItems.map((item, index) => (
<div className="landing-command-step" key={item}>
<span>{String(index + 1).padStart(2, "0")}</span>
<strong>{item}</strong>
</div>
))}
</div>
<div className="landing-command-alerts">
<div><span>已起草</span><strong>{contractStats.totalContracts}</strong></div>
<div><span>预审中</span><strong>{contractStats.autoReviewProcessing}</strong></div>
<div><span>高风险</span><strong>{contractStats.highRiskContracts}</strong></div>
</div>
</div>
<div className="landing-stats">
<div className="landing-stat-card"><strong>{contractStats.totalContracts}</strong><span>累计起草合同</span></div>
<div className="landing-stat-card"><strong>{contractStats.monthlyContracts}</strong><span>本月起草次数</span></div>
<div className="landing-stat-card"><strong>{contractStats.autoReviewCompleted}</strong><span>预审完成</span></div>
<div className="landing-stat-card"><strong>{stats.totalReviews}</strong><span>审查任务</span></div>
</div>
<div className="workspace-focus-strip">
{workspaceFocusItems.slice(0, 3).map((item) => (
<span key={item}>{item}</span>
))}
</div>
</div>
<div className="landing-hero-panel">
<div className="landing-panel-card landing-panel-card-primary">
<span className="landing-panel-eyebrow">Drafting Overview</span>
<strong>起草与预审统计</strong>
<ul className="landing-panel-list">
<li>累计起草:{contractStats.totalContracts}</li>
<li>自动预审完成:{contractStats.autoReviewCompleted}</li>
<li>预审异常:{contractStats.autoReviewFailed}</li>
</ul>
</div>
<div className="landing-panel-card">
<span className="landing-panel-eyebrow">Recent Contract</span>
<strong>最近起草合同</strong>
<p>{latestContract ? `${latestContract.template_name || latestContract.file_name} · ${formatDateTime(latestContract.generated_at)}` : "暂无最近起草合同"}</p>
{latestContract ? <Link href={`/contracts/${latestContract.id}`}>进入合同详情</Link> : <Link href="/contracts">新建合同</Link>}
</div>
<div className="landing-panel-card landing-panel-card-soft">
<span className="landing-panel-eyebrow">Session</span>
<strong>{currentUser?.role === "admin" ? "管理员视角" : "业务视角"}</strong>
<ul className="landing-panel-list landing-panel-list-soft">
<li>合同待办:{myContractTodoCount}</li>
<li>已完成任务:{stats.completedCount}</li>
<li>最近风险:{latestReview?.risk_level ? formatRisk(latestReview.risk_level) : "-"}</li>
</ul>
</div>
</div>
</section>
<section className="workspace-today-panel">
<div className="workspace-panel-head">
<div>
<h2>今日工作</h2>
<p>先处理待办,再进入起草或上传审查。</p>
</div>
<Link href="/contract-ledger">查看合同台账</Link>
</div>
<div className="workspace-today-grid">
<div className="workspace-task-column">
<div className="workspace-column-head">
<strong>待处理</strong>
<Tag color={myContractTodoCount ? "orange" : "default"}>{myContractTodoCount} 个待办</Tag>
</div>
{notifications.length === 0 && contractReminderItems.length === 0 && dashboardHighlights.length === 0 ? (
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="当前没有需要优先处理的任务。" />
) : (
<div className="workspace-compact-list">
{notifications.slice(0, 4).map((item) => (
<Link className="workspace-compact-row" href={item.href} key={item.id}>
<span>{item.title}</span>
<strong>{item.contract_title ?? item.title}</strong>
<small>{item.description ?? "请进入详情继续处理。"}</small>
<Space wrap size={6}>
<Tag color={item.priority === "critical" ? "red" : item.priority === "high" ? "orange" : "blue"}>
{item.priority === "critical" ? "紧急" : item.priority === "high" ? "高优先级" : "提醒"}
</Tag>
{item.is_mine ? <Tag color="orange">待我处理</Tag> : null}
</Space>
</Link>
))}
{notifications.length === 0 ? contractReminderItems.slice(0, 3).map((item) => (
<Link className="workspace-compact-row" href={`/contracts/${item.id}`} key={item.id}>
<span>{item.ledger_attention?.primary?.label ?? "合同提醒"}</span>
<strong>{item.template_name}</strong>
<small>{item.ledger_attention?.primary?.description ?? "请进入合同详情继续处理生命周期事项。"}</small>
<Space wrap size={6}>
{item.ledger_attention?.is_mine ? <Tag color="orange">待我处理</Tag> : null}
<Tag>{item.lifecycle?.due_status === "overdue" ? "已逾期" : item.lifecycle?.due_status === "due_soon" ? "即将到期" : "生命周期"}</Tag>
</Space>
</Link>
)) : null}
{dashboardHighlights.slice(0, notifications.length || contractReminderItems.length ? 1 : 3).map((item) => (
<Link className="workspace-compact-row" href={item.href} key={`${item.eyebrow}-${item.href}`}>
<span>{item.eyebrow}</span>
<strong>{item.title}</strong>
<small>{item.description}</small>
</Link>
))}
</div>
)}
</div>
<div className="workspace-flow-column">
<div className="workspace-column-head">
<strong>快速发起</strong>
<Tag color="blue">{workspacePrimaryActions.length} 个入口</Tag>
</div>
<div className="workspace-flow-list">
{workspacePrimaryActions.map((item, index) => (
<Link className="workspace-flow-row" href={item.href} key={item.href}>
<span>{index + 1}</span>
<div>
<strong>{item.label}</strong>
<small>{item.description}</small>
</div>
</Link>
))}
</div>
</div>
</div>
</section>
<section className="landing-section workspace-tools-section">
<div className="landing-section-header workspace-section-header-compact">
<h2>常用入口</h2>
<p>快速发起之外的资料、搜索和问答集中放在这里。</p>
</div>
<div className="workspace-tool-grid">
{workflowLinks.map((item) => (
<Link className="workspace-tool-link" href={item.href} key={`${item.eyebrow}-${item.href}`}>
<span className="landing-card-eyebrow">{item.eyebrow}</span>
<strong>{item.label}</strong>
<small>{item.description}</small>
</Link>
))}
</div>
</section>
{adminWorkbenchLinks.length ? (
<section className="landing-section workspace-admin-section">
<div className="landing-section-header workspace-section-header-compact">
<h2>管理员入口</h2>
<p>后台治理入口保持可达,但不抢占日常业务操作。</p>
</div>
<div className="workspace-admin-strip">
{adminWorkbenchLinks.map((item) => (
<Link className="workspace-admin-link" href={item.href} key={item.href}>
<strong>{item.label}</strong>
<small>{item.description}</small>
</Link>
))}
</div>
</section>
) : null}
<section className="landing-section">
<div className="landing-section-header">
<h2>最近审查记录</h2>
<p>直接给出最近审查任务,减少反复跳去历史页的成本。</p>
</div>
{loading ? (
<div style={{ padding: 32, textAlign: "center" }}><Spin /></div>
) : recentReviews.length === 0 ? (
<Empty description="还没有审查记录,先发起第一份合同审查。" />
) : (
<div className="landing-grid">
{recentReviews.slice(0, 3).map((item) => (
<Link className="landing-card" href={`/reviews/${item.id}`} key={item.id}>
<span className="landing-card-eyebrow">{formatDateTime(item.created_at)}</span>
<strong>{item.title}</strong>
<span>{formatContractType(item.contract_type)} · {formatReviewAs(item.review_as)}</span>
<Space wrap>
<Tag color={getStatusColor(item.status)}>{formatStatus(item.status)}</Tag>
<Tag color={item.risk_level ? getRiskColor(item.risk_level) : "default"}>
{item.risk_level ? formatRisk(item.risk_level) : "未评级"}
</Tag>
</Space>
</Link>
))}
</div>
)}
</section>
</main>
);
}