Files
DEMO-AGENT/docs/原型设计/registration-prototype-demo.html

1804 lines
59 KiB
HTML
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.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>审核 Agent 原型演示站</title>
<style>
:root {
--bg: #f4f7fc;
--panel: #ffffff;
--panel-soft: #f8fbff;
--ink: #1f2f46;
--muted: #7c8ba5;
--line: #dfE8f5;
--line-strong: #c9d8ee;
--accent: #3a73d9;
--accent-soft: #eaf2ff;
--nav-bg: #ffffff;
--sidebar-bg: linear-gradient(180deg, #edf4ff 0%, #e7f0ff 36%, #eef4ff 100%);
--success: #166534;
--success-bg: #ecfdf3;
--warning: #92400e;
--warning-bg: #fff7ed;
--danger: #b42318;
--danger-bg: #fef3f2;
--shadow: 0 10px 28px rgba(52, 89, 152, 0.08);
--radius-lg: 12px;
--radius-md: 10px;
--radius-sm: 8px;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: "Microsoft YaHei", "PingFang SC", "Segoe UI", sans-serif;
color: var(--ink);
background: var(--bg);
}
button,
input,
textarea {
font: inherit;
}
button {
cursor: pointer;
border: 0;
background: none;
}
.app {
min-height: 100vh;
display: grid;
grid-template-rows: auto 1fr;
}
.topbar {
position: sticky;
top: 0;
z-index: 10;
background: rgba(255, 255, 255, 0.98);
backdrop-filter: blur(12px);
border-bottom: 1px solid var(--line);
}
.topbar-inner {
max-width: 100%;
margin: 0 auto;
padding: 0 18px;
display: flex;
align-items: stretch;
justify-content: space-between;
gap: 20px;
min-height: 64px;
}
.brand-row {
display: flex;
align-items: center;
gap: 18px;
min-width: 0;
}
.brand h1 {
margin: 0;
font-size: 18px;
line-height: 1.2;
font-weight: 700;
}
.brand p {
margin: 4px 0 0;
color: var(--muted);
font-size: 12px;
}
.summary-row {
display: flex;
flex-wrap: wrap;
gap: 10px;
align-items: center;
}
.chip,
.status-tag,
.mini-tag {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 10px;
border-radius: 999px;
border: 1px solid var(--line);
background: #fff;
color: var(--muted);
font-size: 12px;
font-weight: 600;
}
.status-tag.success {
color: var(--success);
background: var(--success-bg);
border-color: #cce9d5;
}
.status-tag.warning {
color: var(--warning);
background: var(--warning-bg);
border-color: #f1d7b8;
}
.status-tag.danger {
color: var(--danger);
background: var(--danger-bg);
border-color: #f0c6c3;
}
.content {
max-width: 100%;
width: 100%;
margin: 0;
padding: 18px;
}
.page {
display: none;
animation: fadeIn 0.2s ease;
}
.page.active {
display: block;
}
.agent-layout {
display: grid;
grid-template-columns: 302px minmax(0, 1fr) 340px;
gap: 18px;
min-height: calc(100vh - 100px);
}
.panel {
background: var(--panel);
border: 1px solid var(--line);
border-radius: var(--radius-lg);
box-shadow: var(--shadow);
}
.panel-head {
padding: 18px 18px 12px;
border-bottom: 1px solid var(--line);
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 12px;
}
.panel-head h2,
.panel-head h3,
.panel-head h4 {
margin: 0;
font-size: 16px;
line-height: 1.4;
}
.panel-head p {
margin: 6px 0 0;
color: var(--muted);
font-size: 13px;
line-height: 1.6;
}
.panel-body {
padding: 18px;
}
.history-list,
.node-list,
.card-list,
.record-list,
.queue-list {
display: grid;
gap: 12px;
}
.history-item,
.record-item,
.queue-item,
.node-item,
.ability-card,
.kb-card,
.crud-card {
border: 1px solid var(--line);
border-radius: var(--radius-md);
background: var(--panel-soft);
}
.history-item {
padding: 14px;
display: grid;
gap: 8px;
cursor: pointer;
}
.history-item.active {
border-color: #b9cdf5;
background: linear-gradient(180deg, #eef4ff 0%, #e5efff 100%);
box-shadow: inset 0 0 0 1px #c7d8f4;
}
.history-item strong,
.record-item strong,
.queue-item strong,
.ability-card strong,
.kb-card strong,
.crud-card strong {
font-size: 14px;
line-height: 1.5;
}
.muted {
color: var(--muted);
font-size: 12px;
line-height: 1.6;
}
.meta-row {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
}
.conversation-shell {
display: grid;
grid-template-rows: auto auto auto 1fr auto;
min-height: 100%;
}
.hero-center {
padding: 54px 24px 24px;
text-align: center;
display: grid;
gap: 18px;
justify-items: center;
}
.hero-badge {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
border-radius: 999px;
background: var(--accent);
color: #fff;
font-size: 13px;
box-shadow: 0 10px 20px rgba(58, 115, 217, 0.18);
}
.hero-center h2 {
margin: 0;
font-size: 28px;
line-height: 1.35;
color: #17315f;
}
.hero-center p {
max-width: 760px;
margin: 0;
color: var(--muted);
font-size: 15px;
line-height: 1.9;
}
.hero-tags {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 10px;
}
.hero-note-row {
display: grid;
grid-template-columns: 1.4fr 0.8fr;
gap: 12px;
width: min(880px, 100%);
}
.hero-note-card {
padding: 16px;
border-radius: 14px;
border: 1px solid #d8e4f8;
background: linear-gradient(180deg, #f6f9ff 0%, #eef4ff 100%);
text-align: left;
}
.hero-note-card strong {
display: block;
margin-bottom: 8px;
color: #2453a6;
font-size: 14px;
}
.prompt-rail {
padding: 14px 18px 0;
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.prompt-chip {
padding: 8px 12px;
border-radius: 999px;
border: 1px solid #dbe6fb;
background: #eef4ff;
color: #4a73b8;
font-size: 12px;
}
.node-strip {
padding: 14px 18px 0;
display: flex;
gap: 10px;
overflow-x: auto;
}
.node-item {
min-width: 180px;
padding: 12px;
text-align: left;
cursor: pointer;
background: #fff;
}
.node-item.active {
border-color: #bfd2f5;
background: #eef4ff;
}
.conversation-board {
padding: 12px 18px 18px;
display: grid;
gap: 14px;
align-content: start;
overflow: auto;
}
.message {
max-width: 86%;
padding: 14px 16px;
border-radius: 14px;
border: 1px solid var(--line);
background: #fff;
}
.message.user {
margin-left: auto;
background: #eef4ff;
border-color: #d6e2f8;
}
.message.agent {
margin-right: auto;
background: #ffffff;
}
.message.system {
margin-right: auto;
background: #f8fbff;
}
.message-head {
display: flex;
justify-content: space-between;
gap: 10px;
margin-bottom: 8px;
font-size: 12px;
color: var(--muted);
}
.message-body p {
margin: 0 0 8px;
line-height: 1.7;
font-size: 14px;
}
.message-body p:last-child {
margin-bottom: 0;
}
.composer {
border-top: 1px solid var(--line);
padding: 18px;
display: grid;
gap: 12px;
background: linear-gradient(180deg, rgba(244,247,252,0) 0%, #f7faff 100%);
}
.composer-box {
min-height: 58px;
border: 1px solid #d7e3f8;
border-radius: 999px;
background: #fff;
padding: 16px 20px;
color: var(--muted);
line-height: 1.4;
display: flex;
align-items: center;
}
.composer-actions,
.action-row,
.crud-actions,
.table-actions {
display: flex;
flex-wrap: wrap;
gap: 10px;
align-items: center;
}
.primary-btn,
.secondary-btn,
.ghost-btn,
.mini-btn {
border-radius: 10px;
padding: 10px 14px;
font-size: 13px;
font-weight: 600;
border: 1px solid #d4def2;
background: #fff;
color: #345383;
}
.primary-btn {
background: linear-gradient(180deg, #4f86e8 0%, #336fd7 100%);
border-color: #336fd7;
color: #fff;
}
.ghost-btn {
background: transparent;
}
.mini-btn {
padding: 7px 10px;
font-size: 12px;
}
.side-stack {
display: grid;
gap: 18px;
align-content: start;
}
.upload-box {
border: 1px dashed #c8d8f2;
border-radius: var(--radius-md);
padding: 16px;
background: linear-gradient(180deg, #f9fbff 0%, #f1f6ff 100%);
display: grid;
gap: 12px;
}
.upload-file {
padding: 12px;
border-radius: var(--radius-sm);
border: 1px solid var(--line);
background: #fff;
display: grid;
gap: 6px;
}
.ability-card,
.kb-card,
.crud-card,
.record-item,
.queue-item {
padding: 14px;
display: grid;
gap: 10px;
background: #fff;
}
.ability-card.active {
border-color: #bfd2f5;
background: linear-gradient(180deg, #f5f9ff 0%, #eef4ff 100%);
}
.metric-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 14px;
margin-bottom: 18px;
}
.metric-card {
background: #fff;
border: 1px solid var(--line);
border-radius: var(--radius-md);
padding: 16px;
box-shadow: var(--shadow);
}
.metric-card strong {
display: block;
margin-top: 10px;
font-size: 26px;
line-height: 1;
}
.knowledge-layout,
.history-layout {
display: grid;
gap: 18px;
}
.knowledge-layout {
grid-template-columns: 1.2fr 0.8fr;
}
.kb-grid,
.crud-grid,
.history-grid {
display: grid;
gap: 14px;
}
.kb-grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.crud-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.table-shell {
overflow: auto;
border: 1px solid var(--line);
border-radius: var(--radius-md);
}
table {
width: 100%;
border-collapse: collapse;
background: #fff;
}
th,
td {
padding: 12px 14px;
border-bottom: 1px solid var(--line);
text-align: left;
font-size: 13px;
vertical-align: top;
}
th {
background: #fafafa;
color: var(--muted);
font-weight: 600;
}
.process-steps {
display: grid;
gap: 10px;
}
.step-card {
padding: 12px 14px;
border-radius: var(--radius-md);
border: 1px solid var(--line);
background: #fff;
display: grid;
grid-template-columns: 1fr auto;
gap: 12px;
align-items: center;
}
.step-card.running {
background: #fffbeb;
border-color: #f3d9a4;
}
.step-card.completed {
background: #f0fdf4;
border-color: #cce9d5;
}
.step-card.pending {
background: #fff;
}
.step-card.blocked {
background: #fef3f2;
border-color: #f0c6c3;
}
.record-item {
grid-template-columns: 1.2fr 0.8fr;
align-items: start;
}
.record-main,
.record-side {
display: grid;
gap: 10px;
}
.search-bar {
display: grid;
grid-template-columns: 1fr auto auto;
gap: 10px;
margin-bottom: 18px;
}
.search-input {
padding: 11px 14px;
border-radius: 10px;
border: 1px solid #d4def2;
background: #fff;
color: var(--ink);
}
.search-input::placeholder {
color: #9aa8bf;
}
.system-nav {
display: flex;
align-items: stretch;
gap: 2px;
justify-content: center;
flex: 1;
margin-left: 0;
}
.topbar-user {
display: flex;
align-items: center;
gap: 10px;
margin-left: 18px;
white-space: nowrap;
}
.system-nav-btn {
min-height: 64px;
padding: 0 16px;
display: inline-flex;
align-items: center;
color: #3b4e6d;
border-bottom: 2px solid transparent;
font-size: 15px;
}
.system-nav-btn.active {
background: #eef4ff;
color: var(--accent);
border-color: #c5d7f6;
}
.history-panel {
background: var(--sidebar-bg);
border-color: #dbe7fb;
overflow: hidden;
}
.history-panel .panel-head,
.history-panel .panel-body {
border-bottom-color: rgba(201, 216, 238, 0.9);
}
.history-toolbar {
display: grid;
gap: 12px;
}
.search-shell {
padding: 10px 12px;
border-radius: 10px;
border: 1px solid #d5e1f5;
background: rgba(255,255,255,0.86);
color: #8a98b3;
font-size: 13px;
}
.history-caption {
padding: 10px 12px;
border-radius: 10px;
background: rgba(215, 229, 255, 0.72);
color: #4b73b5;
font-size: 13px;
text-align: center;
font-weight: 600;
}
.history-footer {
margin-top: 16px;
padding-top: 16px;
border-top: 1px solid rgba(201, 216, 238, 0.9);
display: flex;
align-items: center;
gap: 12px;
}
.history-avatar {
width: 38px;
height: 38px;
border-radius: 50%;
background: linear-gradient(180deg, #5d8ee9 0%, #3c74db 100%);
color: #fff;
display: grid;
place-items: center;
font-weight: 700;
box-shadow: 0 8px 18px rgba(58, 115, 217, 0.2);
}
.topbar-avatar {
width: 34px;
height: 34px;
border-radius: 50%;
background: linear-gradient(180deg, #5d8ee9 0%, #3c74db 100%);
color: #fff;
display: grid;
place-items: center;
font-weight: 700;
box-shadow: 0 8px 18px rgba(58, 115, 217, 0.18);
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(4px); }
to { opacity: 1; transform: translateY(0); }
}
@media (max-width: 1280px) {
.agent-layout {
grid-template-columns: 240px minmax(0, 1fr) 300px;
}
.kb-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.system-nav {
display: none;
}
}
@media (max-width: 1080px) {
.agent-layout,
.knowledge-layout,
.record-item,
.crud-grid {
grid-template-columns: 1fr;
}
.metric-grid,
.kb-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.hero-note-row {
grid-template-columns: 1fr;
}
}
@media (max-width: 720px) {
.content,
.topbar-inner {
padding-left: 16px;
padding-right: 16px;
}
.topbar-inner,
.brand-row {
flex-wrap: wrap;
}
.metric-grid,
.kb-grid {
grid-template-columns: 1fr;
}
.search-bar {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<div class="app">
<header class="topbar">
<div class="topbar-inner">
<div class="brand-row">
<div class="brand">
<h1>试剂盒临床注册文件准备与审核 Agent</h1>
<p>以对话为核心的人机协同审核原型,聚焦上传、知识库、留痕与结构化结论。</p>
</div>
<nav class="system-nav">
<button class="system-nav-btn" data-top-nav="chat">审核智能体</button>
<button class="system-nav-btn" data-top-nav="materials">资料包</button>
<button class="system-nav-btn" data-top-nav="knowledge">知识库</button>
<button class="system-nav-btn" data-top-nav="history">处理历史</button>
</nav>
<div class="topbar-user">
<span class="muted" style="font-size:13px;">admin1</span>
<span class="topbar-avatar">A</span>
</div>
</div>
</div>
</header>
<main class="content">
<div id="pages"></div>
</main>
</div>
<script>
const data = {
conversations: [
{
id: "conv-001",
packageId: "pkg-001",
productName: "新型冠状病毒 2019-nCoV 核酸检测试剂盒",
title: "新型冠状病毒 2019-nCoV 核酸检测试剂盒",
time: "今天 10:05",
status: "高风险",
summary: "已完成目录汇总、完整性检查、字段抽取与一致性核查。",
messages: [
{
role: "user",
title: "用户",
meta: "10:05",
body: [
"请帮我审核这一批注册资料,先汇总目录和页数,再检查完整性,之后抽取字段并判断是否允许正式导出。"
]
},
{
role: "agent",
title: "Agent",
meta: "10:05 · 计划",
body: [
"我会按 4 个节点执行:资料目录汇总 -> 法规完整性检查 -> 字段抽取与一致性核查 -> 风险结论与导出建议。",
"你可以点击上方节点快速跳转到对应阶段的对话与结构化结果。"
]
},
{
role: "system",
title: "节点结果",
meta: "10:07 · 目录汇总",
body: [
"已识别 18 份文件,共 236 页,章节点覆盖 CH1 至 CH6。",
"发现 1 份 DOC 页数待人工复核1 份资料疑似目录错放。"
]
},
{
role: "agent",
title: "Agent",
meta: "10:09 · 完整性检查",
body: [
"基于法规规则包检查后,发现 CH1.11.4 缺失授权声明类资料,判定为高风险缺失项。",
"同时识别到 CH1.9 沟通说明位置疑似错放。"
]
},
{
role: "agent",
title: "Agent",
meta: "10:12 · 字段抽取",
body: [
"已抽取 13 个目标字段中的 12 个,产品名称在说明书与申请表之间存在表述差异。",
"储存条件字段置信度较低,已标记待复核。"
]
},
{
role: "system",
title: "结论",
meta: "10:15 · 风险结论",
body: [
"综合结论为高风险,不允许正式导出,仅允许生成草稿版用于内部校对。",
"建议先补齐 CH1.11.4 资料,并确认产品名称采用值。"
]
}
],
nodes: [
{ id: "node-import", title: "目录汇总", state: "completed", summary: "18 份文件 / 236 页 / 2 个异常" },
{ id: "node-completeness", title: "完整性检查", state: "completed", summary: "1 缺失 / 1 错放 / 高风险" },
{ id: "node-fields", title: "字段抽取", state: "running", summary: "12 / 13 已抽取 / 1 待复核" },
{ id: "node-risk", title: "风险结论", state: "blocked", summary: "正式导出受阻" }
],
abilities: {
import: {
title: "自动汇总文件夹目录与页数",
desc: "适合在上传资料后快速建立批次目录视图、页数统计和章节点识别结果。",
details: [
"当前识别结果18 份文件 / 236 页 / 14 个章节点命中",
"待复核异常1 份 DOC 页数、1 份目录错放、1 份 OCR 质量偏低",
"可生成结果registration_overview_report"
]
},
completeness: {
title: "执行法规完整性检查",
desc: "调用规则包对 CH1~CH6 必交项进行对照,输出缺失项、错放项和法规依据。",
details: [
"发现缺失CH1.11.4 授权书 / 声明类文件",
"发现错放CH1.9 产品申报前沟通说明",
"法规依据:体外诊断试剂注册申报资料要求及说明"
]
},
fields: {
title: "抽取字段并生成字段池",
desc: "从说明书、申请表和临床资料中抽取字段,沉淀来源、置信度和待复核状态。",
details: [
"已抽取字段:产品名称、适用范围、检测靶标、储存条件等",
"冲突字段:产品名称",
"待复核字段:储存条件"
]
},
export: {
title: "给出回填导出建议",
desc: "在完成核查后决定是否允许 Word 正式导出,并给出整改建议。",
details: [
"当前仅允许草稿导出",
"正式导出阻断原因:高风险缺失项 + 冲突字段",
"下一步:补资料、确认采用值、重新核查"
]
}
},
uploads: [
{ name: "注册资料批次_A.zip", meta: "压缩包 · 128MB · 已解包" },
{ name: "目标产品说明书.docx", meta: "DOCX · 18 页 · 已解析" },
{ name: "CH1.4 申请表.docx", meta: "DOCX · 5 页 · 已解析" }
]
},
{
id: "conv-002",
packageId: "pkg-002",
productName: "乙型肝炎病毒核酸检测试剂盒",
title: "乙型肝炎病毒核酸检测试剂盒",
time: "昨天 16:40",
status: "已完成",
summary: "生成目录、页数和章节点结果,未继续完整性核查。",
messages: [
{
role: "user",
title: "用户",
meta: "16:40",
body: [
"只帮我汇总这批文件目录和页数,暂时不做审核。"
]
},
{
role: "agent",
title: "Agent",
meta: "16:41",
body: [
"已完成目录汇总,共 12 份文件、148 页CH1 至 CH4 节点已识别。"
]
}
],
nodes: [
{ id: "node-only-import", title: "目录汇总", state: "completed", summary: "12 份文件 / 148 页" }
],
abilities: {},
uploads: [
{ name: "历史批次目录.zip", meta: "压缩包 · 92MB · 已归档" }
]
}
],
activeConversationId: "conv-001",
activeAbilityKey: "import",
materialSearchQuery: "",
knowledge: {
metrics: [
{ label: "知识库资料总数", value: "27", note: "法规、模板、示例资料、内部 SOP" },
{ label: "待处理任务", value: "3", note: "2 个入库中1 个待复核" },
{ label: "切片总数", value: "486", note: "支持 RAG 检索与解释" },
{ label: "启用规则包", value: "4", note: "完整性检查与一致性核查均已启用" }
],
queue: [
{ name: "体外诊断试剂注册申报资料要求及说明_2026修订版.docx", type: "法规资料", state: "文档解析中", detail: "正在识别条款标题、章节层级和必交项关键词。" },
{ name: "说明书模板映射补充包.zip", type: "模板资料", state: "解析完成", detail: "已识别 12 个占位符映射,待写入模板库。" },
{ name: "注册申报常见问题汇编.docx", type: "业务知识", state: "切片处理中", detail: "正在生成切片、关键词、召回标签与来源索引。" },
{ name: "临床评价参考示例.pdf", type: "示例资料", state: "处理完成", detail: "已入知识库,支持问答检索和示例引用。" }
],
cards: [
{ title: "法规规则包", value: "4", desc: "用于完整性检查、风险判定和一致性规则。" },
{ title: "RAG 文档源", value: "27", desc: "支持手动上传法规原文、模板文档和业务资料。" },
{ title: "切片与召回", value: "486", desc: "管理切片、召回阈值、命中场景和是否启用。" },
{ title: "字段 Schema", value: "38", desc: "维护统一字段标准、强一致字段和回填约束。" },
{ title: "模板与映射", value: "9", desc: "维护 Word 模板、占位符映射和导出阻断条件。" },
{ title: "通知配置", value: "12", desc: "维护责任人映射、飞书群聊和消息模板。" }
],
crudGroups: [
{
title: "知识库资料 CRUD",
desc: "手动上传、编辑元数据、停用、删除、重新入库。",
actions: ["上传资料", "编辑资料", "停用资料", "删除资料", "重新入库"]
},
{
title: "切片解析 CRUD",
desc: "查看切片、手工新增、拆分、合并、删除、重建向量。",
actions: ["查看切片", "新增切片", "拆分切片", "合并切片", "删除切片", "重建向量"]
},
{
title: "规则包 CRUD",
desc: "新增规则包、复制版本、启停版本、编辑适用流程与风险等级。",
actions: ["新增规则包", "编辑规则", "复制版本", "启用/停用", "删除草稿"]
},
{
title: "模板映射 CRUD",
desc: "上传模板、维护字段映射、预览占位符和回填约束。",
actions: ["上传模板", "编辑映射", "预览模板", "停用模板", "删除草稿"]
}
],
sourceRows: [
["体外诊断试剂注册申报资料要求及说明.docx", "法规资料", "2026-06-03", "已启用", "124", "法规管理员"],
["目标产品说明书.docx", "业务资料", "演示版", "已启用", "36", "注册经理"],
["注册申报常见问题汇编.docx", "业务知识", "2026-Q2", "处理中", "待生成", "数据治理专员"]
],
chunkRows: [
["chunk-001", "法规要求说明", "CH1", "声明类资料提交要求", "438", "可召回"],
["chunk-104", "目标产品说明书", "适用范围", "定性检测场景描述", "286", "可召回"],
["chunk-208", "常见问题汇编", "FAQ-导出", "正式导出阻断示例", "174", "处理中"]
]
},
history: {
records: [
{
batchId: "SUB-20260603-001",
title: "注册批次主审核",
time: "2026-06-03 10:05",
status: "高风险",
summary: "完成目录汇总、完整性检查、字段抽取和一致性核查,正式导出被阻断。",
files: "18 份文件 / 236 页",
result: "缺失 CH1.11.4,产品名称冲突,草稿可导出",
trace: ["目录汇总", "完整性检查", "字段抽取", "一致性核查", "风险结论"]
},
{
batchId: "SUB-20260602-004",
title: "目录汇总任务",
time: "2026-06-02 16:40",
status: "已完成",
summary: "仅执行目录汇总和章节点识别,未触发后续审核。",
files: "12 份文件 / 148 页",
result: "已生成目录报告",
trace: ["目录汇总"]
},
{
batchId: "SUB-20260601-007",
title: "字段抽取复核",
time: "2026-06-01 09:18",
status: "待复核",
summary: "聚焦字段池抽取结果,人工复核后重新回填。",
files: "7 份文件 / 84 页",
result: "2 个字段低置信度,需人工确认",
trace: ["字段抽取", "字段复核", "回填草稿"]
}
]
},
materials: {
metrics: [
{ label: "资料文件数", value: "18", note: "含监管信息、说明书与补充材料" },
{ label: "已识别页数", value: "236", note: "PDF / DOCX 已统计1 份 DOC 待复核" },
{ label: "章节点命中", value: "14", note: "覆盖 CH1 至 CH6 关键章节" },
{ label: "目录异常", value: "2", note: "1 份页数待复核1 份疑似目录错放" }
],
packages: [
{
id: "pkg-001",
conversationId: "conv-001",
productName: "新型冠状病毒 2019-nCoV 核酸检测试剂盒",
batchId: "SUB-20260603-001",
fileCount: "18",
pages: "236",
status: "审核中",
note: "已完成目录汇总并进入完整性检查。"
},
{
id: "pkg-002",
conversationId: "conv-002",
productName: "乙型肝炎病毒核酸检测试剂盒",
batchId: "SUB-20260602-004",
fileCount: "12",
pages: "148",
status: "已完成",
note: "仅执行目录汇总与页数识别。"
}
],
items: [
{ path: "第1章 监管信息/CH1.2 监管信息目录.docx", chapter: "CH1.2", pages: "3", status: "已识别", note: "命中监管信息目录" },
{ path: "第1章 监管信息/CH1.4 申请表.docx", chapter: "CH1.4", pages: "5", status: "已识别", note: "用于字段抽取与一致性核查" },
{ path: "第1章 监管信息/CH1.9 产品申报前沟通的说明.doc", chapter: "CH1.9", pages: "待复核", status: "异常", note: "疑似目录错放,页数待人工确认" },
{ path: "第1章 监管信息/CH1.11.1 符合标准的清单.docx", chapter: "CH1.11.1", pages: "2", status: "已识别", note: "可用于完整性检查" },
{ path: "第1章 监管信息/CH1.11.5 真实性声明.docx", chapter: "CH1.11.5", pages: "1", status: "已识别", note: "监管声明材料" },
{ path: "目标产品说明书.docx", chapter: "CH3", pages: "18", status: "已识别", note: "主要字段来源文档" }
]
}
};
let activeTabId = "chat";
let activeNodeId = "node-import";
function getActiveConversation() {
return data.conversations.find(item => item.id === data.activeConversationId) || data.conversations[0];
}
function getFilteredMaterialPackages() {
const query = data.materialSearchQuery.trim().toLowerCase();
if (!query) {
return data.materials.packages;
}
return data.materials.packages.filter(item =>
item.productName.toLowerCase().includes(query)
|| item.batchId.toLowerCase().includes(query)
);
}
function statusClass(text) {
if (text.includes("高风险") || text.includes("阻断") || text.includes("失败")) return "danger";
if (text.includes("待复核") || text.includes("处理中") || text.includes("解析中")) return "warning";
return "success";
}
function renderTopNav() {
document.querySelectorAll("[data-top-nav]").forEach(item => {
const target = item.dataset.topNav;
const isActive = activeTabId === target;
item.classList.toggle("active", isActive);
});
}
function renderMetrics(items) {
return `
<div class="metric-grid">
${items.map(item => `
<div class="metric-card">
<div class="muted">${item.label}</div>
<strong>${item.value}</strong>
<div class="muted" style="margin-top:10px;">${item.note}</div>
</div>
`).join("")}
</div>
`;
}
function renderMessages(messages) {
return messages.map(message => `
<div class="message ${message.role}">
<div class="message-head">
<span>${message.title}</span>
<span>${message.meta}</span>
</div>
<div class="message-body">
${message.body.map(line => `<p>${line}</p>`).join("")}
</div>
</div>
`).join("");
}
function renderHistoryList(conversations) {
return conversations.map(item => `
<div class="history-item ${item.id === data.activeConversationId ? "active" : ""}" data-conversation-id="${item.id}">
<div class="meta-row">
<span class="status-tag ${statusClass(item.status)}">${item.status}</span>
<span class="muted">${item.time}</span>
</div>
<strong>${item.title}</strong>
<div class="muted">${item.summary}</div>
</div>
`).join("");
}
function renderNodeStrip(nodes) {
return nodes.map(node => `
<button class="node-item ${node.id === activeNodeId ? "active" : ""}" data-node-id="${node.id}">
<strong>${node.title}</strong>
<div class="muted" style="margin-top:6px;">${node.summary}</div>
<div class="meta-row" style="margin-top:8px;">
<span class="status-tag ${statusClass(node.summary + node.state)}">${node.state === "completed" ? "已完成" : node.state === "running" ? "执行中" : node.state === "blocked" ? "已阻断" : "待执行"}</span>
</div>
</button>
`).join("");
}
function renderAbilityCards(abilities) {
const entries = Object.entries(abilities);
return entries.map(([key, item]) => `
<button class="ability-card ${key === data.activeAbilityKey ? "active" : ""}" data-ability-key="${key}">
<strong>${item.title}</strong>
<div class="muted">${item.desc}</div>
</button>
`).join("");
}
function renderActiveAbility(abilities) {
const item = abilities[data.activeAbilityKey] || Object.values(abilities)[0];
if (!item) {
return `<div class="ability-card"><strong>暂无能力卡片</strong><div class="muted">当前会话未配置功能流程。</div></div>`;
}
return `
<div class="ability-card active">
<strong>${item.title}</strong>
<div class="muted">${item.desc}</div>
<div class="card-list">
${item.details.map(detail => `<div class="queue-item">${detail}</div>`).join("")}
</div>
</div>
`;
}
function renderChatPage() {
const conversation = getActiveConversation();
return `
<section class="page ${activeTabId === "chat" ? "active" : ""}" data-page="chat">
<div class="agent-layout">
<aside class="panel history-panel">
<div class="panel-head">
<div class="history-toolbar" style="width:100%;">
<button class="primary-btn" style="width:100%; justify-content:center;">+ 新对话</button>
<div class="search-shell">搜索会话...</div>
<div class="history-caption">对话记录</div>
</div>
</div>
<div class="panel-body">
<div class="muted" style="margin-bottom:12px; color:#6f81a2; font-weight:600;">更早</div>
<div class="history-list">
${renderHistoryList(data.conversations)}
</div>
</div>
</aside>
<section class="panel conversation-shell">
<div class="hero-center">
<span class="hero-badge">审核智能助手</span>
<h2>试剂盒临床注册资料智能工作台</h2>
<p>面向注册申报资料准备、审核与整改协同场景,提供资料上传、目录汇总、完整性检查、字段抽取、一致性核查和导出建议的一站式 Agent 对话入口。</p>
<div class="hero-tags">
<span class="prompt-chip">自动汇总文件夹目录与页数</span>
<span class="prompt-chip">执行法规完整性检查</span>
<span class="prompt-chip">抽取字段并生成字段池</span>
<span class="prompt-chip">解释冲突字段原因</span>
<span class="prompt-chip">给出整改与导出建议</span>
</div>
<div class="hero-note-row">
<div class="hero-note-card">
<strong>推荐提问方式</strong>
<div class="muted">建议说明统计范围、资料对象和输出目标,例如“请自动汇总这个资料包的目录、页数与章节点,并标出待复核异常”。</div>
</div>
<div class="hero-note-card">
<strong>当前数据权限</strong>
<div class="muted">可查询当前批次资料、知识库法规规则、已入库模板与历史处理记录。</div>
</div>
</div>
</div>
<div class="panel-head" style="border-top:1px solid var(--line);">
<div>
<h2>${conversation.title}</h2>
<p>对话中穿插节点结果,点击节点可以快速定位审核阶段。</p>
</div>
<span class="status-tag ${statusClass(conversation.status)}">${conversation.status}</span>
</div>
<div class="node-strip">
${renderNodeStrip(conversation.nodes)}
</div>
<div class="conversation-board" id="conversationBoard">
${renderMessages(conversation.messages)}
</div>
<div class="composer">
<div class="composer-box">在这里继续输入指令,例如:“先自动汇总目录与页数,再按法规要求执行完整性检查,并输出缺失项和法规依据。”</div>
<div class="composer-actions">
<button class="secondary-btn">上传附件</button>
<button class="secondary-btn">插入模板提示词</button>
<button class="primary-btn">发送给 Agent</button>
</div>
</div>
</section>
<aside class="side-stack">
<section class="panel">
<div class="panel-head">
<div>
<h3>上传栏</h3>
<p>支持附件上传Agent 会结合当前会话继续处理。</p>
</div>
</div>
<div class="panel-body">
<div class="upload-box">
<strong>拖拽或选择资料包 / 文档</strong>
<div class="muted">支持 <code>pdf / docx / doc / zip / rar / 7z</code>,可用于目录汇总、完整性检查和字段抽取。</div>
<div class="action-row">
<button class="primary-btn">上传文件</button>
<button class="secondary-btn">上传压缩包</button>
</div>
</div>
<div class="queue-list" style="margin-top:14px;">
${conversation.uploads.map(file => `
<div class="upload-file">
<strong>${file.name}</strong>
<div class="muted">${file.meta}</div>
</div>
`).join("")}
</div>
</div>
</section>
<section class="panel">
<div class="panel-head">
<div>
<h3>功能流程卡片</h3>
<p>右下角内容会随着你选中的功能流程变化。</p>
</div>
</div>
<div class="panel-body">
<div class="card-list">
${renderAbilityCards(conversation.abilities)}
</div>
<div style="margin-top:14px;">
${renderActiveAbility(conversation.abilities)}
</div>
</div>
</section>
</aside>
</div>
</section>
`;
}
function renderMaterialsPage() {
const filteredPackages = getFilteredMaterialPackages();
const activeConversation = getActiveConversation();
return `
<section class="page ${activeTabId === "materials" ? "active" : ""}" data-page="materials">
${renderMetrics(data.materials.metrics)}
<div class="panel" style="margin-bottom:18px;">
<div class="panel-head">
<div>
<h2>资料包总览</h2>
<p>资料包与对话记录一一关联,对话名称直接采用解析后的产品名称。</p>
</div>
<div class="table-actions">
<button class="mini-btn">导入资料包</button>
<button class="mini-btn">刷新目录</button>
<button class="mini-btn">生成目录报告</button>
</div>
</div>
<div class="panel-body">
<div class="search-bar">
<input
class="search-input"
id="materialSearchInput"
value="${data.materialSearchQuery}"
placeholder="根据产品名称或批次号搜索资料包"
/>
<button class="secondary-btn">搜索资料包</button>
<button class="secondary-btn">清空搜索</button>
</div>
<div class="table-shell" style="margin-bottom:16px;">
<table>
<thead>
<tr>
<th>产品名称</th>
<th>关联对话</th>
<th>批次号</th>
<th>文件数</th>
<th>页数</th>
<th>状态</th>
<th>操作</th>
</tr>
</thead>
<tbody>
${filteredPackages.map(item => `
<tr>
<td>${item.productName}</td>
<td>${item.productName}</td>
<td>${item.batchId}</td>
<td>${item.fileCount}</td>
<td>${item.pages}</td>
<td><span class="status-tag ${statusClass(item.status)}">${item.status}</span></td>
<td><button class="mini-btn" data-open-conversation="${item.conversationId}">查看对话</button></td>
</tr>
`).join("")}
${filteredPackages.length === 0 ? `
<tr>
<td colspan="7">未找到匹配的产品名称资料包。</td>
</tr>
` : ""}
</tbody>
</table>
</div>
<div class="table-shell">
<table>
<thead>
<tr>
<th>相对路径</th>
<th>命中章节</th>
<th>页数</th>
<th>状态</th>
<th>说明</th>
</tr>
</thead>
<tbody>
${data.materials.items.map(item => `
<tr>
<td>${item.path}</td>
<td>${item.chapter}</td>
<td>${item.pages}</td>
<td><span class="status-tag ${statusClass(item.status)}">${item.status}</span></td>
<td>${item.note}${activeConversation.productName ? `<br /><span class="muted">所属产品:${activeConversation.productName}</span>` : ""}</td>
</tr>
`).join("")}
</tbody>
</table>
</div>
</div>
</div>
</section>
`;
}
function renderKnowledgePage() {
return `
<section class="page ${activeTabId === "knowledge" ? "active" : ""}" data-page="knowledge">
${renderMetrics(data.knowledge.metrics)}
<div class="knowledge-layout">
<div class="history-layout">
<section class="panel">
<div class="panel-head">
<div>
<h2>知识库管理</h2>
<p>保留基本 CRUD、手动上传、切片解析与重建能力支撑 Agent 通过 RAG 检索法规依据、业务知识和模板映射。</p>
</div>
</div>
<div class="panel-body">
<div class="upload-box">
<strong>手动上传知识库资料</strong>
<div class="muted">上传法规资料、模板文档、示例资料或内部 SOP进入解析、切片和入库流程。</div>
<div class="action-row">
<button class="primary-btn">上传法规资料</button>
<button class="secondary-btn">上传业务资料</button>
<button class="secondary-btn">导入模板压缩包</button>
</div>
</div>
<div class="queue-list" style="margin-top:16px;">
${data.knowledge.queue.map(item => `
<div class="queue-item">
<div class="meta-row">
<span class="status-tag ${statusClass(item.state)}">${item.state}</span>
<span class="mini-tag">${item.type}</span>
</div>
<strong>${item.name}</strong>
<div class="muted">${item.detail}</div>
</div>
`).join("")}
</div>
</div>
</section>
<section class="panel">
<div class="panel-head">
<div>
<h3>知识库能力卡片</h3>
<p>法规依据不再单独成页,而是作为法规资料进入知识库,在对话中由 RAG 命中后返回解释。</p>
</div>
</div>
<div class="panel-body">
<div class="kb-grid">
${data.knowledge.cards.map(card => `
<div class="kb-card">
<div class="muted">${card.title}</div>
<strong style="font-size:24px; line-height:1;">${card.value}</strong>
<div class="muted">${card.desc}</div>
</div>
`).join("")}
</div>
</div>
</section>
<section class="panel">
<div class="panel-head">
<div>
<h3>文档源表</h3>
<p>用于查看知识库资料的基础元数据和入库状态。</p>
</div>
<div class="table-actions">
<button class="mini-btn">新增资料</button>
<button class="mini-btn">编辑资料</button>
<button class="mini-btn">停用资料</button>
<button class="mini-btn">删除资料</button>
</div>
</div>
<div class="panel-body">
<div class="table-shell">
<table>
<thead>
<tr>
<th>资料名称</th>
<th>类型</th>
<th>版本</th>
<th>状态</th>
<th>切片数</th>
<th>维护人</th>
</tr>
</thead>
<tbody>
${data.knowledge.sourceRows.map(row => `<tr>${row.map(cell => `<td>${cell}</td>`).join("")}</tr>`).join("")}
</tbody>
</table>
</div>
</div>
</section>
</div>
<div class="side-stack">
<section class="panel">
<div class="panel-head">
<div>
<h3>CRUD 能力</h3>
<p>知识库页必须保留的基础增删改查与解析能力。</p>
</div>
</div>
<div class="panel-body">
<div class="crud-grid">
${data.knowledge.crudGroups.map(item => `
<div class="crud-card">
<strong>${item.title}</strong>
<div class="muted">${item.desc}</div>
<div class="crud-actions">
${item.actions.map(action => `<button class="mini-btn">${action}</button>`).join("")}
</div>
</div>
`).join("")}
</div>
</div>
</section>
<section class="panel">
<div class="panel-head">
<div>
<h3>切片解析流程</h3>
<p>展示从法规/业务文档上传到切片可召回,再到对话中返回依据的处理链路。</p>
</div>
</div>
<div class="panel-body">
<div class="process-steps">
<div class="step-card completed">
<div>
<strong>资料上传</strong>
<div class="muted">手动上传法规资料、模板资料或示例文档。</div>
</div>
<span class="status-tag success">已完成</span>
</div>
<div class="step-card running">
<div>
<strong>文档解析</strong>
<div class="muted">提取标题、页数、正文段落、表格与元数据。</div>
</div>
<span class="status-tag warning">解析中</span>
</div>
<div class="step-card running">
<div>
<strong>切片与标签</strong>
<div class="muted">按章节、语义和业务主题生成切片与召回标签。</div>
</div>
<span class="status-tag warning">处理中</span>
</div>
<div class="step-card pending">
<div>
<strong>对话命中依据</strong>
<div class="muted">完成入库后,可在审核智能体对话中命中法规条款、模板约束和示例依据。</div>
</div>
<span class="status-tag">可检索</span>
</div>
</div>
</div>
</section>
<section class="panel">
<div class="panel-head">
<div>
<h3>RAG 命中依据示例</h3>
<p>模拟 Agent 在对话中返回的法规依据片段。</p>
</div>
</div>
<div class="panel-body">
<div class="record-list">
<div class="queue-item">
<div class="meta-row">
<span class="mini-tag">核心法规</span>
<span class="status-tag warning">CH1.11.4 必交项</span>
</div>
<strong>附件 4 体外诊断试剂注册申报资料要求及说明</strong>
<div class="muted">用于判断授权书/声明类资料缺失是否构成高风险Agent 在完整性检查对话中会引用该依据。</div>
</div>
<div class="queue-item">
<div class="meta-row">
<span class="mini-tag">配套公告</span>
<span class="status-tag warning">资料结构要求</span>
</div>
<strong>关于公布体外诊断试剂注册申报资料要求和批准证明文件格式的公告</strong>
<div class="muted">用于解释章节结构和申报资料组织要求,支持目录错放和资料结构判断。</div>
</div>
<div class="queue-item">
<div class="meta-row">
<span class="mini-tag">业务依据</span>
<span class="status-tag warning">字段取值比对</span>
</div>
<strong>目标产品说明书.docx</strong>
<div class="muted">用于字段抽取、一致性核查和最终采用值解释。</div>
</div>
</div>
</div>
</section>
<section class="panel">
<div class="panel-head">
<div>
<h3>切片表</h3>
<p>支持查看、拆分、合并和重建向量。</p>
</div>
</div>
<div class="panel-body">
<div class="table-shell">
<table>
<thead>
<tr>
<th>切片ID</th>
<th>所属文档</th>
<th>章节</th>
<th>摘要</th>
<th>长度</th>
<th>状态</th>
</tr>
</thead>
<tbody>
${data.knowledge.chunkRows.map(row => `<tr>${row.map(cell => `<td>${cell}</td>`).join("")}</tr>`).join("")}
</tbody>
</table>
</div>
</div>
</section>
</div>
</div>
</section>
`;
}
function renderHistoryPage() {
return `
<section class="page ${activeTabId === "history" ? "active" : ""}" data-page="history">
<div class="panel" style="margin-bottom:18px;">
<div class="panel-head">
<div>
<h2>处理历史</h2>
<p>按批次回看历史对话、上传资料、命中节点和最终结论。</p>
</div>
</div>
<div class="panel-body">
<div class="search-bar">
<input class="search-input" value="搜索批次号 / 产品名称 / 风险状态" readonly />
<button class="secondary-btn">筛选高风险</button>
<button class="secondary-btn">导出历史记录</button>
</div>
<div class="record-list">
${data.history.records.map(record => `
<div class="record-item">
<div class="record-main">
<div class="meta-row">
<span class="status-tag ${statusClass(record.status)}">${record.status}</span>
<span class="muted">${record.time}</span>
<span class="mini-tag">${record.batchId}</span>
</div>
<strong>${record.title}</strong>
<div class="muted">${record.summary}</div>
<div class="meta-row">
${record.trace.map(item => `<span class="chip">${item}</span>`).join("")}
</div>
</div>
<div class="record-side">
<div class="queue-item">
<strong>资料规模</strong>
<div class="muted">${record.files}</div>
</div>
<div class="queue-item">
<strong>最终结果</strong>
<div class="muted">${record.result}</div>
</div>
<div class="action-row">
<button class="mini-btn">查看会话</button>
<button class="mini-btn">查看附件</button>
<button class="mini-btn">复制结论</button>
</div>
</div>
</div>
`).join("")}
</div>
</div>
</div>
</section>
`;
}
function renderPages() {
document.getElementById("pages").innerHTML = [
renderMaterialsPage(),
renderChatPage(),
renderKnowledgePage(),
renderHistoryPage()
].join("");
}
function rerender() {
renderTopNav();
renderPages();
}
rerender();
document.addEventListener("click", event => {
const topNav = event.target.closest("[data-top-nav]");
if (topNav) {
const target = topNav.dataset.topNav;
if (target === "materials" || target === "chat" || target === "knowledge" || target === "history") {
activeTabId = target;
}
rerender();
return;
}
const conversation = event.target.closest("[data-conversation-id]");
if (conversation) {
data.activeConversationId = conversation.dataset.conversationId;
const currentConversation = getActiveConversation();
activeNodeId = currentConversation.nodes[0]?.id || "";
data.activeAbilityKey = Object.keys(currentConversation.abilities)[0] || "";
rerender();
return;
}
const openConversation = event.target.closest("[data-open-conversation]");
if (openConversation) {
data.activeConversationId = openConversation.dataset.openConversation;
activeTabId = "chat";
const currentConversation = getActiveConversation();
activeNodeId = currentConversation.nodes[0]?.id || "";
data.activeAbilityKey = Object.keys(currentConversation.abilities)[0] || "";
rerender();
return;
}
const node = event.target.closest("[data-node-id]");
if (node) {
activeNodeId = node.dataset.nodeId;
const board = document.getElementById("conversationBoard");
if (board) {
board.scrollTo({ top: Math.max(0, board.scrollHeight * 0.35), behavior: "smooth" });
}
rerender();
return;
}
const ability = event.target.closest("[data-ability-key]");
if (ability) {
data.activeAbilityKey = ability.dataset.abilityKey;
rerender();
}
});
document.addEventListener("input", event => {
if (event.target.id === "materialSearchInput") {
data.materialSearchQuery = event.target.value;
if (activeTabId === "materials") {
renderPages();
}
}
});
</script>
</body>
</html>