feat(chat): refine workspace chat layout

This commit is contained in:
2026-06-04 22:30:37 +08:00
parent efb06519d8
commit fecaee0b03
2 changed files with 473 additions and 73 deletions

View File

@@ -376,7 +376,7 @@
</div>
</header>
<main class="page">
<main class="page {% block page_class %}{% endblock %}">
{% if messages %}
<div class="stack">
{% for message in messages %}

View File

@@ -1,16 +1,359 @@
{% extends "base.html" %}
{% block title %}审核智能体{% endblock %}
{% block page_class %}chat-page{% endblock %}
{% block content %}
<section class="page-header">
<style>
.chat-page {
width: min(100vw - 32px, 1920px);
margin-top: 12px;
gap: 12px;
}
.chat-hero {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
min-height: 74px;
padding: 16px 20px;
}
.chat-hero .page-title {
font-size: 1.45rem;
}
.chat-layout {
display: grid;
grid-template-columns: 280px minmax(680px, 1fr) 340px;
gap: 12px;
align-items: start;
}
.history-toggle {
position: absolute;
opacity: 0;
pointer-events: none;
}
.chat-layout:has(.history-toggle:checked) {
grid-template-columns: 54px minmax(760px, 1fr) 340px;
}
.chat-layout:has(.history-toggle:checked) .history-content {
display: none;
}
.chat-layout:has(.history-toggle:checked) .history-panel {
padding: 12px 8px;
}
.chat-layout:has(.history-toggle:checked) .history-collapse {
width: 36px;
height: 36px;
padding: 0;
}
.chat-layout:has(.history-toggle:checked) .history-collapse span {
display: none;
}
.history-panel,
.right-rail {
position: sticky;
top: 84px;
}
.history-head,
.right-rail-head,
.chat-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
margin-bottom: 12px;
}
.history-collapse {
flex: none;
min-height: 34px;
border-radius: 8px;
font-size: 0.86rem;
background: var(--surface-soft);
color: var(--muted);
}
.history-list {
max-height: calc(100vh - 230px);
overflow: auto;
padding-right: 2px;
}
.history-card {
border-radius: 10px;
background: #fff;
}
.chat-shell {
min-height: calc(100vh - 190px);
display: grid;
grid-template-rows: auto minmax(360px, 1fr) auto;
padding: 0;
overflow: hidden;
}
.chat-head {
padding: 16px 18px 12px;
border-bottom: 1px solid var(--border);
margin: 0;
}
.node-strip {
display: flex;
gap: 8px;
overflow-x: auto;
padding-bottom: 2px;
max-width: 52vw;
}
.chat-thread {
display: grid;
align-content: start;
gap: 18px;
padding: 22px min(7vw, 72px);
background: linear-gradient(180deg, #ffffff 0%, #f8fafc 100%);
overflow: auto;
}
.chat-message {
display: grid;
grid-template-columns: 34px minmax(0, 1fr);
gap: 12px;
max-width: 980px;
}
.chat-message.user {
justify-self: end;
grid-template-columns: minmax(0, 1fr) 34px;
}
.avatar {
width: 34px;
height: 34px;
border-radius: 10px;
display: inline-flex;
align-items: center;
justify-content: center;
font-weight: 700;
font-size: 0.82rem;
background: var(--primary);
color: #fff;
}
.chat-message.user .avatar {
grid-column: 2;
background: #111827;
}
.bubble {
padding: 13px 15px;
border: 1px solid var(--border);
border-radius: 12px;
background: #fff;
line-height: 1.75;
box-shadow: 0 4px 14px rgba(31, 45, 61, 0.04);
}
.chat-message.user .bubble {
grid-column: 1;
grid-row: 1;
background: #eef4ff;
border-color: #d8e5ff;
}
.bubble-meta {
display: block;
margin-bottom: 4px;
color: var(--muted);
font-size: 0.78rem;
font-weight: 700;
}
.prompt-chips {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.composer {
padding: 14px min(7vw, 72px) 18px;
border-top: 1px solid var(--border);
background: rgba(255, 255, 255, 0.96);
}
.composer-box {
display: grid;
gap: 10px;
max-width: 1040px;
margin: 0 auto;
border: 1px solid var(--border);
border-radius: 16px;
background: #fff;
padding: 10px;
box-shadow: 0 10px 28px rgba(31, 45, 61, 0.08);
}
.composer-box label {
display: none;
}
.composer-box textarea {
height: 86px;
min-height: 78px;
max-height: 180px;
border: 0;
padding: 8px 10px;
resize: vertical;
box-shadow: none;
}
.composer-actions {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
border-top: 1px solid #edf1f5;
padding-top: 10px;
}
.scope-summary {
color: var(--muted);
font-size: 0.86rem;
}
.document-scope {
display: none;
padding: 10px;
border-radius: 10px;
background: var(--surface-soft);
}
.composer-box:focus-within .document-scope {
display: block;
}
.send-button {
border-radius: 999px;
min-width: 92px;
}
.result-panel {
padding: 14px 18px;
}
.upload-card {
background: linear-gradient(180deg, #ffffff 0%, #f8fbff 100%);
}
.upload-dropzone {
position: relative;
display: grid;
place-items: center;
min-height: 164px;
padding: 18px;
border: 1.5px dashed #9fb7e8;
border-radius: 12px;
background: #f4f8ff;
text-align: center;
color: var(--text);
overflow: hidden;
}
.upload-dropzone input[type="file"] {
position: absolute;
inset: 0;
opacity: 0;
cursor: pointer;
}
.upload-icon {
width: 44px;
height: 44px;
border-radius: 12px;
display: inline-flex;
align-items: center;
justify-content: center;
margin: 0 auto 10px;
background: #ffffff;
color: var(--primary);
box-shadow: 0 6px 18px rgba(47, 111, 236, 0.14);
font-size: 1.3rem;
font-weight: 700;
}
.upload-dropzone strong {
display: block;
margin-bottom: 4px;
}
.rail-card {
border: 1px solid var(--border);
border-radius: 12px;
background: #fff;
padding: 12px;
}
.status-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px;
}
.status-tile {
min-height: 74px;
border: 1px solid var(--border);
border-radius: 12px;
padding: 10px;
background: var(--surface-soft);
}
.status-tile strong {
display: block;
margin-bottom: 6px;
font-size: 0.82rem;
}
@media (max-width: 1200px) {
.chat-layout,
.chat-layout:has(.history-toggle:checked) {
grid-template-columns: 1fr;
}
.history-panel,
.right-rail {
position: static;
}
.node-strip {
max-width: 100%;
}
.chat-thread,
.composer {
padding-left: 18px;
padding-right: 18px;
}
}
</style>
<section class="page-header chat-hero">
<div>
<span class="eyebrow">Agent Workspace</span>
<h1 class="page-title">审核智能体</h1>
<p class="page-lead">以会话为中心组织资料包上传、节点审核结果和动态任务信息卡</p>
<p class="page-lead">围绕会话完成知识库问答、资料包上传、节点审核和导出留痕</p>
</div>
{% if conversation %}
<div class="badge-row">
<span class="pill pill-accent">批次:{{ conversation.batch_id }}</span>
<span class="pill">产品:{{ conversation.product_name|default:"未识别产品名称" }}</span>
<span class="pill pill-accent">批次:{{ conversation.batch_id|default:"未绑定" }}</span>
<span class="pill">产品:{{ conversation.product_name|default:"知识库问答" }}</span>
<span class="pill">阶段:{{ conversation_context.task_status }}</span>
</div>
{% endif %}
@@ -43,14 +386,20 @@
</section>
{% endif %}
<section class="workspace-grid" style="grid-template-columns: 320px minmax(0, 1fr) 360px;">
<div class="stack">
<article class="panel">
<section class="chat-layout">
<input class="history-toggle" id="history-collapsed" type="checkbox">
<aside class="panel history-panel">
<div class="history-head">
<div class="history-content">
<h2 class="section-title">会话历史</h2>
<p class="section-copy">左侧保留历史会话,标题默认使用解析后的产品名称</p>
<ul class="detail-list">
<p class="section-copy">按会话快速切换资料包和知识库问答</p>
</div>
<label class="button history-collapse" for="history-collapsed" title="收起或展开会话历史"> <span>收起</span></label>
</div>
<div class="history-content">
<ul class="detail-list history-list">
{% for item in conversation_history %}
<li class="detail-item">
<li class="detail-item history-card">
<strong><a href="{% url 'chat:detail' item.conversation_id %}">{{ item.title }}</a></strong>
<div class="muted">产品:{{ item.product_name|default:"未识别" }}</div>
<div class="muted">批次:{{ item.batch_id|default:"未绑定" }}</div>
@@ -67,34 +416,78 @@
</li>
{% endfor %}
</ul>
</article>
</div>
</aside>
<div class="stack">
<article class="panel">
<div class="section-heading">
<article class="panel chat-shell">
<div class="chat-head">
<div>
<h2 class="section-title">对话区与节点导航</h2>
<p class="section-copy">中间区域承接用户问题、Agent 回答和节点式结果摘要</p>
</div>
<p class="section-copy">像聊天一样提问Agent 会结合知识库和所选文档回答</p>
</div>
{% if node_results %}
<div class="badge-row" style="margin-bottom: 14px;">
<div class="node-strip">
{% for node in node_results %}
<span class="pill {% if node.status == '已完成' %}pill-success{% else %}pill-signal{% endif %}">{{ node.label }} / {{ node.status }}</span>
{% endfor %}
</div>
{% endif %}
<form method="post" class="stack" action="{% if conversation %}{% url 'chat:detail' conversation.conversation_id %}{% else %}{% url 'chat:index' %}{% endif %}">
</div>
<div class="chat-thread">
{% if conversation %}
<div class="chat-message assistant">
<div class="avatar">RA</div>
<div class="bubble">
<span class="bubble-meta">审核智能体</span>
当前会话已就绪。可以询问资料目录、法规依据、字段抽取、一致性问题或风险整改建议。
</div>
</div>
{% else %}
<div class="chat-message assistant">
<div class="avatar">RA</div>
<div class="bubble">
<span class="bubble-meta">审核智能体</span>
可以先不上传资料,直接询问注册法规、资料清单、模板字段或历史知识库内容。
</div>
</div>
{% endif %}
{% if form.message.value %}
<div class="chat-message user">
<div class="avatar"></div>
<div class="bubble">
<span class="bubble-meta">用户问题</span>
{{ form.message.value|linebreaksbr }}
</div>
</div>
{% endif %}
{% if result %}
<div class="chat-message assistant">
<div class="avatar">RA</div>
<div class="bubble">
<span class="bubble-meta">Agent 回答</span>
{{ result.answer|linebreaksbr }}
</div>
</div>
{% else %}
<div class="prompt-chips">
{% for item in prompt_templates %}
<span class="pill pill-accent">{{ item }}</span>
{% endfor %}
</div>
{% endif %}
</div>
<form method="post" class="composer" action="{% if conversation %}{% url 'chat:detail' conversation.conversation_id %}{% else %}{% url 'chat:index' %}{% endif %}">
{% csrf_token %}
<div>
<div class="composer-box">
{{ form.message.label_tag }}
{{ form.message }}
{% if form.message.errors %}
<p class="notice notice-error">{{ form.message.errors|join:" " }}</p>
{% endif %}
</div>
<div>
<div class="document-scope">
{{ form.document_ids.label_tag }}
<div class="checkbox-list">
{% for checkbox in form.document_ids %}
@@ -107,21 +500,15 @@
{% endfor %}
</div>
</div>
<div class="button-row">
<button type="submit">发送问题</button>
<div class="composer-actions">
<span class="scope-summary">默认检索知识库;聚焦输入框可选择文档范围。</span>
<button class="send-button" type="submit">发送问题</button>
</div>
</div>
</form>
{% if result %}
<div class="detail-item" style="margin-top: 16px;">
<strong>Agent 回答</strong>
<div>{{ result.answer|linebreaksbr }}</div>
</div>
{% elif not conversation %}
<div class="notice" style="margin-top: 16px;">可以先不上传资料,直接询问注册法规、资料清单、模板字段或历史知识库内容。</div>
{% endif %}
</article>
<article class="panel">
<article class="panel result-panel">
<h2 class="section-title">节点式结果</h2>
{% if analysis_card or export_card or risk_card or notify_card %}
<div class="stack">
@@ -391,13 +778,17 @@
</article>
</div>
<div class="stack">
<article class="panel">
<aside class="stack right-rail">
<article class="panel upload-card">
<div class="right-rail-head">
<div>
<h2 class="section-title">上传区</h2>
<p class="section-copy">右侧保留资料包上传入口和当前会话的资料上下文</p>
<p class="section-copy">拖拽或点击导入资料包</p>
</div>
</div>
{% if batch %}
<ul class="detail-list">
<li class="detail-item">
<ul class="detail-list" style="margin-bottom: 14px;">
<li class="detail-item rail-card">
<strong>当前资料包</strong>
<div>批次:{{ batch.batch_id }}</div>
<div>文件数:{{ batch.file_count }}</div>
@@ -407,10 +798,14 @@
</ul>
<form method="post" action="{% url 'chat:upload-documents' conversation.conversation_id %}" enctype="multipart/form-data" class="stack" style="margin-top: 16px;">
{% csrf_token %}
<div>
{{ upload_form.files.label_tag }}
<label class="upload-dropzone">
{{ upload_form.files }}
</div>
<span>
<span class="upload-icon"></span>
<strong>拖拽补充资料到这里</strong>
<span class="muted">或点击选择文件,支持多选和压缩包。</span>
</span>
</label>
<div class="button-row">
<button type="submit">继续上传资料</button>
<a class="button" href="{% url 'documents:list' %}">返回资料包</a>
@@ -430,11 +825,14 @@
<form method="post" action="{% url 'documents:upload' %}" enctype="multipart/form-data" class="stack" style="margin-top: 16px;">
{% csrf_token %}
<input type="hidden" name="scenario_id" value="document_review">
<div>
{{ upload_form.files.label_tag }}
<label class="upload-dropzone">
{{ upload_form.files }}
<p class="help-text">支持 PDF、DOCX、MD、TXT、ZIP、7Z 与 RAR。上传后会自动形成资料包并绑定新的审核会话。</p>
</div>
<span>
<span class="upload-icon"></span>
<strong>拖拽资料包到这里</strong>
<span class="muted">PDF、DOCX、MD、TXT、ZIP、7Z、RAR 均可。</span>
</span>
</label>
<div class="button-row">
<button type="submit">导入资料包</button>
<a class="button" href="{% url 'documents:upload' %}">打开导入向导</a>
@@ -445,24 +843,26 @@
<article class="panel">
<h2 class="section-title">动态信息卡</h2>
<ul class="detail-list">
<li class="detail-item">
<div class="status-grid">
<div class="status-tile">
<strong>最高风险等级</strong>
<div>{{ workspace_summary.highest_risk_level }}</div>
</li>
<li class="detail-item">
</div>
<div class="status-tile">
<strong>是否允许正式导出</strong>
<div>{{ workspace_summary.export_allowed }}</div>
</li>
<li class="detail-item">
</div>
<div class="status-tile">
<strong>通知状态</strong>
<div>{{ workspace_summary.notify_status }}</div>
</li>
<li class="detail-item">
</div>
<div class="status-tile">
<strong>导出状态</strong>
<div>{{ workspace_summary.export_status }}</div>
</li>
<li class="detail-item">
</div>
</div>
<ul class="detail-list" style="margin-top: 10px;">
<li class="detail-item rail-card">
<strong>导出下载地址</strong>
<div>
{% if workspace_summary.download_url %}
@@ -478,6 +878,6 @@
{% endif %}
</ul>
</article>
</div>
</aside>
</section>
{% endblock %}