277 lines
12 KiB
HTML
277 lines
12 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="页面切换">
|
|
<button class="tab" type="button" role="tab" aria-selected="false">首页</button>
|
|
<button class="tab" type="button" role="tab" aria-selected="false">知识库管理</button>
|
|
<button class="tab active" type="button" role="tab" aria-selected="true">审核智能体</button>
|
|
<button class="tab" type="button" role="tab" aria-selected="false">视频实时监测</button>
|
|
</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 %}
|
|
<a
|
|
class="history-item{% if current_conversation and current_conversation.pk == conversation.pk %} active{% endif %}"
|
|
data-conversation-id="{{ conversation.pk }}"
|
|
href="/?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>
|
|
{% 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 %}
|
|
{% 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="/" 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">
|
|
<span class="tool-chip passive-chip">法规核查</span>
|
|
<span class="tool-chip passive-chip">说明书审核</span>
|
|
<span class="tool-chip passive-chip">风险识别</span>
|
|
</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-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>
|
|
</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 summary_batches %}
|
|
<article
|
|
class="workflow-card{% if forloop.first %} active{% endif %}"
|
|
data-batch-id="{{ batch.pk }}"
|
|
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.error_message %}
|
|
<p class="workflow-error">{{ batch.error_message }}</p>
|
|
{% endif %}
|
|
<ol>
|
|
{% for node in batch.node_runs.all %}
|
|
<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 summary_batches %}
|
|
<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 summary_batches %}
|
|
<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' %}"></script>
|
|
{% endblock %}
|