feat(file-summary): 增加前端汇总面板
This commit is contained in:
74
docs/5.开发计划/1.自动汇总-前端线框图.md
Normal file
74
docs/5.开发计划/1.自动汇总-前端线框图.md
Normal file
@@ -0,0 +1,74 @@
|
||||
# 自动汇总前端线框图
|
||||
|
||||
## 评审目标
|
||||
|
||||
在实现三栏页面前,先确认审核智能体工作台的信息架构、右侧文件汇总面板、工作流状态展示和移动端降级方式。
|
||||
|
||||
## 桌面端布局
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A["左栏:会话列表<br/>新对话 / 搜索 / 历史会话"] --> B["中栏:聊天区<br/>顶部导航 / 消息流 / 输入框"]
|
||||
B --> C["右栏:文件汇总面板"]
|
||||
C --> C1["上半区:上传区<br/>拖拽上传 / 选择文件 / 上传状态"]
|
||||
C --> C2["中段:当前对话附件<br/>文件名 / 版本 / 大小 / 状态 / 删除"]
|
||||
C --> C3["下半区:工作流卡片<br/>批次号 / 节点进度 / 下载入口"]
|
||||
```
|
||||
|
||||
## 右侧面板结构
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
P["文件汇总面板"] --> U["上传拖拽区"]
|
||||
U --> U0["无附件:提示上传文件或压缩包"]
|
||||
U --> U1["上传中:显示文件名和处理中状态"]
|
||||
U --> U2["上传失败:展示错误并允许重试"]
|
||||
P --> L["附件列表"]
|
||||
L --> L1["active 版本优先展示"]
|
||||
L --> L2["历史版本保留展示"]
|
||||
L --> L3["逻辑删除后从默认候选移除"]
|
||||
P --> W["工作流卡片列表"]
|
||||
W --> W1["运行中:节点逐项更新"]
|
||||
W --> W2["成功:展示 Markdown/Excel 下载"]
|
||||
W --> W3["失败:展示失败节点和错误说明"]
|
||||
```
|
||||
|
||||
## 工作流状态流转
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> Pending: 用户上传附件
|
||||
Pending --> Running: 发送自动汇总提示词
|
||||
Running --> Extracting: 固化附件
|
||||
Extracting --> Scanning: 解压完成或跳过
|
||||
Scanning --> Counting: 生成文件清单
|
||||
Counting --> Detecting: 页数统计完成
|
||||
Detecting --> Reporting: 产品名识别完成
|
||||
Reporting --> Success: 生成报告与下载
|
||||
Running --> Failed: 批次级异常
|
||||
Extracting --> Failed: 解压安全检查失败
|
||||
Reporting --> Failed: 报告生成失败
|
||||
Success --> Restored: 刷新页面后状态恢复
|
||||
Failed --> Restored: 刷新页面后状态恢复
|
||||
```
|
||||
|
||||
## 移动端布局
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
M["移动端工作台"] --> T["顶部:侧栏按钮 / 当前页面 / 用户菜单"]
|
||||
T --> Chat["聊天区优先展示"]
|
||||
Chat --> Composer["底部输入框"]
|
||||
T --> Drawer["会话侧栏抽屉"]
|
||||
Chat --> Panel["文件汇总面板下移或折叠"]
|
||||
Panel --> Upload["上传区"]
|
||||
Panel --> Workflow["工作流卡片"]
|
||||
```
|
||||
|
||||
## 关键评审点
|
||||
|
||||
- 桌面端保持左侧会话、中间聊天、右侧文件汇总三栏,不改变现有聊天主路径。
|
||||
- 右侧面板上半部分用于上传和附件列表,下半部分用于批次工作流卡片。
|
||||
- 工作流卡片节点顺序固定为:附件固化、压缩包解压、文件扫描、页数统计、产品识别、报告输出、完成。
|
||||
- 助手消息中的文件汇总结果使用安全 Markdown 渲染,用户消息仍按纯文本转义。
|
||||
- 移动端优先保证聊天可用,文件汇总面板折叠或下移,不能遮挡输入框。
|
||||
@@ -10,6 +10,7 @@ from .services import (
|
||||
send_message,
|
||||
stream_message,
|
||||
)
|
||||
from .models import FileAttachment, FileSummaryBatch
|
||||
|
||||
|
||||
@login_required
|
||||
@@ -49,6 +50,8 @@ def workspace(request: HttpRequest) -> HttpResponse:
|
||||
"conversations": conversations,
|
||||
"current_conversation": current,
|
||||
"messages": current.messages.all() if current else [],
|
||||
"attachments": FileAttachment.objects.filter(conversation=current).order_by("original_name", "-version_no") if current else [],
|
||||
"summary_batches": FileSummaryBatch.objects.filter(conversation=current).prefetch_related("node_runs").order_by("-created_at")[:5] if current else [],
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -127,7 +127,7 @@ input:focus {
|
||||
|
||||
.workspace {
|
||||
display: grid;
|
||||
grid-template-columns: 296px minmax(0, 1fr);
|
||||
grid-template-columns: 296px minmax(0, 1fr) 340px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
@@ -760,9 +760,176 @@ input:focus {
|
||||
padding-right: 12px;
|
||||
}
|
||||
|
||||
.summary-panel {
|
||||
display: grid;
|
||||
grid-template-rows: auto auto minmax(0, 1fr);
|
||||
gap: 14px;
|
||||
min-width: 0;
|
||||
max-height: 100vh;
|
||||
padding: 16px;
|
||||
overflow: auto;
|
||||
border-left: 1px solid var(--line);
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.summary-section {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
padding: 14px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
background: var(--panel-soft);
|
||||
}
|
||||
|
||||
.summary-heading,
|
||||
.summary-subheading,
|
||||
.workflow-card header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.summary-heading h2,
|
||||
.summary-subheading h3 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.summary-heading span {
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.upload-dropzone {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
gap: 6px;
|
||||
min-height: 112px;
|
||||
padding: 18px;
|
||||
border: 1px dashed var(--accent);
|
||||
border-radius: 8px;
|
||||
background: #f5f9ff;
|
||||
color: var(--text);
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.upload-dropzone.dragging {
|
||||
border-color: var(--accent-dark);
|
||||
background: #eaf2ff;
|
||||
}
|
||||
|
||||
.upload-dropzone span,
|
||||
.upload-status,
|
||||
.attachment-item span,
|
||||
.workflow-card em {
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.upload-status {
|
||||
margin: 0;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.attachment-list,
|
||||
.workflow-card-list {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.attachment-item,
|
||||
.workflow-card {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
padding: 12px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.attachment-item {
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.attachment-item strong,
|
||||
.workflow-card strong {
|
||||
display: block;
|
||||
overflow-wrap: anywhere;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.attachment-item em,
|
||||
.workflow-status {
|
||||
padding: 3px 8px;
|
||||
border-radius: 999px;
|
||||
background: #eaf2ff;
|
||||
color: var(--accent);
|
||||
font-size: 11px;
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.workflow-card ol {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.node-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
padding: 8px 0;
|
||||
border-top: 1px solid var(--line);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.status-running,
|
||||
.status-retrying {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.status-success {
|
||||
color: #047857;
|
||||
}
|
||||
|
||||
.status-failed {
|
||||
color: var(--danger-text);
|
||||
}
|
||||
|
||||
.panel-empty {
|
||||
padding: 14px;
|
||||
border: 1px dashed var(--line);
|
||||
border-radius: 8px;
|
||||
color: var(--muted);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.message-bubble table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.message-bubble th,
|
||||
.message-bubble td {
|
||||
padding: 8px;
|
||||
border: 1px solid var(--line);
|
||||
text-align: left;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
@media (max-width: 980px) {
|
||||
.workspace {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
min-height: 100vh;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
@@ -815,7 +982,14 @@ input:focus {
|
||||
}
|
||||
|
||||
.chat-stage {
|
||||
height: calc(100vh - 88px);
|
||||
min-height: calc(100vh - 88px);
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.summary-panel {
|
||||
max-height: none;
|
||||
border-left: 0;
|
||||
border-top: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.chat-scroll {
|
||||
@@ -889,7 +1063,7 @@ input:focus {
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
.node-dot {
|
||||
.node-dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
189
static/js/app.js
189
static/js/app.js
@@ -11,6 +11,12 @@
|
||||
var sendButton = document.getElementById("sendButton");
|
||||
var conversationIdInput = document.getElementById("conversationIdInput");
|
||||
var chatStage = document.querySelector(".chat-stage");
|
||||
var summaryPanel = document.getElementById("summaryPanel");
|
||||
var uploadDropzone = document.getElementById("uploadDropzone");
|
||||
var attachmentInput = document.getElementById("attachmentInput");
|
||||
var attachmentList = document.getElementById("attachmentList");
|
||||
var uploadStatus = document.getElementById("uploadStatus");
|
||||
var workflowCardList = document.getElementById("workflowCardList");
|
||||
var nodeAnchors = [];
|
||||
|
||||
if (!workspace) {
|
||||
@@ -32,7 +38,7 @@
|
||||
|
||||
function syncSidebarState() {
|
||||
if (isMobile()) {
|
||||
if (workspace.getAttribute("data-sidebar-state") === "collapsed") {
|
||||
if (workspace.getAttribute("data-sidebar-state") !== "closed") {
|
||||
workspace.setAttribute("data-sidebar-state", "closed");
|
||||
}
|
||||
} else if (workspace.getAttribute("data-sidebar-state") === "closed") {
|
||||
@@ -147,6 +153,13 @@
|
||||
return escapeHtml(text).replace(/\n/g, "<br>");
|
||||
}
|
||||
|
||||
function renderAssistantContent(text) {
|
||||
if (window.marked && window.DOMPurify) {
|
||||
return window.DOMPurify.sanitize(window.marked.parse(text || ""));
|
||||
}
|
||||
return nl2br(text || "");
|
||||
}
|
||||
|
||||
function scrollChatToBottom() {
|
||||
if (chatScroll) {
|
||||
chatScroll.scrollTop = chatScroll.scrollHeight;
|
||||
@@ -169,7 +182,7 @@
|
||||
bubble.className = "message-bubble";
|
||||
|
||||
var text = document.createElement("p");
|
||||
text.innerHTML = nl2br(content);
|
||||
text.innerHTML = role === "assistant" ? renderAssistantContent(content) : nl2br(content);
|
||||
bubble.appendChild(text);
|
||||
|
||||
article.appendChild(avatar);
|
||||
@@ -271,6 +284,149 @@
|
||||
}
|
||||
}
|
||||
|
||||
function currentConversationId() {
|
||||
return conversationIdInput ? conversationIdInput.value : "";
|
||||
}
|
||||
|
||||
function templateUrl(attributeName, token, value) {
|
||||
if (!summaryPanel) {
|
||||
return "";
|
||||
}
|
||||
return summaryPanel.getAttribute(attributeName).replace(token, value);
|
||||
}
|
||||
|
||||
function renderAttachments(attachments) {
|
||||
if (!attachmentList) {
|
||||
return;
|
||||
}
|
||||
attachmentList.innerHTML = "";
|
||||
if (!attachments.length) {
|
||||
attachmentList.innerHTML = '<div class="panel-empty">暂无附件</div>';
|
||||
return;
|
||||
}
|
||||
attachments.forEach(function (attachment) {
|
||||
var item = document.createElement("div");
|
||||
item.className = "attachment-item";
|
||||
item.setAttribute("data-attachment-id", attachment.id);
|
||||
item.innerHTML =
|
||||
"<div><strong>" +
|
||||
escapeHtml(attachment.original_name) +
|
||||
"</strong><span>v" +
|
||||
attachment.version_no +
|
||||
" · " +
|
||||
attachment.file_size +
|
||||
" bytes · " +
|
||||
escapeHtml(attachment.upload_status) +
|
||||
"</span></div>" +
|
||||
(attachment.is_active ? "<em>active</em>" : "");
|
||||
attachmentList.appendChild(item);
|
||||
});
|
||||
}
|
||||
|
||||
async function refreshAttachments() {
|
||||
var conversationId = currentConversationId();
|
||||
if (!conversationId || !summaryPanel) {
|
||||
return;
|
||||
}
|
||||
var response = await fetch(templateUrl("data-attachment-url-template", "__conversation_id__", conversationId));
|
||||
if (!response.ok) {
|
||||
return;
|
||||
}
|
||||
var payload = await response.json();
|
||||
renderAttachments(payload.attachments || []);
|
||||
}
|
||||
|
||||
async function uploadFiles(files) {
|
||||
var conversationId = currentConversationId();
|
||||
if (!conversationId || !files.length || !summaryPanel) {
|
||||
if (uploadStatus) {
|
||||
uploadStatus.textContent = "请先创建或选择一个对话。";
|
||||
}
|
||||
return;
|
||||
}
|
||||
var data = new FormData();
|
||||
Array.prototype.forEach.call(files, function (file) {
|
||||
data.append("files", file);
|
||||
});
|
||||
var csrf = new FormData(composer).get("csrfmiddlewaretoken");
|
||||
if (uploadStatus) {
|
||||
uploadStatus.textContent = "正在上传 " + files.length + " 个文件...";
|
||||
}
|
||||
try {
|
||||
var response = await fetch(templateUrl("data-attachment-url-template", "__conversation_id__", conversationId), {
|
||||
method: "POST",
|
||||
headers: { "X-CSRFToken": csrf },
|
||||
body: data,
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error("上传失败。");
|
||||
}
|
||||
var payload = await response.json();
|
||||
renderAttachments(payload.attachments || []);
|
||||
if (uploadStatus) {
|
||||
uploadStatus.textContent = "上传完成,可发送自动汇总提示词。";
|
||||
}
|
||||
await refreshAttachments();
|
||||
} catch (error) {
|
||||
if (uploadStatus) {
|
||||
uploadStatus.textContent = "上传失败,请重试。";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function ensureWorkflowCard(batch) {
|
||||
if (!workflowCardList || !batch) {
|
||||
return null;
|
||||
}
|
||||
var empty = workflowCardList.querySelector(".panel-empty");
|
||||
if (empty) {
|
||||
empty.remove();
|
||||
}
|
||||
var card = workflowCardList.querySelector('[data-batch-id="' + batch.batch_id + '"]');
|
||||
if (card) {
|
||||
return card;
|
||||
}
|
||||
card = document.createElement("article");
|
||||
card.className = "workflow-card";
|
||||
card.setAttribute("data-batch-id", batch.batch_id);
|
||||
card.innerHTML =
|
||||
"<header><strong>" +
|
||||
escapeHtml(batch.batch_no || "文件汇总") +
|
||||
'</strong><span class="workflow-status status-running">running</span></header><ol></ol>';
|
||||
workflowCardList.prepend(card);
|
||||
return card;
|
||||
}
|
||||
|
||||
async function refreshWorkflowCard(batchId) {
|
||||
if (!summaryPanel || !batchId) {
|
||||
return;
|
||||
}
|
||||
var response = await fetch(templateUrl("data-status-url-template", "__batch_id__", batchId));
|
||||
if (!response.ok) {
|
||||
return;
|
||||
}
|
||||
var payload = await response.json();
|
||||
var card = ensureWorkflowCard({
|
||||
batch_id: payload.batch.id,
|
||||
batch_no: payload.batch.batch_no,
|
||||
});
|
||||
if (!card) {
|
||||
return;
|
||||
}
|
||||
var status = card.querySelector(".workflow-status");
|
||||
status.textContent = payload.batch.status;
|
||||
status.className = "workflow-status status-" + payload.batch.status;
|
||||
var list = card.querySelector("ol");
|
||||
list.innerHTML = "";
|
||||
(payload.nodes || []).forEach(function (node) {
|
||||
var item = document.createElement("li");
|
||||
item.className = "node-status status-" + node.status;
|
||||
item.setAttribute("data-node-code", node.node_code);
|
||||
item.innerHTML = "<span>" + escapeHtml(node.node_name) + "</span><em>" + node.progress + "%</em>";
|
||||
list.appendChild(item);
|
||||
});
|
||||
}
|
||||
|
||||
async function streamChat(event) {
|
||||
event.preventDefault();
|
||||
if (!composer || !promptInput || !sendButton || !chatStage) {
|
||||
@@ -356,11 +512,14 @@
|
||||
}
|
||||
} else if (eventName === "chunk") {
|
||||
assistantText += payload.delta || "";
|
||||
assistantMessage.text.innerHTML = nl2br(assistantText);
|
||||
assistantMessage.text.innerHTML = renderAssistantContent(assistantText);
|
||||
scrollChatToBottom();
|
||||
} else if (eventName === "error") {
|
||||
assistantText = payload.message || "模型调用失败。";
|
||||
assistantMessage.text.innerHTML = nl2br(assistantText);
|
||||
assistantMessage.text.innerHTML = renderAssistantContent(assistantText);
|
||||
} else if (eventName === "workflow_started") {
|
||||
ensureWorkflowCard(payload);
|
||||
refreshWorkflowCard(payload.batch_id);
|
||||
} else if (eventName === "done") {
|
||||
if (payload.assistant_message_id) {
|
||||
assistantMessage.article.id = "message-" + payload.assistant_message_id;
|
||||
@@ -400,6 +559,28 @@
|
||||
composer.addEventListener("submit", streamChat);
|
||||
}
|
||||
|
||||
if (uploadDropzone && attachmentInput) {
|
||||
uploadDropzone.addEventListener("click", function () {
|
||||
attachmentInput.click();
|
||||
});
|
||||
uploadDropzone.addEventListener("dragover", function (event) {
|
||||
event.preventDefault();
|
||||
uploadDropzone.classList.add("dragging");
|
||||
});
|
||||
uploadDropzone.addEventListener("dragleave", function () {
|
||||
uploadDropzone.classList.remove("dragging");
|
||||
});
|
||||
uploadDropzone.addEventListener("drop", function (event) {
|
||||
event.preventDefault();
|
||||
uploadDropzone.classList.remove("dragging");
|
||||
uploadFiles(event.dataTransfer.files);
|
||||
});
|
||||
attachmentInput.addEventListener("change", function () {
|
||||
uploadFiles(attachmentInput.files);
|
||||
attachmentInput.value = "";
|
||||
});
|
||||
}
|
||||
|
||||
window.addEventListener("resize", syncSidebarState);
|
||||
syncSidebarState();
|
||||
})();
|
||||
|
||||
@@ -164,9 +164,77 @@
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<aside
|
||||
class="summary-panel"
|
||||
id="summaryPanel"
|
||||
data-attachment-url-template="/api/review-agent/conversations/__conversation_id__/attachments/"
|
||||
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" id="workflowCardList">
|
||||
{% for batch in summary_batches %}
|
||||
<article class="workflow-card" data-batch-id="{{ batch.pk }}">
|
||||
<header>
|
||||
<strong>{{ batch.batch_no }}</strong>
|
||||
<span class="workflow-status status-{{ batch.status }}">{{ batch.status }}</span>
|
||||
</header>
|
||||
<ol>
|
||||
{% for node in batch.node_runs.all %}
|
||||
<li class="node-status status-{{ node.status }}" data-node-code="{{ node.node_code }}">
|
||||
<span>{{ node.node_name }}</span>
|
||||
<em>{{ node.progress }}%</em>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ol>
|
||||
</article>
|
||||
{% empty %}
|
||||
<div class="panel-empty">暂无工作流</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
</aside>
|
||||
</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 %}
|
||||
|
||||
22
tests/test_file_summary_frontend.py
Normal file
22
tests/test_file_summary_frontend.py
Normal file
@@ -0,0 +1,22 @@
|
||||
import pytest
|
||||
from django.urls import reverse
|
||||
|
||||
from review_agent.models import Conversation
|
||||
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
def test_workspace_renders_summary_panel(client, django_user_model):
|
||||
user = django_user_model.objects.create_user(username="owner", password="pass")
|
||||
conversation = Conversation.objects.create(user=user, title="会话")
|
||||
client.force_login(user)
|
||||
|
||||
response = client.get(f"{reverse('home')}?conversation={conversation.pk}")
|
||||
|
||||
assert response.status_code == 200
|
||||
content = response.content.decode("utf-8")
|
||||
assert 'id="summaryPanel"' in content
|
||||
assert 'id="uploadDropzone"' in content
|
||||
assert 'id="workflowCardList"' in content
|
||||
assert "自动汇总文件目录与页数" in content
|
||||
Reference in New Issue
Block a user