fix(frontend): 修复会话补充与工作流刷新
This commit is contained in:
@@ -581,18 +581,30 @@ input:focus {
|
|||||||
border-radius: 18px;
|
border-radius: 18px;
|
||||||
background: #f8fbff;
|
background: #f8fbff;
|
||||||
line-height: 1.7;
|
line-height: 1.7;
|
||||||
|
min-width: 0;
|
||||||
|
max-width: 100%;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-bubble p,
|
.message-bubble p,
|
||||||
.message-content p {
|
.message-content p {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
line-height: 1.8;
|
line-height: 1.8;
|
||||||
|
min-width: 0;
|
||||||
|
max-width: 100%;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-content {
|
.message-content {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 14px;
|
gap: 14px;
|
||||||
line-height: 1.8;
|
line-height: 1.8;
|
||||||
|
min-width: 0;
|
||||||
|
max-width: 100%;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-content a {
|
.message-content a {
|
||||||
@@ -963,6 +975,7 @@ input:focus {
|
|||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
|
table-layout: fixed;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-bubble th,
|
.message-bubble th,
|
||||||
@@ -971,6 +984,21 @@ input:focus {
|
|||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
text-align: left;
|
text-align: left;
|
||||||
vertical-align: top;
|
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) {
|
@media (max-width: 980px) {
|
||||||
|
|||||||
@@ -18,6 +18,8 @@
|
|||||||
var uploadStatus = document.getElementById("uploadStatus");
|
var uploadStatus = document.getElementById("uploadStatus");
|
||||||
var workflowCardList = document.getElementById("workflowCardList");
|
var workflowCardList = document.getElementById("workflowCardList");
|
||||||
var nodeAnchors = [];
|
var nodeAnchors = [];
|
||||||
|
var workflowPollingTimers = {};
|
||||||
|
var WORKFLOW_POLL_INTERVAL_MS = 1500;
|
||||||
|
|
||||||
if (!workspace) {
|
if (!workspace) {
|
||||||
return;
|
return;
|
||||||
@@ -236,10 +238,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderAssistantContent(text) {
|
function renderAssistantContent(text) {
|
||||||
if (window.marked && window.DOMPurify) {
|
try {
|
||||||
return window.DOMPurify.sanitize(window.marked.parse(text || ""));
|
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() {
|
function renderExistingAssistantMessages() {
|
||||||
@@ -313,7 +320,7 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var encodedTitle = title;
|
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 list = document.querySelector(".history-list");
|
||||||
var currentTime = new Date();
|
var currentTime = new Date();
|
||||||
var month = String(currentTime.getMonth() + 1).padStart(2, "0");
|
var month = String(currentTime.getMonth() + 1).padStart(2, "0");
|
||||||
@@ -347,6 +354,7 @@
|
|||||||
|
|
||||||
var item = document.createElement("a");
|
var item = document.createElement("a");
|
||||||
item.className = "history-item active";
|
item.className = "history-item active";
|
||||||
|
item.setAttribute("data-conversation-id", conversationId);
|
||||||
item.href = "/?conversation=" + conversationId;
|
item.href = "/?conversation=" + conversationId;
|
||||||
item.innerHTML =
|
item.innerHTML =
|
||||||
'<span class="history-title">' +
|
'<span class="history-title">' +
|
||||||
@@ -496,11 +504,20 @@
|
|||||||
|
|
||||||
async function refreshWorkflowCard(batchId) {
|
async function refreshWorkflowCard(batchId) {
|
||||||
if (!summaryPanel || !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) {
|
if (!response.ok) {
|
||||||
return;
|
console.error("Workflow status refresh returned non-OK", { batchId: batchId, status: response.status });
|
||||||
|
return "";
|
||||||
}
|
}
|
||||||
var payload = await response.json();
|
var payload = await response.json();
|
||||||
var card = ensureWorkflowCard({
|
var card = ensureWorkflowCard({
|
||||||
@@ -508,7 +525,7 @@
|
|||||||
batch_no: payload.batch.batch_no,
|
batch_no: payload.batch.batch_no,
|
||||||
});
|
});
|
||||||
if (!card) {
|
if (!card) {
|
||||||
return;
|
return payload.batch.status || "";
|
||||||
}
|
}
|
||||||
var status = card.querySelector(".workflow-status");
|
var status = card.querySelector(".workflow-status");
|
||||||
status.textContent = payload.batch.status;
|
status.textContent = payload.batch.status;
|
||||||
@@ -522,6 +539,50 @@
|
|||||||
item.innerHTML = "<span>" + escapeHtml(node.node_name) + "</span><em>" + node.progress + "%</em>";
|
item.innerHTML = "<span>" + escapeHtml(node.node_name) + "</span><em>" + node.progress + "%</em>";
|
||||||
list.appendChild(item);
|
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) {
|
async function streamChat(event) {
|
||||||
@@ -597,7 +658,13 @@
|
|||||||
return;
|
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 (eventName === "meta") {
|
||||||
if (payload.conversation_id) {
|
if (payload.conversation_id) {
|
||||||
conversationIdInput.value = payload.conversation_id;
|
conversationIdInput.value = payload.conversation_id;
|
||||||
@@ -616,7 +683,7 @@
|
|||||||
assistantMessage.text.innerHTML = renderAssistantContent(assistantText);
|
assistantMessage.text.innerHTML = renderAssistantContent(assistantText);
|
||||||
} else if (eventName === "workflow_started") {
|
} else if (eventName === "workflow_started") {
|
||||||
ensureWorkflowCard(payload);
|
ensureWorkflowCard(payload);
|
||||||
refreshWorkflowCard(payload.batch_id);
|
startWorkflowPolling(payload.batch_id);
|
||||||
} else if (eventName === "done") {
|
} else if (eventName === "done") {
|
||||||
if (payload.assistant_message_id) {
|
if (payload.assistant_message_id) {
|
||||||
assistantMessage.article.id = "message-" + payload.assistant_message_id;
|
assistantMessage.article.id = "message-" + payload.assistant_message_id;
|
||||||
@@ -647,6 +714,7 @@
|
|||||||
syncNodeRailVisibility();
|
syncNodeRailVisibility();
|
||||||
bindNodeAnchorClicks();
|
bindNodeAnchorClicks();
|
||||||
renderExistingAssistantMessages();
|
renderExistingAssistantMessages();
|
||||||
|
refreshRunningWorkflowCards();
|
||||||
|
|
||||||
if (chatScroll) {
|
if (chatScroll) {
|
||||||
chatScroll.addEventListener("scroll", setActiveNode, { passive: true });
|
chatScroll.addEventListener("scroll", setActiveNode, { passive: true });
|
||||||
|
|||||||
@@ -74,6 +74,7 @@
|
|||||||
{% for conversation in conversations %}
|
{% for conversation in conversations %}
|
||||||
<a
|
<a
|
||||||
class="history-item{% if current_conversation and current_conversation.pk == conversation.pk %} active{% endif %}"
|
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 %}"
|
href="/?conversation={{ conversation.pk }}{% if search_query %}&q={{ search_query|urlencode }}{% endif %}"
|
||||||
>
|
>
|
||||||
<span class="history-title">{{ conversation.title|default:"新对话" }}</span>
|
<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="summaryPanel"' in content
|
||||||
assert 'id="uploadDropzone"' in content
|
assert 'id="uploadDropzone"' in content
|
||||||
assert 'id="workflowCardList"' in content
|
assert 'id="workflowCardList"' in content
|
||||||
|
assert 'data-conversation-id="' in content
|
||||||
assert 'class="message-content markdown-content"' in content
|
assert 'class="message-content markdown-content"' in content
|
||||||
assert 'class="message-raw"' in content
|
assert 'class="message-raw"' in content
|
||||||
assert "自动汇总文件目录与页数" 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