(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 = []; 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")); } 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) { if (window.marked && window.DOMPurify) { return window.DOMPurify.sanitize(window.marked.parse(text || "")); } return renderBasicMarkdown(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 (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 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[href*="conversation=' + 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.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 = 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 = "" + escapeHtml(node.node_name) + "" + node.progress + "%"; list.appendChild(item); }); } 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 = JSON.parse(dataText); if (eventName === "meta") { 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 === "error") { assistantText = payload.message || "模型调用失败。"; 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; } 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(); } } syncNodeRailVisibility(); bindNodeAnchorClicks(); renderExistingAssistantMessages(); if (chatScroll) { chatScroll.addEventListener("scroll", setActiveNode, { passive: true }); setActiveNode(); } if (composer) { 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(); })();