feat(file-summary): 增加前端汇总面板
This commit is contained in:
189
static/js/app.js
189
static/js/app.js
@@ -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();
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user