fix(frontend): 调整审核页布局与报告渲染
This commit is contained in:
@@ -125,10 +125,20 @@ input:focus {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.app-shell {
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: 60px minmax(0, 1fr);
|
||||||
|
height: 100vh;
|
||||||
|
min-height: 0;
|
||||||
|
background: var(--bg);
|
||||||
|
}
|
||||||
|
|
||||||
.workspace {
|
.workspace {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 296px minmax(0, 1fr) 340px;
|
grid-template-columns: 296px minmax(0, 1fr) 340px;
|
||||||
min-height: 100vh;
|
min-height: 0;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar {
|
.sidebar {
|
||||||
@@ -136,6 +146,8 @@ input:focus {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 24px;
|
gap: 24px;
|
||||||
padding: 18px;
|
padding: 18px;
|
||||||
|
min-height: 0;
|
||||||
|
overflow-y: auto;
|
||||||
background: linear-gradient(180deg, var(--sidebar) 0%, var(--sidebar-strong) 100%);
|
background: linear-gradient(180deg, var(--sidebar) 0%, var(--sidebar-strong) 100%);
|
||||||
border-right: 1px solid var(--line);
|
border-right: 1px solid var(--line);
|
||||||
transition: width 180ms ease, padding 180ms ease, transform 180ms ease;
|
transition: width 180ms ease, padding 180ms ease, transform 180ms ease;
|
||||||
@@ -146,6 +158,12 @@ input:focus {
|
|||||||
gap: 14px;
|
gap: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sidebar-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
.brand {
|
.brand {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -310,8 +328,9 @@ input:focus {
|
|||||||
|
|
||||||
.chat-shell {
|
.chat-shell {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-rows: auto minmax(0, 1fr);
|
grid-template-rows: minmax(0, 1fr);
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
min-height: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -322,6 +341,8 @@ input:focus {
|
|||||||
gap: 16px;
|
gap: 16px;
|
||||||
padding: 0 24px;
|
padding: 0 24px;
|
||||||
min-height: 60px;
|
min-height: 60px;
|
||||||
|
position: relative;
|
||||||
|
z-index: 30;
|
||||||
border-bottom: 1px solid var(--line);
|
border-bottom: 1px solid var(--line);
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
}
|
}
|
||||||
@@ -470,7 +491,7 @@ input:focus {
|
|||||||
display: grid;
|
display: grid;
|
||||||
grid-template-rows: minmax(0, 1fr) auto;
|
grid-template-rows: minmax(0, 1fr) auto;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
height: calc(100vh - 60px);
|
height: 100%;
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
@@ -562,8 +583,26 @@ input:focus {
|
|||||||
line-height: 1.7;
|
line-height: 1.7;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-bubble p {
|
.message-bubble p,
|
||||||
|
.message-content p {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
line-height: 1.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-content {
|
||||||
|
display: grid;
|
||||||
|
gap: 14px;
|
||||||
|
line-height: 1.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-content a {
|
||||||
|
color: var(--accent);
|
||||||
|
font-weight: 700;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-content a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-bubble.streaming {
|
.message-bubble.streaming {
|
||||||
@@ -737,7 +776,7 @@ input:focus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.workspace[data-sidebar-state="collapsed"] {
|
.workspace[data-sidebar-state="collapsed"] {
|
||||||
grid-template-columns: 88px minmax(0, 1fr);
|
grid-template-columns: 88px minmax(0, 1fr) 340px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.workspace[data-sidebar-state="collapsed"] .brand-text,
|
.workspace[data-sidebar-state="collapsed"] .brand-text,
|
||||||
@@ -760,12 +799,20 @@ input:focus {
|
|||||||
padding-right: 12px;
|
padding-right: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.workspace[data-sidebar-state="collapsed"] .sidebar-header {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace[data-sidebar-state="collapsed"] .brand {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.summary-panel {
|
.summary-panel {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-rows: auto auto minmax(0, 1fr);
|
grid-template-rows: auto auto minmax(0, 1fr);
|
||||||
gap: 14px;
|
gap: 14px;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
max-height: 100vh;
|
max-height: 100%;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
border-left: 1px solid var(--line);
|
border-left: 1px solid var(--line);
|
||||||
@@ -915,6 +962,7 @@ input:focus {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
|
line-height: 1.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-bubble th,
|
.message-bubble th,
|
||||||
@@ -926,15 +974,26 @@ input:focus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 980px) {
|
@media (max-width: 980px) {
|
||||||
|
.app-body {
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-shell {
|
||||||
|
grid-template-rows: 60px auto;
|
||||||
|
height: auto;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
.workspace {
|
.workspace {
|
||||||
grid-template-columns: minmax(0, 1fr);
|
grid-template-columns: minmax(0, 1fr);
|
||||||
min-height: 100vh;
|
height: auto;
|
||||||
overflow: auto;
|
min-height: 0;
|
||||||
|
overflow: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar {
|
.sidebar {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0 auto 0 0;
|
inset: 60px auto 0 0;
|
||||||
width: 280px;
|
width: 280px;
|
||||||
z-index: 20;
|
z-index: 20;
|
||||||
box-shadow: var(--shadow);
|
box-shadow: var(--shadow);
|
||||||
@@ -953,10 +1012,6 @@ input:focus {
|
|||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-toggle {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.topbar,
|
.topbar,
|
||||||
.chat-scroll,
|
.chat-scroll,
|
||||||
.composer-wrap {
|
.composer-wrap {
|
||||||
@@ -965,16 +1020,22 @@ input:focus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.topbar {
|
.topbar {
|
||||||
align-items: flex-start;
|
align-items: center;
|
||||||
flex-direction: column;
|
flex-direction: row;
|
||||||
min-height: auto;
|
min-height: 60px;
|
||||||
padding-top: 12px;
|
padding-top: 0;
|
||||||
padding-bottom: 0;
|
padding-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.topbar-left {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
.topbar-right {
|
.topbar-right {
|
||||||
width: 100%;
|
flex: 0 0 auto;
|
||||||
justify-content: space-between;
|
width: auto;
|
||||||
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
.conversation-header {
|
.conversation-header {
|
||||||
@@ -982,7 +1043,7 @@ input:focus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.chat-stage {
|
.chat-stage {
|
||||||
min-height: calc(100vh - 88px);
|
min-height: calc(100vh - 60px);
|
||||||
height: auto;
|
height: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1050,7 +1111,8 @@ input:focus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.chat-stage {
|
.chat-stage {
|
||||||
height: calc(100vh - 126px);
|
min-height: calc(100vh - 60px);
|
||||||
|
height: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-scroll {
|
.chat-scroll {
|
||||||
|
|||||||
102
static/js/app.js
102
static/js/app.js
@@ -153,11 +153,105 @@
|
|||||||
return escapeHtml(text).replace(/\n/g, "<br>");
|
return escapeHtml(text).replace(/\n/g, "<br>");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderInlineMarkdown(text) {
|
||||||
|
return escapeHtml(text || "").replace(/\[([^\]]+)\]\(([^)]+)\)/g, function (_match, label, href) {
|
||||||
|
var safeHref = escapeHtml(href);
|
||||||
|
var safeLabel = escapeHtml(label);
|
||||||
|
if (!/^\/[^/\\]/.test(href) && !/^https?:\/\//.test(href)) {
|
||||||
|
return safeLabel;
|
||||||
|
}
|
||||||
|
return '<a href="' + safeHref + '" target="_blank" rel="noopener">' + safeLabel + "</a>";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderMarkdownTable(lines, startIndex) {
|
||||||
|
var header = lines[startIndex].trim();
|
||||||
|
var separator = lines[startIndex + 1] ? lines[startIndex + 1].trim() : "";
|
||||||
|
if (header.charAt(0) !== "|" || separator.indexOf("---") === -1) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function cells(line) {
|
||||||
|
return line
|
||||||
|
.trim()
|
||||||
|
.replace(/^\|/, "")
|
||||||
|
.replace(/\|$/, "")
|
||||||
|
.split("|")
|
||||||
|
.map(function (cell) {
|
||||||
|
return cell.trim();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var html = "<table><thead><tr>";
|
||||||
|
cells(header).forEach(function (cell) {
|
||||||
|
html += "<th>" + renderInlineMarkdown(cell) + "</th>";
|
||||||
|
});
|
||||||
|
html += "</tr></thead><tbody>";
|
||||||
|
|
||||||
|
var index = startIndex + 2;
|
||||||
|
while (index < lines.length && lines[index].trim().charAt(0) === "|") {
|
||||||
|
html += "<tr>";
|
||||||
|
cells(lines[index]).forEach(function (cell) {
|
||||||
|
html += "<td>" + renderInlineMarkdown(cell || "-") + "</td>";
|
||||||
|
});
|
||||||
|
html += "</tr>";
|
||||||
|
index += 1;
|
||||||
|
}
|
||||||
|
html += "</tbody></table>";
|
||||||
|
return { html: html, nextIndex: index };
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderBasicMarkdown(text) {
|
||||||
|
var lines = (text || "").split(/\r?\n/);
|
||||||
|
var html = "";
|
||||||
|
var paragraph = [];
|
||||||
|
var index = 0;
|
||||||
|
|
||||||
|
function flushParagraph() {
|
||||||
|
if (!paragraph.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
html += "<p>" + renderInlineMarkdown(paragraph.join("\n")).replace(/\n/g, "<br>") + "</p>";
|
||||||
|
paragraph = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
while (index < lines.length) {
|
||||||
|
var line = lines[index];
|
||||||
|
var table = renderMarkdownTable(lines, index);
|
||||||
|
if (table) {
|
||||||
|
flushParagraph();
|
||||||
|
html += table.html;
|
||||||
|
index = table.nextIndex;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!line.trim()) {
|
||||||
|
flushParagraph();
|
||||||
|
} else {
|
||||||
|
paragraph.push(line);
|
||||||
|
}
|
||||||
|
index += 1;
|
||||||
|
}
|
||||||
|
flushParagraph();
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
function renderAssistantContent(text) {
|
function renderAssistantContent(text) {
|
||||||
if (window.marked && window.DOMPurify) {
|
if (window.marked && window.DOMPurify) {
|
||||||
return window.DOMPurify.sanitize(window.marked.parse(text || ""));
|
return window.DOMPurify.sanitize(window.marked.parse(text || ""));
|
||||||
}
|
}
|
||||||
return nl2br(text || "");
|
return renderBasicMarkdown(text || "");
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderExistingAssistantMessages() {
|
||||||
|
document.querySelectorAll(".message.assistant .message-bubble").forEach(function (bubble) {
|
||||||
|
var target = bubble.querySelector(".markdown-content");
|
||||||
|
var raw = bubble.querySelector(".message-raw");
|
||||||
|
if (!target || !raw || target.dataset.rendered === "true") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
target.innerHTML = renderAssistantContent(raw.content ? raw.content.textContent : raw.textContent);
|
||||||
|
target.dataset.rendered = "true";
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function scrollChatToBottom() {
|
function scrollChatToBottom() {
|
||||||
@@ -181,7 +275,10 @@
|
|||||||
var bubble = document.createElement("div");
|
var bubble = document.createElement("div");
|
||||||
bubble.className = "message-bubble";
|
bubble.className = "message-bubble";
|
||||||
|
|
||||||
var text = document.createElement("p");
|
var text = document.createElement(role === "assistant" ? "div" : "p");
|
||||||
|
if (role === "assistant") {
|
||||||
|
text.className = "message-content markdown-content";
|
||||||
|
}
|
||||||
text.innerHTML = role === "assistant" ? renderAssistantContent(content) : nl2br(content);
|
text.innerHTML = role === "assistant" ? renderAssistantContent(content) : nl2br(content);
|
||||||
bubble.appendChild(text);
|
bubble.appendChild(text);
|
||||||
|
|
||||||
@@ -549,6 +646,7 @@
|
|||||||
|
|
||||||
syncNodeRailVisibility();
|
syncNodeRailVisibility();
|
||||||
bindNodeAnchorClicks();
|
bindNodeAnchorClicks();
|
||||||
|
renderExistingAssistantMessages();
|
||||||
|
|
||||||
if (chatScroll) {
|
if (chatScroll) {
|
||||||
chatScroll.addEventListener("scroll", setActiveNode, { passive: true });
|
chatScroll.addEventListener("scroll", setActiveNode, { passive: true });
|
||||||
|
|||||||
@@ -5,18 +5,56 @@
|
|||||||
{% block body_class %}app-body{% endblock %}
|
{% block body_class %}app-body{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<main class="workspace" data-sidebar-state="open">
|
<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">
|
<aside class="sidebar" id="sidebar">
|
||||||
<div class="sidebar-top">
|
<div class="sidebar-top">
|
||||||
<button class="icon-button sidebar-toggle" type="button" id="sidebarToggle" aria-label="折叠侧边栏">
|
<div class="sidebar-header">
|
||||||
<span></span>
|
<button class="icon-button sidebar-toggle" type="button" id="sidebarToggle" aria-label="折叠侧边栏">
|
||||||
<span></span>
|
<span></span>
|
||||||
</button>
|
<span></span>
|
||||||
<div class="brand">
|
</button>
|
||||||
<span class="brand-mark">审</span>
|
<div class="brand">
|
||||||
<div class="brand-copy">
|
<span class="brand-mark">审</span>
|
||||||
<strong class="brand-text">审核智能体</strong>
|
<div class="brand-copy">
|
||||||
<span class="brand-subtitle">临床注册文件审核工作台</span>
|
<strong class="brand-text">审核智能体</strong>
|
||||||
|
<span class="brand-subtitle">临床注册文件审核工作台</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<form method="post">
|
<form method="post">
|
||||||
@@ -53,45 +91,6 @@
|
|||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<section class="chat-shell">
|
<section class="chat-shell">
|
||||||
<header class="topbar">
|
|
||||||
<div class="topbar-left">
|
|
||||||
<button class="icon-button mobile-toggle" type="button" id="mobileSidebarToggle" aria-label="展开侧边栏">
|
|
||||||
<span></span>
|
|
||||||
<span></span>
|
|
||||||
</button>
|
|
||||||
<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="chat-stage" data-stream-url="{% url 'chat_stream' %}">
|
<section class="chat-stage" data-stream-url="{% url 'chat_stream' %}">
|
||||||
<div class="chat-scroll-wrap">
|
<div class="chat-scroll-wrap">
|
||||||
<div class="chat-scroll" id="chatScroll">
|
<div class="chat-scroll" id="chatScroll">
|
||||||
@@ -114,7 +113,12 @@
|
|||||||
{% if message.role == "assistant" %}AI{% else %}{{ request.user.username|slice:":1"|upper }}{% endif %}
|
{% if message.role == "assistant" %}AI{% else %}{{ request.user.username|slice:":1"|upper }}{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="message-bubble">
|
<div class="message-bubble">
|
||||||
<p>{{ message.content|linebreaksbr }}</p>
|
{% 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>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@@ -230,6 +234,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</aside>
|
</aside>
|
||||||
|
</section>
|
||||||
</main>
|
</main>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from pathlib import Path
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from review_agent.models import Conversation
|
from review_agent.models import Conversation, Message
|
||||||
|
|
||||||
|
|
||||||
pytestmark = pytest.mark.django_db
|
pytestmark = pytest.mark.django_db
|
||||||
@@ -26,7 +26,18 @@ def test_file_summary_panel_desktop_and_mobile_with_playwright(live_server, djan
|
|||||||
pytest.skip("No Chrome or Edge executable available for Playwright E2E.")
|
pytest.skip("No Chrome or Edge executable available for Playwright E2E.")
|
||||||
|
|
||||||
user = django_user_model.objects.create_user(username="e2e_user", password="e2e-pass-123")
|
user = django_user_model.objects.create_user(username="e2e_user", password="e2e-pass-123")
|
||||||
Conversation.objects.create(user=user, title="E2E 会话")
|
conversation = Conversation.objects.create(user=user, title="E2E 会话")
|
||||||
|
Message.objects.create(
|
||||||
|
conversation=conversation,
|
||||||
|
role=Message.Role.ASSISTANT,
|
||||||
|
content=(
|
||||||
|
"文件目录与页数汇总已完成。\n\n"
|
||||||
|
"| 序号 | 文件名 | 页数 | 状态 |\n"
|
||||||
|
"| --- | --- | --- | --- |\n"
|
||||||
|
"| 1 | a.pdf | 4 | success |\n\n"
|
||||||
|
"[下载 Markdown 报告](/api/review-agent/file-summary/exports/1/download/)"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
with playwright_api.sync_playwright() as p:
|
with playwright_api.sync_playwright() as p:
|
||||||
browser = p.chromium.launch(headless=True, executable_path=executable_path)
|
browser = p.chromium.launch(headless=True, executable_path=executable_path)
|
||||||
@@ -40,6 +51,8 @@ def test_file_summary_panel_desktop_and_mobile_with_playwright(live_server, djan
|
|||||||
playwright_api.expect(page.locator("#summaryPanel")).to_be_visible()
|
playwright_api.expect(page.locator("#summaryPanel")).to_be_visible()
|
||||||
playwright_api.expect(page.locator("#uploadDropzone")).to_be_visible()
|
playwright_api.expect(page.locator("#uploadDropzone")).to_be_visible()
|
||||||
playwright_api.expect(page.locator("#workflowCardList")).to_be_visible()
|
playwright_api.expect(page.locator("#workflowCardList")).to_be_visible()
|
||||||
|
playwright_api.expect(page.locator(".message.assistant table")).to_be_visible()
|
||||||
|
playwright_api.expect(page.locator('.message.assistant a[href="/api/review-agent/file-summary/exports/1/download/"]')).to_be_visible()
|
||||||
|
|
||||||
page.set_viewport_size({"width": 390, "height": 844})
|
page.set_viewport_size({"width": 390, "height": 844})
|
||||||
playwright_api.expect(page.locator("#summaryPanel")).to_be_visible()
|
playwright_api.expect(page.locator("#summaryPanel")).to_be_visible()
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import pytest
|
import pytest
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
from review_agent.models import Conversation
|
from review_agent.models import Conversation, Message
|
||||||
|
|
||||||
|
|
||||||
pytestmark = pytest.mark.django_db
|
pytestmark = pytest.mark.django_db
|
||||||
@@ -10,6 +10,11 @@ pytestmark = pytest.mark.django_db
|
|||||||
def test_workspace_renders_summary_panel(client, django_user_model):
|
def test_workspace_renders_summary_panel(client, django_user_model):
|
||||||
user = django_user_model.objects.create_user(username="owner", password="pass")
|
user = django_user_model.objects.create_user(username="owner", password="pass")
|
||||||
conversation = Conversation.objects.create(user=user, title="会话")
|
conversation = Conversation.objects.create(user=user, title="会话")
|
||||||
|
Message.objects.create(
|
||||||
|
conversation=conversation,
|
||||||
|
role=Message.Role.ASSISTANT,
|
||||||
|
content="| 序号 | 文件名 |\n| --- | --- |\n| 1 | a.pdf |\n\n[下载](/api/review-agent/file-summary/exports/1/download/)",
|
||||||
|
)
|
||||||
client.force_login(user)
|
client.force_login(user)
|
||||||
|
|
||||||
response = client.get(f"{reverse('home')}?conversation={conversation.pk}")
|
response = client.get(f"{reverse('home')}?conversation={conversation.pk}")
|
||||||
@@ -19,4 +24,6 @@ def test_workspace_renders_summary_panel(client, django_user_model):
|
|||||||
assert 'id="summaryPanel"' in content
|
assert 'id="summaryPanel"' in content
|
||||||
assert 'id="uploadDropzone"' in content
|
assert 'id="uploadDropzone"' in content
|
||||||
assert 'id="workflowCardList"' in content
|
assert 'id="workflowCardList"' in content
|
||||||
|
assert 'class="message-content markdown-content"' in content
|
||||||
|
assert 'class="message-raw"' in content
|
||||||
assert "自动汇总文件目录与页数" in content
|
assert "自动汇总文件目录与页数" in content
|
||||||
|
|||||||
Reference in New Issue
Block a user