fix(frontend): 修复会话补充与工作流刷新
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user