373 lines
17 KiB
HTML
373 lines
17 KiB
HTML
{% 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="上一个工作流">‹</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="下一个工作流">›</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 %}
|