feat(file-summary): 增加前端汇总面板

This commit is contained in:
2026-06-06 10:25:11 +08:00
parent 61bd31790b
commit a917a18ca1
6 changed files with 529 additions and 7 deletions

View File

@@ -11,6 +11,12 @@
var sendButton = document.getElementById("sendButton");
var conversationIdInput = document.getElementById("conversationIdInput");
var chatStage = document.querySelector(".chat-stage");
var summaryPanel = document.getElementById("summaryPanel");
var uploadDropzone = document.getElementById("uploadDropzone");
var attachmentInput = document.getElementById("attachmentInput");
var attachmentList = document.getElementById("attachmentList");
var uploadStatus = document.getElementById("uploadStatus");
var workflowCardList = document.getElementById("workflowCardList");
var nodeAnchors = [];
if (!workspace) {
@@ -32,7 +38,7 @@
function syncSidebarState() {
if (isMobile()) {
if (workspace.getAttribute("data-sidebar-state") === "collapsed") {
if (workspace.getAttribute("data-sidebar-state") !== "closed") {
workspace.setAttribute("data-sidebar-state", "closed");
}
} else if (workspace.getAttribute("data-sidebar-state") === "closed") {
@@ -147,6 +153,13 @@
return escapeHtml(text).replace(/\n/g, "<br>");
}
function renderAssistantContent(text) {
if (window.marked && window.DOMPurify) {
return window.DOMPurify.sanitize(window.marked.parse(text || ""));
}
return nl2br(text || "");
}
function scrollChatToBottom() {
if (chatScroll) {
chatScroll.scrollTop = chatScroll.scrollHeight;
@@ -169,7 +182,7 @@
bubble.className = "message-bubble";
var text = document.createElement("p");
text.innerHTML = nl2br(content);
text.innerHTML = role === "assistant" ? renderAssistantContent(content) : nl2br(content);
bubble.appendChild(text);
article.appendChild(avatar);
@@ -271,6 +284,149 @@
}
}
function currentConversationId() {
return conversationIdInput ? conversationIdInput.value : "";
}
function templateUrl(attributeName, token, value) {
if (!summaryPanel) {
return "";
}
return summaryPanel.getAttribute(attributeName).replace(token, value);
}
function renderAttachments(attachments) {
if (!attachmentList) {
return;
}
attachmentList.innerHTML = "";
if (!attachments.length) {
attachmentList.innerHTML = '<div class="panel-empty">暂无附件</div>';
return;
}
attachments.forEach(function (attachment) {
var item = document.createElement("div");
item.className = "attachment-item";
item.setAttribute("data-attachment-id", attachment.id);
item.innerHTML =
"<div><strong>" +
escapeHtml(attachment.original_name) +
"</strong><span>v" +
attachment.version_no +
" · " +
attachment.file_size +
" bytes · " +
escapeHtml(attachment.upload_status) +
"</span></div>" +
(attachment.is_active ? "<em>active</em>" : "");
attachmentList.appendChild(item);
});
}
async function refreshAttachments() {
var conversationId = currentConversationId();
if (!conversationId || !summaryPanel) {
return;
}
var response = await fetch(templateUrl("data-attachment-url-template", "__conversation_id__", conversationId));
if (!response.ok) {
return;
}
var payload = await response.json();
renderAttachments(payload.attachments || []);
}
async function uploadFiles(files) {
var conversationId = currentConversationId();
if (!conversationId || !files.length || !summaryPanel) {
if (uploadStatus) {
uploadStatus.textContent = "请先创建或选择一个对话。";
}
return;
}
var data = new FormData();
Array.prototype.forEach.call(files, function (file) {
data.append("files", file);
});
var csrf = new FormData(composer).get("csrfmiddlewaretoken");
if (uploadStatus) {
uploadStatus.textContent = "正在上传 " + files.length + " 个文件...";
}
try {
var response = await fetch(templateUrl("data-attachment-url-template", "__conversation_id__", conversationId), {
method: "POST",
headers: { "X-CSRFToken": csrf },
body: data,
});
if (!response.ok) {
throw new Error("上传失败。");
}
var payload = await response.json();
renderAttachments(payload.attachments || []);
if (uploadStatus) {
uploadStatus.textContent = "上传完成,可发送自动汇总提示词。";
}
await refreshAttachments();
} catch (error) {
if (uploadStatus) {
uploadStatus.textContent = "上传失败,请重试。";
}
}
}
function ensureWorkflowCard(batch) {
if (!workflowCardList || !batch) {
return null;
}
var empty = workflowCardList.querySelector(".panel-empty");
if (empty) {
empty.remove();
}
var card = workflowCardList.querySelector('[data-batch-id="' + batch.batch_id + '"]');
if (card) {
return card;
}
card = document.createElement("article");
card.className = "workflow-card";
card.setAttribute("data-batch-id", batch.batch_id);
card.innerHTML =
"<header><strong>" +
escapeHtml(batch.batch_no || "文件汇总") +
'</strong><span class="workflow-status status-running">running</span></header><ol></ol>';
workflowCardList.prepend(card);
return card;
}
async function refreshWorkflowCard(batchId) {
if (!summaryPanel || !batchId) {
return;
}
var response = await fetch(templateUrl("data-status-url-template", "__batch_id__", batchId));
if (!response.ok) {
return;
}
var payload = await response.json();
var card = ensureWorkflowCard({
batch_id: payload.batch.id,
batch_no: payload.batch.batch_no,
});
if (!card) {
return;
}
var status = card.querySelector(".workflow-status");
status.textContent = payload.batch.status;
status.className = "workflow-status status-" + payload.batch.status;
var list = card.querySelector("ol");
list.innerHTML = "";
(payload.nodes || []).forEach(function (node) {
var item = document.createElement("li");
item.className = "node-status status-" + node.status;
item.setAttribute("data-node-code", node.node_code);
item.innerHTML = "<span>" + escapeHtml(node.node_name) + "</span><em>" + node.progress + "%</em>";
list.appendChild(item);
});
}
async function streamChat(event) {
event.preventDefault();
if (!composer || !promptInput || !sendButton || !chatStage) {
@@ -356,11 +512,14 @@
}
} else if (eventName === "chunk") {
assistantText += payload.delta || "";
assistantMessage.text.innerHTML = nl2br(assistantText);
assistantMessage.text.innerHTML = renderAssistantContent(assistantText);
scrollChatToBottom();
} else if (eventName === "error") {
assistantText = payload.message || "模型调用失败。";
assistantMessage.text.innerHTML = nl2br(assistantText);
assistantMessage.text.innerHTML = renderAssistantContent(assistantText);
} else if (eventName === "workflow_started") {
ensureWorkflowCard(payload);
refreshWorkflowCard(payload.batch_id);
} else if (eventName === "done") {
if (payload.assistant_message_id) {
assistantMessage.article.id = "message-" + payload.assistant_message_id;
@@ -400,6 +559,28 @@
composer.addEventListener("submit", streamChat);
}
if (uploadDropzone && attachmentInput) {
uploadDropzone.addEventListener("click", function () {
attachmentInput.click();
});
uploadDropzone.addEventListener("dragover", function (event) {
event.preventDefault();
uploadDropzone.classList.add("dragging");
});
uploadDropzone.addEventListener("dragleave", function () {
uploadDropzone.classList.remove("dragging");
});
uploadDropzone.addEventListener("drop", function (event) {
event.preventDefault();
uploadDropzone.classList.remove("dragging");
uploadFiles(event.dataTransfer.files);
});
attachmentInput.addEventListener("change", function () {
uploadFiles(attachmentInput.files);
attachmentInput.value = "";
});
}
window.addEventListener("resize", syncSidebarState);
syncSidebarState();
})();