merge: 合并V2到master

# Conflicts:
#	.gitignore
#	README.md
#	config/asgi.py
#	config/settings.py
#	config/urls.py
#	config/wsgi.py
#	manage.py
#	requirements.txt
#	templates/base.html
#	tests/conftest.py
This commit is contained in:
2026-06-11 00:08:00 +08:00
278 changed files with 45245 additions and 647 deletions

View File

@@ -0,0 +1,139 @@
{% extends "base.html" %}
{% load static %}
{% block title %}附件管理 - DEMO-AGENT V2{% endblock %}
{% block body_class %}app-body{% endblock %}
{% block content %}
<main class="app-shell">
<header class="topbar">
<div class="topbar-left">
<div class="tabbar" role="tablist" aria-label="页面切换">
<a class="tab" href="{% url 'home' %}" role="tab" aria-selected="false">首页</a>
<a class="tab" href="{% url 'chat' %}" role="tab" aria-selected="false">审核智能体</a>
<a class="tab" href="{% url 'knowledge_base_manager' %}" role="tab" aria-selected="false">知识库管理</a>
<a class="tab active" href="{% url 'attachment_manager' %}" role="tab" aria-selected="true">附件管理</a>
</div>
</div>
<div class="topbar-right">
<div class="user-menu">
<button class="user-menu-trigger" type="button">
<span class="avatar large">{{ request.user.username|slice:":1"|upper }}</span>
<div class="user-copy">
<strong>{{ request.user.username }}</strong>
<span>当前登录用户</span>
</div>
</button>
</div>
</div>
</header>
<section
class="attachment-manager-page"
data-selected-conversation="{% if selected_conversation %}{{ selected_conversation.pk }}{% endif %}"
>
<header class="attachment-manager-hero attachment-manager-toolbar">
<div>
<p class="eyebrow">附件管理</p>
<h1>附件管理</h1>
<p>管理各对话下上传的审核资料、版本、状态和下载。</p>
</div>
<div class="attachment-manager-selectbar">
<label for="attachmentConversationSelect">对话</label>
<select class="attachment-manager-select-control" id="attachmentConversationSelect">
<option value="">请选择对话</option>
{% for conversation in conversations %}
<option
value="{{ conversation.pk }}"
{% if selected_conversation and selected_conversation.pk == conversation.pk %}selected{% endif %}
>
{{ conversation.title|default:"新对话" }} · {{ conversation.updated_at|date:"m月d日 H:i" }} · {{ conversation.attachment_count }} 个附件
</option>
{% endfor %}
</select>
{% if selected_conversation %}
<a class="return-chat-link" href="{% url 'chat' %}?conversation={{ selected_conversation.pk }}">返回对话</a>
{% endif %}
</div>
</header>
{% if selected_conversation %}
<div class="attachment-manager-content attachment-manager-split">
<section class="attachment-manager-panel upload-manager-panel">
<div class="summary-subheading">
<h3>上传附件</h3>
<span>{{ selected_conversation.title|default:"新对话" }}</span>
</div>
<div
class="upload-dropzone manager-upload-dropzone"
id="managerUploadDropzone"
data-upload-url="{% url 'file_summary_attachment_upload' selected_conversation.pk %}"
tabindex="0"
role="button"
>
<input id="managerAttachmentInput" type="file" multiple hidden>
<strong>拖拽文件到这里</strong>
<span>支持 doc、docx、xls、xlsx、ppt、pptx、pdf、zip、7z、rar</span>
</div>
<p class="upload-status" id="managerUploadStatus">上传后会归属到当前选择的对话。</p>
</section>
<section class="attachment-manager-panel">
<div class="summary-subheading">
<h3>附件列表</h3>
<input class="attachment-search" id="attachmentSearch" type="search" placeholder="搜索文件名">
</div>
<div class="attachment-table-wrap">
<table class="attachment-table" id="attachmentManagerTable">
<thead>
<tr>
<th>状态</th>
<th>文件名</th>
<th>版本</th>
<th>大小</th>
<th>上传时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{% for attachment in attachments %}
<tr
data-attachment-id="{{ attachment.pk }}"
data-update-url="{% url 'file_summary_attachment_detail' selected_conversation.pk attachment.pk %}"
data-download-url="{% url 'file_summary_attachment_download' selected_conversation.pk attachment.pk %}"
>
<td>{% if attachment.is_active %}启用{% else %}禁用{% endif %}</td>
<td class="attachment-name">{{ attachment.original_name }}</td>
<td>v{{ attachment.version_no }}</td>
<td>{{ attachment.file_size }} bytes</td>
<td>{{ attachment.created_at|date:"Y-m-d H:i" }}</td>
<td class="attachment-actions">
<a href="{% url 'file_summary_attachment_download' selected_conversation.pk attachment.pk %}">下载</a>
<button type="button" data-attachment-action="edit">编辑</button>
<button type="button" data-attachment-action="toggle">{% if attachment.is_active %}禁用{% else %}启用{% endif %}</button>
<button type="button" data-attachment-action="delete">删除</button>
</td>
</tr>
{% empty %}
<tr>
<td colspan="6" class="table-empty">当前对话暂无附件</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</section>
</div>
{% else %}
<section class="attachment-manager-panel attachment-manager-empty attachment-manager-content">
<h2>请选择一个对话查看附件</h2>
<p>通过上方下拉框选择对话后,可上传、下载、编辑、启用禁用或删除附件。</p>
</section>
{% endif %}
</section>
</main>
{% endblock %}
{% block scripts %}
<script src="{% static 'js/attachment_manager.js' %}"></script>
{% endblock %}

View File

@@ -1,351 +1,14 @@
{% load static %}
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}Universal Agent Demo Framework{% endblock %}</title>
<style>
:root {
--bg: #f4f1ea;
--surface: #fffdf8;
--surface-strong: #fff7ea;
--border: #d7c9b0;
--text: #2f261d;
--muted: #73614f;
--accent: #a54c2b;
--accent-soft: #f1d2b8;
--success: #2b6a4d;
--warning: #9a5a00;
--danger: #8d2f2f;
--shadow: 0 18px 40px rgba(91, 63, 36, 0.10);
}
* { box-sizing: border-box; }
body {
margin: 0;
font-family: "Microsoft YaHei", "PingFang SC", sans-serif;
color: var(--text);
background:
radial-gradient(circle at top left, #f7e2c8 0, transparent 30%),
linear-gradient(180deg, #f6efe2 0%, #f4f1ea 45%, #efe7da 100%);
min-height: 100vh;
}
a { color: var(--accent); text-decoration: none; }
a:hover { text-decoration: underline; }
.shell {
width: min(1180px, calc(100vw - 32px));
margin: 0 auto;
padding: 24px 0 40px;
}
.topbar {
display: flex;
justify-content: space-between;
gap: 16px;
align-items: center;
margin-bottom: 24px;
padding: 18px 20px;
border: 1px solid rgba(215, 201, 176, 0.7);
background: rgba(255, 253, 248, 0.88);
backdrop-filter: blur(12px);
border-radius: 22px;
box-shadow: var(--shadow);
}
.brand-title {
margin: 0;
font-size: 1.35rem;
font-weight: 700;
}
.brand-note {
margin: 6px 0 0;
color: var(--muted);
font-size: 0.95rem;
}
.nav-links {
display: flex;
gap: 10px;
flex-wrap: wrap;
justify-content: flex-end;
}
.nav-link,
.button,
button {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
border-radius: 999px;
border: 1px solid var(--border);
background: var(--surface);
color: var(--text);
padding: 10px 16px;
font-size: 0.95rem;
cursor: pointer;
transition: transform 0.15s ease, box-shadow 0.15s ease, border-color 0.15s ease;
}
.button-primary,
button {
border-color: var(--accent);
background: linear-gradient(135deg, #b9562f, #94452a);
color: #fffaf4;
box-shadow: 0 10px 22px rgba(165, 76, 43, 0.20);
}
.nav-link:hover,
.button:hover,
button:hover {
text-decoration: none;
transform: translateY(-1px);
box-shadow: 0 12px 24px rgba(91, 63, 36, 0.12);
}
.page-header {
margin-bottom: 24px;
}
.eyebrow {
display: inline-block;
margin-bottom: 10px;
padding: 6px 12px;
border-radius: 999px;
background: rgba(241, 210, 184, 0.8);
color: var(--accent);
font-size: 0.85rem;
font-weight: 700;
letter-spacing: 0.04em;
}
.page-title {
margin: 0 0 10px;
font-size: clamp(2rem, 3vw, 3rem);
line-height: 1.1;
}
.page-lead {
margin: 0;
color: var(--muted);
font-size: 1rem;
max-width: 720px;
}
.card-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
gap: 18px;
}
.panel,
.card {
background: rgba(255, 253, 248, 0.94);
border: 1px solid rgba(215, 201, 176, 0.85);
border-radius: 24px;
padding: 20px;
box-shadow: var(--shadow);
}
.card h2,
.panel h2,
.panel h3 {
margin-top: 0;
}
.meta-list {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin: 12px 0 0;
padding: 0;
list-style: none;
color: var(--muted);
font-size: 0.92rem;
}
.meta-badge {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
border-radius: 999px;
background: var(--surface-strong);
border: 1px solid rgba(215, 201, 176, 0.75);
}
.layout-two-columns {
display: grid;
grid-template-columns: minmax(320px, 420px) minmax(0, 1fr);
gap: 20px;
align-items: start;
}
.stack {
display: grid;
gap: 16px;
}
textarea,
select,
input[type="text"],
input[type="file"] {
width: 100%;
border-radius: 16px;
border: 1px solid var(--border);
background: #fff;
padding: 12px 14px;
font: inherit;
color: var(--text);
}
textarea { min-height: 150px; resize: vertical; }
label {
display: block;
margin-bottom: 8px;
font-weight: 700;
}
.help-text,
.muted {
color: var(--muted);
font-size: 0.92rem;
}
.checkbox-list {
display: grid;
gap: 8px;
margin-top: 10px;
}
.checkbox-item {
display: flex;
gap: 10px;
align-items: flex-start;
padding: 10px 12px;
border-radius: 16px;
border: 1px solid rgba(215, 201, 176, 0.85);
background: #fff;
}
.status {
display: inline-flex;
align-items: center;
padding: 6px 12px;
border-radius: 999px;
font-size: 0.9rem;
font-weight: 700;
background: var(--surface-strong);
color: var(--accent);
}
.status-success { color: var(--success); }
.status-failed { color: var(--danger); }
.notice {
padding: 14px 16px;
border-radius: 18px;
border: 1px solid rgba(215, 201, 176, 0.85);
background: rgba(255, 247, 234, 0.92);
color: var(--text);
}
.notice-error {
border-color: rgba(141, 47, 47, 0.25);
background: rgba(255, 238, 238, 0.95);
color: var(--danger);
}
.kv-table {
width: 100%;
border-collapse: collapse;
}
.kv-table th,
.kv-table td {
padding: 12px 10px;
border-bottom: 1px solid rgba(215, 201, 176, 0.6);
vertical-align: top;
text-align: left;
}
.kv-table th {
width: 150px;
color: var(--muted);
font-weight: 700;
}
.detail-list {
display: grid;
gap: 12px;
margin: 0;
padding: 0;
list-style: none;
}
.detail-item {
padding: 14px 16px;
border-radius: 18px;
background: #fff;
border: 1px solid rgba(215, 201, 176, 0.75);
}
.detail-item strong {
display: block;
margin-bottom: 6px;
}
.code-block {
margin: 0;
padding: 14px;
border-radius: 18px;
background: #2f261d;
color: #fdf8f1;
overflow: auto;
white-space: pre-wrap;
word-break: break-word;
font-size: 0.92rem;
line-height: 1.6;
}
.section-title {
margin: 0 0 12px;
font-size: 1.1rem;
}
@media (max-width: 900px) {
.layout-two-columns { grid-template-columns: 1fr; }
.topbar { flex-direction: column; align-items: flex-start; }
.nav-links { justify-content: flex-start; }
}
</style>
<title>{% block title %}DEMO-AGENT V2{% endblock %}</title>
<link rel="stylesheet" href="{% static 'css/login.css' %}?v=20260608-chat-delete1">
</head>
<body>
<div class="shell">
<header class="topbar">
<div>
<p class="brand-title">Universal Agent Demo Framework</p>
<p class="brand-note">面向复试演示的可配置 AI Agent 单体系统</p>
</div>
<nav class="nav-links">
<a class="nav-link" href="{% url 'scenarios:index' %}">场景首页</a>
<a class="nav-link" href="{% url 'documents:list' %}">文档中心</a>
<a class="nav-link" href="{% url 'audit:list' %}">审计日志</a>
</nav>
</header>
{% if messages %}
<section class="stack" style="margin-bottom: 18px;">
{% for message in messages %}
<div class="notice{% if message.tags == 'error' %} notice-error{% endif %}">{{ message }}</div>
{% endfor %}
</section>
{% endif %}
{% block content %}{% endblock %}
</div>
<body class="{% block body_class %}{% endblock %}">
{% block content %}{% endblock %}
{% block scripts %}{% endblock %}
</body>
</html>

372
templates/home.html Normal file
View File

@@ -0,0 +1,372 @@
{% extends "base.html" %}
{% load static %}
{% block title %}审核智能体 - DEMO-AGENT V2{% endblock %}
{% block body_class %}app-body{% endblock %}
{% block content %}
<main class="app-shell">
<header class="topbar">
<div class="topbar-left">
<div class="tabbar" role="tablist" aria-label="页面切换">
<a class="tab" href="{% url 'home' %}" role="tab" aria-selected="false">首页</a>
<a class="tab active" href="{% url 'chat' %}" role="tab" aria-selected="true">审核智能体</a>
<a class="tab" href="{% url 'knowledge_base_manager' %}" role="tab" aria-selected="false">知识库管理</a>
<a class="tab" href="{% url 'attachment_manager' %}" role="tab" aria-selected="false">附件管理</a>
</div>
</div>
<div class="topbar-right">
<div class="user-menu" id="userMenu">
<button class="user-menu-trigger" id="userMenuTrigger" type="button" aria-haspopup="menu" aria-expanded="false">
<span class="avatar large">{{ request.user.username|slice:":1"|upper }}</span>
<div class="user-copy">
<strong>{{ request.user.username }}</strong>
<span>当前登录用户</span>
</div>
<span class="caret"></span>
</button>
<div class="user-dropdown" id="userDropdown" role="menu">
<div class="user-dropdown-section" role="none">
<p class="user-dropdown-label">用户信息</p>
<strong class="user-dropdown-name">{{ request.user.username }}</strong>
</div>
<a class="user-dropdown-link" href="{% url 'password_change' %}" role="menuitem">修改密码</a>
<form action="{% url 'logout' %}" method="post" class="user-dropdown-form" role="none">
{% csrf_token %}
<button class="user-dropdown-link danger-link" type="submit" role="menuitem">退出登录</button>
</form>
</div>
</div>
</div>
</header>
<section class="workspace" data-sidebar-state="open">
<aside class="sidebar" id="sidebar">
<div class="sidebar-top">
<div class="sidebar-header">
<button class="icon-button sidebar-toggle" type="button" id="sidebarToggle" aria-label="折叠侧边栏">
<span></span>
<span></span>
</button>
<div class="brand">
<span class="brand-mark"></span>
<div class="brand-copy">
<strong class="brand-text">审核智能体</strong>
<span class="brand-subtitle">临床注册文件审核工作台</span>
</div>
</div>
</div>
<form method="post">
{% csrf_token %}
<input type="hidden" name="action" value="new_conversation">
<button class="new-chat" type="submit">+ 新对话</button>
</form>
<form class="search-form" method="get">
<label class="sr-only" for="conversationSearch">搜索会话</label>
<input id="conversationSearch" type="text" name="q" value="{{ search_query }}" placeholder="搜索会话...">
</form>
</div>
<div class="sidebar-group">
<p class="sidebar-label">对话记录</p>
<nav class="history-list" aria-label="对话历史">
{% for conversation in conversations %}
<div
class="history-item{% if current_conversation and current_conversation.pk == conversation.pk %} active{% endif %}"
data-conversation-id="{{ conversation.pk }}"
data-delete-url="{% url 'review_agent_conversation_detail' conversation.pk %}"
>
<a
class="history-link"
href="{% url 'chat' %}?conversation={{ conversation.pk }}{% if search_query %}&q={{ search_query|urlencode }}{% endif %}"
>
<span class="history-title">{{ conversation.title|default:"新对话" }}</span>
<span class="history-meta">{{ conversation.updated_at|date:"m月d日 H:i" }}</span>
</a>
<button
class="history-delete"
type="button"
data-conversation-delete
aria-label="删除对话 {{ conversation.title|default:'新对话' }}"
title="删除对话"
>×</button>
</div>
{% empty %}
<div class="history-empty">
<p>暂无会话记录</p>
<span>点击上方“新对话”开始审核。</span>
</div>
{% endfor %}
</nav>
</div>
</aside>
<section class="chat-shell">
<section class="chat-stage" data-stream-url="{% url 'chat_stream' %}">
<div class="chat-scroll-wrap">
<div class="chat-scroll" id="chatScroll">
{% if current_conversation %}
<div class="conversation-header" id="conversation-top" data-node-label="会话开始">
<div>
<p class="eyebrow">审核智能体</p>
<h1>{{ current_conversation.title|default:"新对话" }}</h1>
</div>
<span class="conversation-meta">最后更新 {{ current_conversation.updated_at|date:"Y-m-d H:i" }}</span>
</div>
{% for message in messages %}
<article
class="message {{ message.role }}"
id="message-{{ message.pk }}"
data-message-id="{{ message.pk }}"
data-node-label="{% if message.role == 'assistant' %}AI{% else %}用户{% endif %} {{ forloop.counter }}"
>
<div class="message-avatar{% if message.role == 'user' %} user-mark{% endif %}">
{% if message.role == "assistant" %}AI{% else %}{{ request.user.username|slice:":1"|upper }}{% endif %}
</div>
<div class="message-bubble">
{% if message.role == "assistant" %}
<div class="message-content markdown-content"></div>
<template class="message-raw">{{ message.content }}</template>
{% else %}
<p>{{ message.content|linebreaksbr }}</p>
{% endif %}
</div>
</article>
{% endfor %}
{% if condition_confirmation %}
<article
class="message assistant"
id="condition-confirmation-{{ condition_confirmation.id }}"
data-condition-confirmation-card
data-node-label="AI 适用条件确认"
>
<div class="message-avatar">AI</div>
<div class="message-bubble">
<form
class="condition-confirm-form"
data-condition-confirm-form
data-batch-id="{{ condition_confirmation.id }}"
data-confirm-url="/api/review-agent/regulatory-review/{{ condition_confirmation.id }}/conditions/"
>
{% csrf_token %}
<strong>适用条件确认</strong>
<p>请确认 {{ condition_confirmation.batch_no }} 的产品类别、注册类型和临床评价路径,确认后我会继续法规核查。</p>
{% for field, config in condition_confirmation.candidates.items %}
<label>
<span>{{ config.label }}</span>
{% if config.input_type == "select" %}
<select name="{{ field }}">
{% for option in config.options %}
<option value="{{ option }}"{% if option == config.suggested %} selected{% endif %}>{{ option }}</option>
{% endfor %}
</select>
{% else %}
<input type="text" name="{{ field }}" value="{{ config.suggested|default:'' }}">
{% endif %}
</label>
{% endfor %}
<button type="submit">确认并继续</button>
<p class="condition-confirm-status" data-condition-confirm-status></p>
</form>
</div>
</article>
{% endif %}
{% else %}
<div class="empty-state">
<p class="eyebrow">审核智能体</p>
<h1>开始新的审核对话</h1>
<p class="muted">输入资料疑点、法规条款、说明书问题或风险项,系统会为你保留真实会话记录。</p>
</div>
{% endif %}
</div>
<nav class="node-rail{% if not current_conversation %} hidden{% endif %}" id="nodeRail" aria-label="对话节点导航">
<div class="node-rail-line"></div>
{% if current_conversation %}
{% for message in messages %}
{% if message.role == "user" %}
<a
class="node-anchor{% if forloop.last %} latest{% endif %}"
href="#message-{{ message.pk }}"
data-target="message-{{ message.pk }}"
title="用户 {{ forloop.counter }}"
>
<span class="node-dot"></span>
</a>
{% endif %}
{% endfor %}
{% endif %}
</nav>
</div>
<div class="composer-wrap">
<form class="composer" action="{% url 'chat' %}" method="post" id="chatComposer">
{% csrf_token %}
<input type="hidden" name="action" value="send_message">
<input type="hidden" name="conversation_id" id="conversationIdInput" value="{% if current_conversation %}{{ current_conversation.pk }}{% endif %}">
<label class="sr-only" for="prompt">输入消息</label>
<textarea id="prompt" name="prompt" rows="1" placeholder="输入审核问题、法规条款、说明书疑点或上传需求"></textarea>
<div class="composer-actions">
<div class="composer-tools">
<button
class="tool-chip"
type="button"
data-prompt-template="请对当前对话已上传的文件或压缩包自动汇总文件目录、文件类型和页数,并生成可下载的汇总报告。"
>目录自动汇总</button>
<button
class="tool-chip"
type="button"
data-prompt-template="请对当前对话最近成功汇总的注册资料发起 NMPA 法规核查与风险预警,检查完整性、章节结构、一致性、高风险问题、阻断项、证据来源和整改建议。"
>法规核查与风险预警</button>
<button
class="tool-chip"
type="button"
data-prompt-template="请基于当前对话最近成功汇总的产品资料,自动提取产品关键信息并填入申报文件模板"
>申报文件填表</button>
<button
class="tool-chip"
type="button"
data-prompt-template="根据说明书生成第1章监管信息"
>第1章监管信息</button>
</div>
<button class="send-button" type="submit" id="sendButton">发送</button>
</div>
</form>
</div>
</section>
</section>
<aside
class="summary-panel"
id="summaryPanel"
data-attachment-url-template="/api/review-agent/conversations/__conversation_id__/attachments/"
data-message-url-template="/api/review-agent/conversations/__conversation_id__/messages/"
data-status-url-template="/api/review-agent/file-summary/__batch_id__/status/"
data-regulatory-status-url-template="/api/review-agent/regulatory-review/__batch_id__/status/"
data-application-form-fill-status-url-template="/api/review-agent/application-form-fill/__batch_id__/status/"
data-regulatory-info-package-status-url-template="/api/review-agent/regulatory-info-package/__batch_id__/status/"
data-events-url-template="/api/review-agent/file-summary/__batch_id__/events/"
>
<section class="summary-section upload-section">
<div class="summary-heading">
<h2>文件汇总</h2>
<span>当前对话</span>
</div>
<div class="upload-dropzone" id="uploadDropzone" tabindex="0" role="button">
<input id="attachmentInput" type="file" multiple hidden>
<strong>拖拽文件到这里</strong>
<span>支持多文件、zip、7z、rar</span>
</div>
<p class="upload-status" id="uploadStatus">上传后发送“自动汇总文件目录与页数”启动工作流。</p>
</section>
<section class="summary-section attachment-section">
<div class="summary-subheading">
<h3>附件</h3>
<a
class="attachment-manager-link"
href="{% url 'attachment_manager' %}{% if current_conversation %}?conversation={{ current_conversation.pk }}{% endif %}"
aria-label="打开附件管理页面"
></a>
</div>
<div class="attachment-list" id="attachmentList">
{% for attachment in attachments %}
<div class="attachment-item" data-attachment-id="{{ attachment.pk }}">
<div>
<strong>{{ attachment.original_name }}</strong>
<span>v{{ attachment.version_no }} · {{ attachment.file_size }} bytes · {{ attachment.upload_status }}</span>
</div>
{% if attachment.is_active %}<em>active</em>{% endif %}
</div>
{% empty %}
<div class="panel-empty">暂无附件</div>
{% endfor %}
</div>
</section>
<section class="summary-section workflow-section">
<div class="summary-subheading">
<h3>工作流</h3>
</div>
<div class="workflow-card-list workflow-batch-carousel" id="workflowCardList" data-active-index="0">
{% for batch in workflow_cards %}
<article
class="workflow-card{% if forloop.first %} active{% endif %}"
data-batch-id="{{ batch.id }}"
data-workflow-type="{{ batch.workflow_type }}"
data-workflow-index="{{ forloop.counter0 }}"
aria-hidden="{% if forloop.first %}false{% else %}true{% endif %}"
>
<header>
<strong>{{ batch.batch_no }}</strong>
<span class="workflow-status status-{{ batch.status }}">{{ batch.status }}</span>
</header>
{% if batch.risk_label %}
<p class="workflow-risk-summary">{{ batch.risk_label }}</p>
{% endif %}
{% if batch.workflow_type == "regulatory_review" %}
<div class="workflow-card-actions">
<button
type="button"
data-rectification-action="full-review"
data-batch-no="{{ batch.batch_no }}"
>整包复核</button>
<button
type="button"
data-rectification-action="issue-review"
data-batch-no="{{ batch.batch_no }}"
>缺失项复核</button>
</div>
<p class="workflow-record-summary">
通知 {{ batch.notification_count|default:0 }} · 复核记录 {{ batch.review_record_count|default:0 }}
</p>
{% endif %}
{% if batch.error_message %}
<p class="workflow-error">{{ batch.error_message }}</p>
{% endif %}
<ol>
{% for node in batch.nodes %}
<li class="node-status status-{{ node.status }}" data-node-code="{{ node.node_code }}">
<div>
<span>{{ node.node_name }}</span>
{% if node.message %}<small>{{ node.message }}</small>{% endif %}
</div>
<em>{{ node.progress }}%</em>
</li>
{% endfor %}
</ol>
</article>
{% empty %}
<div class="panel-empty">暂无工作流</div>
{% endfor %}
{% if workflow_cards %}
<div class="workflow-batch-controls">
<button type="button" class="workflow-batch-btn" data-workflow-action="prev" aria-label="上一个工作流">&lsaquo;</button>
<div class="workflow-batch-dots" aria-label="工作流批次">
{% for batch in workflow_cards %}
<button
type="button"
class="workflow-batch-dot{% if forloop.first %} active{% endif %}"
data-workflow-index-dot="{{ forloop.counter0 }}"
aria-label="查看{{ batch.batch_no }}状态"
aria-current="{% if forloop.first %}true{% else %}false{% endif %}"
></button>
{% endfor %}
</div>
<button type="button" class="workflow-batch-btn" data-workflow-action="next" aria-label="下一个工作流">&rsaquo;</button>
</div>
{% endif %}
</div>
</section>
</aside>
</section>
</main>
{% endblock %}
{% block scripts %}
<script src="https://cdn.jsdelivr.net/npm/dompurify@3.2.6/dist/purify.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/marked@15.0.12/marked.min.js"></script>
<script src="{% static 'js/app.js' %}?v=20260608-chat-delete1"></script>
{% endblock %}

View File

@@ -0,0 +1,219 @@
{% extends "base.html" %}
{% load static %}
{% block title %}知识库管理 - DEMO-AGENT V2{% endblock %}
{% block body_class %}app-body{% endblock %}
{% block content %}
<main class="app-shell">
<header class="topbar">
<div class="topbar-left">
<div class="tabbar" role="tablist" aria-label="页面切换">
<a class="tab" href="{% url 'home' %}" role="tab" aria-selected="false">首页</a>
<a class="tab" href="{% url 'chat' %}" role="tab" aria-selected="false">审核智能体</a>
<a class="tab active" href="{% url 'knowledge_base_manager' %}" role="tab" aria-selected="true">知识库管理</a>
<a class="tab" href="{% url 'attachment_manager' %}" role="tab" aria-selected="false">附件管理</a>
</div>
</div>
<div class="topbar-right">
<div class="user-menu">
<button class="user-menu-trigger" type="button">
<span class="avatar large">{{ request.user.username|slice:":1"|upper }}</span>
<div class="user-copy">
<strong>{{ request.user.username }}</strong>
<span>当前登录用户</span>
</div>
</button>
</div>
</div>
</header>
<section
class="knowledge-page"
data-document-url="{% url 'knowledge_base_document_list' %}"
data-search-url="{% url 'knowledge_base_search' %}"
data-rebuild-url="{% url 'knowledge_base_rebuild_index' %}"
>
<header class="attachment-manager-hero attachment-manager-toolbar">
<div>
<p class="eyebrow">知识库管理</p>
<h1>知识库管理</h1>
<p>管理当前账号所有对话可调用的法规、制度、模板和审查依据。</p>
</div>
<div class="knowledge-hero-actions">
<span class="knowledge-status status-{{ knowledge_base.status.code }}">{{ knowledge_base.status.label }}</span>
<a class="return-chat-link" href="{% url 'chat' %}">返回对话</a>
</div>
</header>
<div class="attachment-manager-content attachment-manager-split knowledge-workbench">
<aside class="knowledge-left-rail">
<section class="attachment-manager-panel knowledge-panel knowledge-upload-panel">
<div class="summary-subheading">
<h3>上传知识</h3>
<span>所有对话可调用</span>
</div>
<form class="knowledge-document-form" id="knowledgeDocumentForm">
{% csrf_token %}
<div
class="upload-dropzone manager-upload-dropzone knowledge-upload-dropzone"
id="knowledgeUploadDropzone"
tabindex="0"
role="button"
aria-controls="knowledgeDocumentFile"
>
<input id="knowledgeDocumentFile" name="file" type="file" required hidden>
<strong>点击选择文件,或拖拽到这里</strong>
<span>支持 doc、docx、xls、xlsx、ppt、pptx、pdf、txt、md</span>
</div>
<div class="knowledge-inline-actions">
<label class="knowledge-checkbox">
<input name="is_active" type="checkbox" checked>
<span>上传后启用</span>
</label>
<button type="submit">上传并解析</button>
</div>
<p class="upload-status" id="knowledgeDocumentStatus">上传后会进入当前账号的全局知识库。</p>
</form>
</section>
<section class="attachment-manager-panel knowledge-panel knowledge-parse-panel">
<div class="summary-subheading">
<h3>解析与索引</h3>
<span class="knowledge-status status-{{ knowledge_base.status.code }}">{{ knowledge_base.status.label }}</span>
</div>
<dl class="knowledge-compact-stats">
<div>
<dt>向量片段</dt>
<dd>{{ knowledge_base.collection.count }}</dd>
</div>
<div>
<dt>用户材料</dt>
<dd>{{ knowledge_base.managed_document_count|default:0 }}</dd>
</div>
<div>
<dt>内置法规</dt>
<dd>{{ knowledge_base.source_count }}</dd>
</div>
</dl>
<p class="knowledge-panel-note">{{ knowledge_base.status.message }}</p>
<p class="upload-status" id="knowledgeRebuildStatus"></p>
<div class="knowledge-form-actions">
<button type="button" onclick="window.location.reload()">刷新状态</button>
<button type="button" id="knowledgeRebuildIndexButton">重建索引</button>
</div>
</section>
<section class="attachment-manager-panel knowledge-panel knowledge-search-panel">
<div class="summary-subheading">
<h3>RAG 检索测试</h3>
<span>Top 3</span>
</div>
<form class="knowledge-search-form" id="knowledgeSearchForm">
{% csrf_token %}
<label class="sr-only" for="knowledgeSearchQuery">检索问题</label>
<input id="knowledgeSearchQuery" name="query" type="search" placeholder="输入审查问题或关键词">
<button type="submit">测试检索</button>
</form>
<div class="knowledge-search-results" id="knowledgeSearchResults">
<p class="panel-empty">输入问题后查看命中材料、依据片段和相似度。</p>
</div>
</section>
</aside>
<section class="knowledge-right-display">
<section class="attachment-manager-panel knowledge-panel knowledge-document-list-panel">
<div class="summary-subheading">
<h3>知识库材料列表</h3>
<input class="attachment-search" id="knowledgeDocumentSearch" type="search" placeholder="搜索文件名">
</div>
<div class="attachment-table-wrap">
<table class="attachment-table knowledge-document-table" id="knowledgeDocumentTable">
<thead>
<tr>
<th>状态</th>
<th>材料名称</th>
<th>文件名</th>
<th>大小</th>
<th>入库状态</th>
<th>更新时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{% for document in knowledge_base.managed_documents %}
<tr
data-document-id="{{ document.id }}"
data-detail-url="/api/review-agent/knowledge-base/documents/{{ document.id }}/"
data-index-url="/api/review-agent/knowledge-base/documents/{{ document.id }}/index/"
>
<td>{% if document.is_active %}启用{% else %}停用{% endif %}</td>
<td class="attachment-name">{{ document.display_name }}</td>
<td>{{ document.original_name }}</td>
<td>{{ document.file_size }} bytes</td>
<td>{{ document.indexed_label }}</td>
<td>{{ document.updated_at|slice:":19" }}</td>
<td class="attachment-actions">
<button type="button" data-kb-action="index">解析入库</button>
<button type="button" data-kb-action="edit">编辑</button>
<button type="button" data-kb-action="toggle">{% if document.is_active %}停用{% else %}启用{% endif %}</button>
<button type="button" data-kb-action="delete">删除</button>
</td>
</tr>
{% empty %}
<tr>
<td colspan="7" class="table-empty">当前知识库暂无材料</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</section>
<section class="attachment-manager-panel knowledge-panel knowledge-source-panel">
<div class="summary-subheading">
<h3>内置法规材料</h3>
<input class="attachment-search" id="knowledgeSourceSearch" type="search" placeholder="搜索内置材料">
</div>
<div class="attachment-table-wrap">
<table class="attachment-table knowledge-source-table" id="knowledgeSourceTable">
<thead>
<tr>
<th>状态</th>
<th>文件</th>
<th>类型</th>
<th>大小</th>
<th>索引</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{% for source in knowledge_base.sources %}
<tr data-source-name="{{ source.name }}">
<td>{% if source.supported %}可解析{% else %}暂不支持{% endif %}</td>
<td class="attachment-name">{{ source.relative_path }}</td>
<td>{{ source.suffix }}</td>
<td>{{ source.size }} bytes</td>
<td>{{ source.indexed_label }}</td>
<td class="attachment-actions">
<button type="button" data-source-action="index">手动入库</button>
</td>
</tr>
{% empty %}
<tr>
<td colspan="6" class="table-empty">暂无法规材料</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</section>
</section>
</div>
</section>
</main>
{% endblock %}
{% block scripts %}
<script src="{% static 'js/knowledge_base.js' %}?v=20260608-kb6"></script>
{% endblock %}

View File

@@ -0,0 +1,30 @@
{% extends "base.html" %}
{% block title %}登录 - DEMO-AGENT V2{% endblock %}
{% block content %}
<main class="login-page">
<section class="login-card" aria-labelledby="login-title">
<p class="eyebrow">DEMO-AGENT V2</p>
<h1 id="login-title">登录系统</h1>
<p class="muted">请输入账号和密码进入 Django 基础后台。</p>
{% if form.errors %}
<div class="alert" role="alert">用户名或密码不正确,请重新输入。</div>
{% endif %}
<form method="post" novalidate>
{% csrf_token %}
<input type="hidden" name="next" value="{{ next }}">
<label for="{{ form.username.id_for_label }}">用户名</label>
{{ form.username }}
<label for="{{ form.password.id_for_label }}">密码</label>
{{ form.password }}
<button class="button" type="submit">登录</button>
</form>
</section>
</main>
{% endblock %}

View File

@@ -0,0 +1,31 @@
{% extends "base.html" %}
{% block title %}修改密码 - DEMO-AGENT V2{% endblock %}
{% block content %}
<main class="login-page">
<section class="login-card">
<p class="eyebrow">审核智能体</p>
<h1>修改密码</h1>
<p class="muted">输入当前密码,并设置新的登录密码。</p>
<form method="post">
{% csrf_token %}
{% if form.non_field_errors %}
<div class="alert" role="alert">{{ form.non_field_errors }}</div>
{% endif %}
{% for field in form %}
<label for="{{ field.id_for_label }}">{{ field.label }}</label>
{{ field }}
{% if field.errors %}
<div class="alert" role="alert">{{ field.errors }}</div>
{% endif %}
{% endfor %}
<button class="button" type="submit">保存密码</button>
</form>
</section>
</main>
{% endblock %}

173
templates/workbench.html Normal file
View File

@@ -0,0 +1,173 @@
{% extends "base.html" %}
{% load static %}
{% block title %}首页 - DEMO-AGENT V2{% endblock %}
{% block body_class %}app-body{% endblock %}
{% block content %}
<main class="app-shell">
<header class="topbar">
<div class="topbar-left">
<div class="tabbar" role="tablist" aria-label="页面切换">
<a class="tab active" href="{% url 'home' %}" role="tab" aria-selected="true">首页</a>
<a class="tab" href="{% url 'chat' %}" role="tab" aria-selected="false">审核智能体</a>
<a class="tab" href="{% url 'knowledge_base_manager' %}" role="tab" aria-selected="false">知识库管理</a>
<a class="tab" href="{% url 'attachment_manager' %}" role="tab" aria-selected="false">附件管理</a>
</div>
</div>
<div class="topbar-right">
<div class="user-menu">
<button class="user-menu-trigger" type="button">
<span class="avatar large">{{ request.user.username|slice:":1"|upper }}</span>
<div class="user-copy">
<strong>{{ request.user.username }}</strong>
<span>当前登录用户</span>
</div>
</button>
</div>
</div>
</header>
<section class="dashboard-page">
<header class="dashboard-hero attachment-manager-toolbar">
<div>
<p class="eyebrow">首页</p>
<h1>注册资料审核工作台</h1>
<p>当前账号资料、知识库、附件与审核处理数据总览。</p>
</div>
<a class="return-chat-link dashboard-primary-action" href="{% url 'chat' %}">进入审核智能体</a>
</header>
<section class="metric-grid" aria-label="首页关键指标">
<article class="metric-card">
<span>对话总数</span>
<strong>{{ dashboard.metrics.conversation_count }}</strong>
<em>已处理 {{ dashboard.metrics.recent_conversation_count }}</em>
</article>
<article class="metric-card">
<span>附件总数</span>
<strong>{{ dashboard.metrics.attachment_count }}</strong>
<em>启用 {{ dashboard.metrics.active_attachment_count }}</em>
</article>
<article class="metric-card">
<span>知识库材料</span>
<strong>{{ dashboard.metrics.knowledge_document_count }}</strong>
<em>管理 {{ dashboard.knowledge.document_count }} · 内置 {{ dashboard.knowledge.builtin_source_count }}</em>
</article>
<article class="metric-card">
<span>执行中批次</span>
<strong>{{ dashboard.metrics.running_batch_count }}</strong>
<em>总批次 {{ dashboard.metrics.total_batch_count }}</em>
</article>
<article class="metric-card">
<span>已处理批次</span>
<strong>{{ dashboard.metrics.handled_batch_count }}</strong>
<em>成功 {{ dashboard.metrics.success_batch_count }}</em>
</article>
<article class="metric-card">
<span>等待确认</span>
<strong>{{ dashboard.metrics.waiting_batch_count }}</strong>
<em>需人工处理</em>
</article>
<article class="metric-card">
<span>失败批次</span>
<strong>{{ dashboard.metrics.failed_batch_count }}</strong>
<em>需排查</em>
</article>
<article class="metric-card">
<span>申报填表</span>
<strong>{{ dashboard.workflow.application_form_fill_count }}</strong>
<em>自动填表批次</em>
</article>
</section>
<div class="dashboard-split">
<section class="attachment-manager-panel dashboard-panel">
<div class="summary-subheading">
<h3>知识库概览</h3>
</div>
<dl class="dashboard-stat-list">
<div>
<dt>管理文档</dt>
<dd>{{ dashboard.knowledge.document_count }}</dd>
</div>
<div>
<dt>内置材料</dt>
<dd>{{ dashboard.knowledge.builtin_source_count }}</dd>
</div>
<div>
<dt>已索引</dt>
<dd>{{ dashboard.knowledge.indexed_document_count }}</dd>
</div>
<div>
<dt>向量片段</dt>
<dd>{{ dashboard.knowledge.chunk_count }}</dd>
</div>
</dl>
</section>
<section class="attachment-manager-panel dashboard-panel">
<div class="summary-subheading">
<h3>附件与文档概览</h3>
</div>
<dl class="dashboard-stat-list">
<div>
<dt>附件总数</dt>
<dd>{{ dashboard.attachments.attachment_count }}</dd>
</div>
<div>
<dt>启用附件</dt>
<dd>{{ dashboard.attachments.active_attachment_count }}</dd>
</div>
<div>
<dt>最近上传</dt>
<dd>{{ dashboard.attachments.recent_attachment_count }}</dd>
</div>
<div>
<dt>关联对话</dt>
<dd>{{ dashboard.attachments.conversation_count }}</dd>
</div>
</dl>
</section>
</div>
<section class="attachment-manager-panel dashboard-panel">
<div class="summary-subheading">
<h3>最近处理记录</h3>
<span>最近 8 条</span>
</div>
<div class="attachment-table-wrap">
<table class="attachment-table recent-activity-table">
<thead>
<tr>
<th>类型</th>
<th>名称或批次号</th>
<th>状态</th>
<th>更新时间</th>
<th>入口</th>
</tr>
</thead>
<tbody>
{% for record in dashboard.recent_records %}
<tr>
<td>{{ record.type }}</td>
<td class="attachment-name">{{ record.title }}</td>
<td>{{ record.status }}</td>
<td>{{ record.updated_at|date:"Y-m-d H:i" }}</td>
<td class="attachment-actions">
<a href="{{ record.url }}">查看</a>
</td>
</tr>
{% empty %}
<tr>
<td colspan="5" class="table-empty">暂无处理记录</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</section>
</section>
</main>
{% endblock %}