fix(frontend): 修复会话补充与工作流刷新

This commit is contained in:
2026-06-06 17:56:54 +08:00
parent fa77c68d77
commit 54c37edf19
4 changed files with 132 additions and 10 deletions

View File

@@ -581,18 +581,30 @@ input:focus {
border-radius: 18px;
background: #f8fbff;
line-height: 1.7;
min-width: 0;
max-width: 100%;
overflow-wrap: anywhere;
word-break: break-word;
}
.message-bubble p,
.message-content p {
margin: 0;
line-height: 1.8;
min-width: 0;
max-width: 100%;
overflow-wrap: anywhere;
word-break: break-word;
}
.message-content {
display: grid;
gap: 14px;
line-height: 1.8;
min-width: 0;
max-width: 100%;
overflow-wrap: anywhere;
word-break: break-word;
}
.message-content a {
@@ -963,6 +975,7 @@ input:focus {
border-collapse: collapse;
font-size: 13px;
line-height: 1.6;
table-layout: fixed;
}
.message-bubble th,
@@ -971,6 +984,21 @@ input:focus {
border: 1px solid var(--line);
text-align: left;
vertical-align: top;
overflow-wrap: anywhere;
word-break: break-word;
}
.message-bubble pre {
max-width: 100%;
overflow-x: auto;
white-space: pre-wrap;
overflow-wrap: anywhere;
}
.message-bubble code {
white-space: pre-wrap;
overflow-wrap: anywhere;
word-break: break-word;
}
@media (max-width: 980px) {

View File

@@ -18,6 +18,8 @@
var uploadStatus = document.getElementById("uploadStatus");
var workflowCardList = document.getElementById("workflowCardList");
var nodeAnchors = [];
var workflowPollingTimers = {};
var WORKFLOW_POLL_INTERVAL_MS = 1500;
if (!workspace) {
return;
@@ -236,10 +238,15 @@
}
function renderAssistantContent(text) {
if (window.marked && window.DOMPurify) {
return window.DOMPurify.sanitize(window.marked.parse(text || ""));
try {
if (window.marked && window.DOMPurify) {
return window.DOMPurify.sanitize(window.marked.parse(text || ""));
}
return renderBasicMarkdown(text || "");
} catch (error) {
console.error("Markdown render failed", error);
return nl2br(text || "");
}
return renderBasicMarkdown(text || "");
}
function renderExistingAssistantMessages() {
@@ -313,7 +320,7 @@
return;
}
var encodedTitle = title;
var existing = document.querySelector('.history-item[href*="conversation=' + conversationId + '"]');
var existing = document.querySelector('.history-item[data-conversation-id="' + conversationId + '"]');
var list = document.querySelector(".history-list");
var currentTime = new Date();
var month = String(currentTime.getMonth() + 1).padStart(2, "0");
@@ -347,6 +354,7 @@
var item = document.createElement("a");
item.className = "history-item active";
item.setAttribute("data-conversation-id", conversationId);
item.href = "/?conversation=" + conversationId;
item.innerHTML =
'<span class="history-title">' +
@@ -496,11 +504,20 @@
async function refreshWorkflowCard(batchId) {
if (!summaryPanel || !batchId) {
return;
return "";
}
var response;
try {
response = await fetch(templateUrl("data-status-url-template", "__batch_id__", batchId), {
cache: "no-store",
});
} catch (error) {
console.error("Workflow status refresh failed", { batchId: batchId, error: error });
return "";
}
var response = await fetch(templateUrl("data-status-url-template", "__batch_id__", batchId));
if (!response.ok) {
return;
console.error("Workflow status refresh returned non-OK", { batchId: batchId, status: response.status });
return "";
}
var payload = await response.json();
var card = ensureWorkflowCard({
@@ -508,7 +525,7 @@
batch_no: payload.batch.batch_no,
});
if (!card) {
return;
return payload.batch.status || "";
}
var status = card.querySelector(".workflow-status");
status.textContent = payload.batch.status;
@@ -522,6 +539,50 @@
item.innerHTML = "<span>" + escapeHtml(node.node_name) + "</span><em>" + node.progress + "%</em>";
list.appendChild(item);
});
return payload.batch.status || "";
}
function isWorkflowTerminalStatus(status) {
return status === "success" || status === "failed";
}
function stopWorkflowPolling(batchId) {
if (!workflowPollingTimers[batchId]) {
return;
}
window.clearInterval(workflowPollingTimers[batchId]);
delete workflowPollingTimers[batchId];
}
function startWorkflowPolling(batchId) {
if (!batchId || workflowPollingTimers[batchId]) {
return;
}
workflowPollingTimers[batchId] = window.setInterval(async function () {
var status = await refreshWorkflowCard(batchId);
if (isWorkflowTerminalStatus(status)) {
stopWorkflowPolling(batchId);
}
}, WORKFLOW_POLL_INTERVAL_MS);
refreshWorkflowCard(batchId).then(function (status) {
if (isWorkflowTerminalStatus(status)) {
stopWorkflowPolling(batchId);
}
});
}
function refreshRunningWorkflowCards() {
if (!workflowCardList) {
return;
}
workflowCardList.querySelectorAll(".workflow-card").forEach(function (card) {
var batchId = card.getAttribute("data-batch-id");
var status = card.querySelector(".workflow-status");
var statusText = status ? status.textContent.trim() : "";
if (!isWorkflowTerminalStatus(statusText)) {
startWorkflowPolling(batchId);
}
});
}
async function streamChat(event) {
@@ -597,7 +658,13 @@
return;
}
var payload = JSON.parse(dataText);
var payload;
try {
payload = JSON.parse(dataText);
} catch (error) {
console.error("SSE frame parse failed", { error: error, frame: frame });
return;
}
if (eventName === "meta") {
if (payload.conversation_id) {
conversationIdInput.value = payload.conversation_id;
@@ -616,7 +683,7 @@
assistantMessage.text.innerHTML = renderAssistantContent(assistantText);
} else if (eventName === "workflow_started") {
ensureWorkflowCard(payload);
refreshWorkflowCard(payload.batch_id);
startWorkflowPolling(payload.batch_id);
} else if (eventName === "done") {
if (payload.assistant_message_id) {
assistantMessage.article.id = "message-" + payload.assistant_message_id;
@@ -647,6 +714,7 @@
syncNodeRailVisibility();
bindNodeAnchorClicks();
renderExistingAssistantMessages();
refreshRunningWorkflowCards();
if (chatScroll) {
chatScroll.addEventListener("scroll", setActiveNode, { passive: true });

View File

@@ -74,6 +74,7 @@
{% 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>

View File

@@ -24,6 +24,31 @@ def test_workspace_renders_summary_panel(client, django_user_model):
assert 'id="summaryPanel"' in content
assert 'id="uploadDropzone"' in content
assert 'id="workflowCardList"' in content
assert 'data-conversation-id="' in content
assert 'class="message-content markdown-content"' in content
assert 'class="message-raw"' in content
assert "自动汇总文件目录与页数" in content
def test_frontend_prevents_long_message_overflow():
css = open("static/css/login.css", encoding="utf-8").read()
assert ".message-bubble" in css
assert "overflow-wrap: anywhere" in css
assert "word-break: break-word" in css
def test_frontend_polls_running_workflow_cards():
script = open("static/js/app.js", encoding="utf-8").read()
assert "startWorkflowPolling" in script
assert "setInterval" in script
assert "refreshRunningWorkflowCards" in script
def test_frontend_updates_sidebar_conversation_by_stable_id():
script = open("static/js/app.js", encoding="utf-8").read()
assert "data-conversation-id" in script
assert "setAttribute(\"data-conversation-id\"" in script
assert ".history-item[data-conversation-id=" in script