diff --git a/static/css/login.css b/static/css/login.css index ea762c6..48a725a 100644 --- a/static/css/login.css +++ b/static/css/login.css @@ -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) { diff --git a/static/js/app.js b/static/js/app.js index a87c4b2..79d9cf6 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -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 = '' + @@ -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 = "" + escapeHtml(node.node_name) + "" + node.progress + "%"; 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 }); diff --git a/templates/home.html b/templates/home.html index e88a4d7..9c6d482 100644 --- a/templates/home.html +++ b/templates/home.html @@ -74,6 +74,7 @@ {% for conversation in conversations %} {{ conversation.title|default:"新对话" }} diff --git a/tests/test_file_summary_frontend.py b/tests/test_file_summary_frontend.py index a638aa8..4f46de1 100644 --- a/tests/test_file_summary_frontend.py +++ b/tests/test_file_summary_frontend.py @@ -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