(function () { var workspace = document.querySelector(".workspace"); var sidebarToggle = document.getElementById("sidebarToggle"); var mobileSidebarToggle = document.getElementById("mobileSidebarToggle"); var userMenu = document.getElementById("userMenu"); var userMenuTrigger = document.getElementById("userMenuTrigger"); var chatScroll = document.getElementById("chatScroll"); var nodeRail = document.getElementById("nodeRail"); var composer = document.getElementById("chatComposer"); var promptInput = document.getElementById("prompt"); 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 = []; var workflowPollingTimers = {}; var WORKFLOW_POLL_INTERVAL_MS = 1500; var latestMessageId = 0; if (!workspace) { return; } function isMobile() { return window.matchMedia("(max-width: 980px)").matches; } function toggleSidebar() { var state = workspace.getAttribute("data-sidebar-state"); if (isMobile()) { workspace.setAttribute("data-sidebar-state", state === "open" ? "closed" : "open"); return; } workspace.setAttribute("data-sidebar-state", state === "collapsed" ? "open" : "collapsed"); } function syncSidebarState() { if (isMobile()) { if (workspace.getAttribute("data-sidebar-state") !== "closed") { workspace.setAttribute("data-sidebar-state", "closed"); } } else if (workspace.getAttribute("data-sidebar-state") === "closed") { workspace.setAttribute("data-sidebar-state", "open"); } } function refreshNodeAnchors() { nodeAnchors = Array.prototype.slice.call(document.querySelectorAll(".node-anchor")); } function syncLatestMessageIdFromDom() { document.querySelectorAll(".message[data-message-id]").forEach(function (message) { var id = parseInt(message.getAttribute("data-message-id"), 10); if (!Number.isNaN(id)) { latestMessageId = Math.max(latestMessageId, id); } }); } if (sidebarToggle) { sidebarToggle.addEventListener("click", toggleSidebar); } if (mobileSidebarToggle) { mobileSidebarToggle.addEventListener("click", toggleSidebar); } if (userMenu && userMenuTrigger) { userMenuTrigger.addEventListener("click", function () { var isOpen = userMenu.classList.toggle("open"); userMenuTrigger.setAttribute("aria-expanded", isOpen ? "true" : "false"); }); document.addEventListener("click", function (event) { if (!userMenu.contains(event.target)) { userMenu.classList.remove("open"); userMenuTrigger.setAttribute("aria-expanded", "false"); } }); } function setActiveNode() { if (!chatScroll || !nodeAnchors.length) { return; } var activeTarget = nodeAnchors[0].getAttribute("data-target"); var scrollTop = chatScroll.scrollTop; var threshold = 80; nodeAnchors.forEach(function (anchor) { var targetId = anchor.getAttribute("data-target"); var target = document.getElementById(targetId); if (!target) { return; } if (target.offsetTop - threshold <= scrollTop) { activeTarget = targetId; } }); nodeAnchors.forEach(function (anchor) { anchor.classList.toggle("active", anchor.getAttribute("data-target") === activeTarget); }); } function bindNodeAnchorClicks() { if (!chatScroll) { return; } nodeAnchors.forEach(function (anchor) { if (anchor.dataset.bound === "true") { return; } anchor.dataset.bound = "true"; anchor.addEventListener("click", function (event) { event.preventDefault(); var targetId = anchor.getAttribute("data-target"); var target = document.getElementById(targetId); if (!target) { return; } chatScroll.scrollTo({ top: Math.max(target.offsetTop - 20, 0), behavior: "smooth", }); }); }); } function ensureNodeRailVisible() { if (nodeRail) { nodeRail.classList.remove("hidden"); } } function syncNodeRailVisibility() { if (!nodeRail) { return; } refreshNodeAnchors(); if (nodeAnchors.length) { nodeRail.classList.remove("hidden"); } else { nodeRail.classList.add("hidden"); } } function escapeHtml(text) { return text .replace(/&/g, "&") .replace(//g, ">") .replace(/\"/g, """) .replace(/'/g, "'"); } function nl2br(text) { return escapeHtml(text).replace(/\n/g, "
"); } function renderInlineMarkdown(text) { return escapeHtml(text || "").replace(/\[([^\]]+)\]\(([^)]+)\)/g, function (_match, label, href) { var safeHref = escapeHtml(href); var safeLabel = escapeHtml(label); if (!/^\/[^/\\]/.test(href) && !/^https?:\/\//.test(href)) { return safeLabel; } return '' + safeLabel + ""; }); } function renderMarkdownTable(lines, startIndex) { var header = lines[startIndex].trim(); var separator = lines[startIndex + 1] ? lines[startIndex + 1].trim() : ""; if (header.charAt(0) !== "|" || separator.indexOf("---") === -1) { return null; } function cells(line) { return line .trim() .replace(/^\|/, "") .replace(/\|$/, "") .split("|") .map(function (cell) { return cell.trim(); }); } var html = ""; cells(header).forEach(function (cell) { html += ""; }); html += ""; var index = startIndex + 2; while (index < lines.length && lines[index].trim().charAt(0) === "|") { html += ""; cells(lines[index]).forEach(function (cell) { html += ""; }); html += ""; index += 1; } html += "
" + renderInlineMarkdown(cell) + "
" + renderInlineMarkdown(cell || "-") + "
"; return { html: html, nextIndex: index }; } function renderBasicMarkdown(text) { var lines = (text || "").split(/\r?\n/); var html = ""; var paragraph = []; var index = 0; function flushParagraph() { if (!paragraph.length) { return; } html += "

" + renderInlineMarkdown(paragraph.join("\n")).replace(/\n/g, "
") + "

"; paragraph = []; } while (index < lines.length) { var line = lines[index]; var table = renderMarkdownTable(lines, index); if (table) { flushParagraph(); html += table.html; index = table.nextIndex; continue; } if (!line.trim()) { flushParagraph(); } else { paragraph.push(line); } index += 1; } flushParagraph(); return html; } function renderAssistantContent(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 || ""); } } function renderExistingAssistantMessages() { document.querySelectorAll(".message.assistant .message-bubble").forEach(function (bubble) { var target = bubble.querySelector(".markdown-content"); var raw = bubble.querySelector(".message-raw"); if (!target || !raw || target.dataset.rendered === "true") { return; } target.innerHTML = renderAssistantContent(raw.content ? raw.content.textContent : raw.textContent); target.dataset.rendered = "true"; }); } function scrollChatToBottom() { if (chatScroll) { chatScroll.scrollTop = chatScroll.scrollHeight; } } function createMessage(role, content, messageId, label) { var article = document.createElement("article"); article.className = "message " + role; article.id = messageId; if (typeof messageId === "number") { article.setAttribute("data-message-id", messageId); } if (label) { article.setAttribute("data-node-label", label); } var avatar = document.createElement("div"); avatar.className = "message-avatar" + (role === "user" ? " user-mark" : ""); avatar.textContent = role === "assistant" ? "AI" : userMenuTrigger.querySelector(".avatar").textContent.trim(); var bubble = document.createElement("div"); bubble.className = "message-bubble"; var text = document.createElement(role === "assistant" ? "div" : "p"); if (role === "assistant") { text.className = "message-content markdown-content"; } text.innerHTML = role === "assistant" ? renderAssistantContent(content) : nl2br(content); bubble.appendChild(text); article.appendChild(avatar); article.appendChild(bubble); chatScroll.appendChild(article); return { article: article, bubble: bubble, text: text }; } function appendConversationMessage(message) { if (!message || document.querySelector('.message[data-message-id="' + message.id + '"]')) { return; } var label = message.role === "assistant" ? "AI " : "用户 "; label += document.querySelectorAll(".message").length + 1; var created = createMessage(message.role, message.content || "", "message-" + message.id, label); created.article.setAttribute("data-message-id", message.id); latestMessageId = Math.max(latestMessageId, message.id); if (message.role === "user") { appendNode(created.article.id, label, true); } } async function refreshConversationMessages() { var conversationId = currentConversationId(); if (!conversationId || !summaryPanel) { return; } var url = templateUrl("data-message-url-template", "__conversation_id__", conversationId); if (!url) { return; } try { var response = await fetch(url + "?after=" + latestMessageId, { cache: "no-store" }); if (!response.ok) { return; } var payload = await response.json(); (payload.messages || []).forEach(appendConversationMessage); if (payload.latest_message_id) { latestMessageId = Math.max(latestMessageId, payload.latest_message_id); } syncNodeRailVisibility(); bindNodeAnchorClicks(); setActiveNode(); scrollChatToBottom(); } catch (error) { console.error("Conversation message refresh failed", error); } } function appendNode(targetId, title, isLatest) { if (!nodeRail) { return; } ensureNodeRailVisible(); var anchor = document.createElement("a"); anchor.className = "node-anchor" + (isLatest ? " latest" : ""); anchor.href = "#" + targetId; anchor.setAttribute("data-target", targetId); anchor.title = title; var dot = document.createElement("span"); dot.className = "node-dot"; anchor.appendChild(dot); nodeRail.appendChild(anchor); syncNodeRailVisibility(); bindNodeAnchorClicks(); setActiveNode(); } function updateSidebarConversation(conversationId, title) { if (!conversationId || !title) { return; } var encodedTitle = title; 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"); var day = String(currentTime.getDate()).padStart(2, "0"); var hours = String(currentTime.getHours()).padStart(2, "0"); var minutes = String(currentTime.getMinutes()).padStart(2, "0"); var meta = month + "月" + day + "日 " + hours + ":" + minutes; document.querySelectorAll(".history-item.active").forEach(function (item) { item.classList.remove("active"); }); if (existing) { existing.classList.add("active"); existing.querySelector(".history-title").textContent = encodedTitle; existing.querySelector(".history-meta").textContent = meta; if (list.firstElementChild !== existing) { list.prepend(existing); } return; } if (!list) { return; } var empty = list.querySelector(".history-empty"); if (empty) { empty.remove(); } var item = document.createElement("a"); item.className = "history-item active"; item.setAttribute("data-conversation-id", conversationId); item.href = "/?conversation=" + conversationId; item.innerHTML = '' + escapeHtml(encodedTitle) + '' + meta + ""; list.prepend(item); } function setConversationTitle(title) { if (!title) { return; } var header = document.querySelector(".conversation-header h1"); var empty = document.querySelector(".empty-state"); if (empty) { empty.remove(); var headerWrap = document.createElement("div"); headerWrap.className = "conversation-header"; headerWrap.id = "conversation-top"; headerWrap.setAttribute("data-node-label", "会话开始"); headerWrap.innerHTML = '

审核智能体

' + escapeHtml(title) + '

正在生成回复'; chatScroll.prepend(headerWrap); return; } if (header) { header.textContent = title; } } 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 = '
暂无附件
'; return; } attachments.forEach(function (attachment) { var item = document.createElement("div"); item.className = "attachment-item"; item.setAttribute("data-attachment-id", attachment.id); item.innerHTML = "
" + escapeHtml(attachment.original_name) + "v" + attachment.version_no + " · " + attachment.file_size + " bytes · " + escapeHtml(attachment.upload_status) + "
" + (attachment.is_active ? "active" : ""); 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 = "
" + escapeHtml(batch.batch_no || "文件汇总") + 'running
    '; workflowCardList.prepend(card); return card; } async function refreshWorkflowCard(batchId) { if (!summaryPanel || !batchId) { 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 ""; } if (!response.ok) { console.error("Workflow status refresh returned non-OK", { batchId: batchId, status: response.status }); return ""; } var payload = await response.json(); var card = ensureWorkflowCard({ batch_id: payload.batch.id, batch_no: payload.batch.batch_no, }); if (!card) { return payload.batch.status || ""; } var status = card.querySelector(".workflow-status"); status.textContent = payload.batch.status; status.className = "workflow-status status-" + payload.batch.status; var batchError = card.querySelector(".workflow-error"); if (payload.batch.error_message) { if (!batchError) { batchError = document.createElement("p"); batchError.className = "workflow-error"; card.insertBefore(batchError, card.querySelector("ol")); } batchError.textContent = payload.batch.error_message; } else if (batchError) { batchError.remove(); } 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 = '
    ' + escapeHtml(node.node_name) + "" + (node.message ? "" + escapeHtml(node.message) + "" : "") + "
    " + 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)) { refreshConversationMessages(); stopWorkflowPolling(batchId); } }, WORKFLOW_POLL_INTERVAL_MS); refreshWorkflowCard(batchId).then(function (status) { if (isWorkflowTerminalStatus(status)) { refreshConversationMessages(); 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) { event.preventDefault(); if (!composer || !promptInput || !sendButton || !chatStage) { return; } var prompt = promptInput.value.trim(); if (!prompt || sendButton.disabled) { return; } sendButton.disabled = true; sendButton.textContent = "生成中..."; var formData = new FormData(composer); var csrfToken = formData.get("csrfmiddlewaretoken"); var streamUrl = chatStage.getAttribute("data-stream-url"); var tempUserId = "message-user-temp-" + Date.now(); var tempAssistantId = "message-ai-temp-" + (Date.now() + 1); var userLabel = "用户 " + (document.querySelectorAll(".message").length + 1); setConversationTitle((prompt || "").slice(0, 24)); var userMessage = createMessage("user", prompt, tempUserId, userLabel); var assistantMessage = createMessage("assistant", "", tempAssistantId, ""); assistantMessage.bubble.classList.add("streaming"); appendNode(userMessage.article.id, userLabel, false); scrollChatToBottom(); promptInput.value = ""; try { var response = await fetch(streamUrl, { method: "POST", headers: { "X-CSRFToken": csrfToken, }, body: formData, }); if (!response.ok || !response.body) { throw new Error("流式请求失败。"); } var reader = response.body.getReader(); var decoder = new TextDecoder("utf-8"); var buffer = ""; var assistantText = ""; while (true) { var readResult = await reader.read(); if (readResult.done) { break; } buffer += decoder.decode(readResult.value, { stream: true }); var events = buffer.split("\n\n"); buffer = events.pop(); events.forEach(function (frame) { var eventName = ""; var dataText = ""; frame.split("\n").forEach(function (line) { if (line.indexOf("event:") === 0) { eventName = line.slice(6).trim(); } if (line.indexOf("data:") === 0) { dataText += line.slice(5).trim(); } }); if (!eventName || !dataText) { return; } 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.user_message_id) { userMessage.article.id = "message-" + payload.user_message_id; userMessage.article.setAttribute("data-message-id", payload.user_message_id); latestMessageId = Math.max(latestMessageId, payload.user_message_id); } if (payload.conversation_id) { conversationIdInput.value = payload.conversation_id; window.history.replaceState({}, "", "/?conversation=" + payload.conversation_id); } if (payload.title) { setConversationTitle(payload.title); updateSidebarConversation(payload.conversation_id, payload.title); } } else if (eventName === "chunk") { assistantText += payload.delta || ""; assistantMessage.text.innerHTML = renderAssistantContent(assistantText); scrollChatToBottom(); } else if (eventName === "replace") { assistantText = payload.content || ""; assistantMessage.text.innerHTML = renderAssistantContent(assistantText); scrollChatToBottom(); } else if (eventName === "error") { assistantText = payload.message || "模型调用失败。"; assistantMessage.text.innerHTML = renderAssistantContent(assistantText); } else if (eventName === "workflow_started") { ensureWorkflowCard(payload); startWorkflowPolling(payload.batch_id); } else if (eventName === "done") { if (payload.assistant_message_id) { assistantMessage.article.id = "message-" + payload.assistant_message_id; assistantMessage.article.setAttribute("data-message-id", payload.assistant_message_id); latestMessageId = Math.max(latestMessageId, payload.assistant_message_id); } if (payload.title) { setConversationTitle(payload.title); updateSidebarConversation(payload.conversation_id, payload.title); } } }); } assistantMessage.bubble.classList.remove("streaming"); syncNodeRailVisibility(); bindNodeAnchorClicks(); setActiveNode(); scrollChatToBottom(); } catch (error) { assistantMessage.bubble.classList.remove("streaming"); assistantMessage.text.textContent = "请求失败,请稍后重试。"; } finally { sendButton.disabled = false; sendButton.textContent = "发送"; promptInput.focus(); } } function bindPromptKeyboardShortcuts() { if (!promptInput || !composer) { return; } promptInput.addEventListener("keydown", function (event) { if (event.key === "Enter" && !event.ctrlKey) { event.preventDefault(); if (typeof composer.requestSubmit === "function") { composer.requestSubmit(); } else { composer.dispatchEvent(new Event("submit", { cancelable: true })); } } }); } syncNodeRailVisibility(); syncLatestMessageIdFromDom(); bindNodeAnchorClicks(); renderExistingAssistantMessages(); refreshRunningWorkflowCards(); if (chatScroll) { chatScroll.addEventListener("scroll", setActiveNode, { passive: true }); setActiveNode(); } if (composer) { composer.addEventListener("submit", streamChat); } bindPromptKeyboardShortcuts(); 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(); })();