(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 false; } 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); } return 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(); var appendedCount = 0; (payload.messages || []).forEach(function (message) { if (appendConversationMessage(message)) { appendedCount += 1; } }); if (payload.latest_message_id) { latestMessageId = Math.max(latestMessageId, payload.latest_message_id); } syncNodeRailVisibility(); bindNodeAnchorClicks(); setActiveNode(); if (appendedCount > 0) { 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 statusUrlForWorkflow(workflow_type, batchId) { var attributeName = "data-status-url-template"; if (workflow_type === "regulatory_review") { attributeName = "data-regulatory-status-url-template"; } else if (workflow_type === "application_form_fill") { attributeName = "data-application-form-fill-status-url-template"; } return templateUrl(attributeName, "__batch_id__", batchId); } 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 workflow_type = batch.workflow_type || "file_summary"; var card = workflowCardList.querySelector( '[data-batch-id="' + batch.batch_id + '"][data-workflow-type="' + workflow_type + '"]' ); if (card) { return card; } card = document.createElement("article"); card.className = "workflow-card"; card.setAttribute("data-batch-id", batch.batch_id); card.setAttribute("data-workflow-type", workflow_type); card.innerHTML = "
" + escapeHtml(batch.batch_no || "文件汇总") + 'running
    '; workflowCardList.prepend(card); refreshWorkflowBatchCarousel(0); return card; } function workflowCards() { if (!workflowCardList) { return []; } return Array.prototype.slice.call(workflowCardList.querySelectorAll(".workflow-card")); } function ensureWorkflowBatchControls() { if (!workflowCardList || workflowCardList.querySelector(".workflow-batch-controls")) { return; } var controls = document.createElement("div"); controls.className = "workflow-batch-controls"; controls.innerHTML = '' + '
    ' + ''; workflowCardList.appendChild(controls); } function selectWorkflowBatchIndex(index) { var cards = workflowCards(); if (!workflowCardList || !cards.length) { return; } var safeIndex = Math.max(0, Math.min(index, cards.length - 1)); workflowCardList.setAttribute("data-active-index", safeIndex); cards.forEach(function (card, cardIndex) { var isActive = cardIndex === safeIndex; card.classList.toggle("active", isActive); card.setAttribute("data-workflow-index", cardIndex); card.setAttribute("aria-hidden", isActive ? "false" : "true"); }); var dots = workflowCardList.querySelector(".workflow-batch-dots"); if (!dots) { return; } dots.querySelectorAll("[data-workflow-index-dot]").forEach(function (dot) { var dotIndex = parseInt(dot.getAttribute("data-workflow-index-dot"), 10); var isActive = dotIndex === safeIndex; dot.classList.toggle("active", isActive); dot.setAttribute("aria-current", isActive ? "true" : "false"); }); } function refreshWorkflowBatchCarousel(preferredIndex) { var cards = workflowCards(); if (!workflowCardList || !cards.length) { return; } workflowCardList.classList.add("workflow-batch-carousel"); ensureWorkflowBatchControls(); var dots = workflowCardList.querySelector(".workflow-batch-dots"); if (dots) { dots.innerHTML = ""; cards.forEach(function (card, index) { card.setAttribute("data-workflow-index", index); var title = card.querySelector("strong"); var dot = document.createElement("button"); dot.type = "button"; dot.className = "workflow-batch-dot"; dot.setAttribute("data-workflow-index-dot", index); dot.setAttribute("aria-label", "查看" + (title ? title.textContent.trim() : "工作流") + "状态"); dots.appendChild(dot); }); } var activeIndex = typeof preferredIndex === "number" ? preferredIndex : parseInt(workflowCardList.getAttribute("data-active-index") || "0", 10); if (Number.isNaN(activeIndex)) { activeIndex = 0; } selectWorkflowBatchIndex(activeIndex); } function ensureConditionConfirmationCard(confirmation) { if (!chatScroll || !confirmation || !confirmation.candidates) { return; } var cardId = "condition-confirmation-" + confirmation.batch_id; removeStaleConditionConfirmationCards(cardId); if (document.getElementById(cardId)) { return; } var article = document.createElement("article"); article.className = "message assistant"; article.id = cardId; article.setAttribute("data-condition-confirmation-card", ""); article.setAttribute("data-node-label", "AI 适用条件确认"); var avatar = document.createElement("div"); avatar.className = "message-avatar"; avatar.textContent = "AI"; var bubble = document.createElement("div"); bubble.className = "message-bubble"; var form = document.createElement("form"); form.className = "condition-confirm-form"; form.setAttribute("data-condition-confirm-form", ""); form.setAttribute("data-batch-id", confirmation.batch_id); form.setAttribute("data-confirm-url", confirmation.confirm_url); form.innerHTML = '' + "适用条件确认" + "

    请确认 " + escapeHtml(confirmation.batch_no || "") + " 的产品类别、注册类型和临床评价路径,确认后我会继续法规核查。

    " + renderConditionFields(confirmation.candidates) + '' + '

    '; bubble.appendChild(form); article.appendChild(avatar); article.appendChild(bubble); chatScroll.appendChild(article); bindConditionConfirmForms(); scrollChatToBottom(); } function removeStaleConditionConfirmationCards(activeCardId) { document.querySelectorAll("[data-condition-confirmation-card]").forEach(function (card) { if (card.id !== activeCardId) { card.remove(); } }); } function renderConditionFields(candidates) { var html = ""; Object.keys(candidates || {}).forEach(function (field) { var config = candidates[field] || {}; html += ""; }); return html; } function notificationLabel(notification) { if (!notification) { return "暂无飞书通知记录"; } return notification.status_label || notification.send_status || "飞书通知状态未知"; } function renderNotificationSummary(card, notification) { var panel = card.querySelector(".workflow-notification"); if (!panel) { panel = document.createElement("p"); panel.className = "workflow-notification"; card.insertBefore(panel, card.querySelector("ol")); } var text = notificationLabel(notification); if (notification && notification.receiver) { text += " · " + notification.receiver; } if (notification && notification.sent_at) { text += " · " + notification.sent_at; } if (notification && notification.error_message) { text += " · " + notification.error_message; } panel.textContent = text; panel.setAttribute("data-notification-status", notification ? notification.send_status || "" : "none"); } async function refreshWorkflowCard(batchId, workflow_type) { if (!summaryPanel || !batchId) { return ""; } var response; try { response = await fetch(statusUrlForWorkflow(workflow_type || "file_summary", 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(); if (payload.condition_confirmation) { ensureConditionConfirmationCard(payload.condition_confirmation); } var card = ensureWorkflowCard({ batch_id: payload.batch.id, batch_no: payload.batch.batch_no, workflow_type: payload.batch.workflow_type || workflow_type || "file_summary", }); 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 riskSummary = card.querySelector(".workflow-risk-summary"); if (payload.batch.risk_summary_text) { if (!riskSummary) { riskSummary = document.createElement("p"); riskSummary.className = "workflow-risk-summary"; card.insertBefore(riskSummary, card.querySelector("ol")); } riskSummary.textContent = payload.batch.risk_summary_text; } else if (riskSummary) { riskSummary.remove(); } renderNotificationSummary(card, payload.latest_notification); 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); }); refreshWorkflowBatchCarousel(); return payload.batch.status || ""; } function bindWorkflowBatchCarouselControls() { if (!workflowCardList) { return; } workflowCardList.addEventListener("click", function (event) { var cards = workflowCards(); if (!cards.length) { return; } var actionButton = event.target.closest("[data-workflow-action]"); var dotButton = event.target.closest("[data-workflow-index-dot]"); var currentIndex = parseInt(workflowCardList.getAttribute("data-active-index") || "0", 10); if (Number.isNaN(currentIndex)) { currentIndex = 0; } if (actionButton) { var nextIndex = actionButton.getAttribute("data-workflow-action") === "next" ? (currentIndex + 1) % cards.length : (currentIndex - 1 + cards.length) % cards.length; selectWorkflowBatchIndex(nextIndex); } else if (dotButton) { selectWorkflowBatchIndex(parseInt(dotButton.getAttribute("data-workflow-index-dot"), 10)); } }); } function isWorkflowTerminalStatus(status) { return status === "success" || status === "partial_success" || status === "failed"; } function workflowTimerKey(batchId, workflow_type) { return (workflow_type || "file_summary") + ":" + batchId; } function stopWorkflowPolling(batchId, workflow_type) { var key = workflowTimerKey(batchId, workflow_type); if (!workflowPollingTimers[key]) { return; } window.clearInterval(workflowPollingTimers[key]); delete workflowPollingTimers[key]; } function startWorkflowPolling(batchId, workflow_type) { var card = workflowCardList ? workflowCardList.querySelector('[data-batch-id="' + batchId + '"]') : null; workflow_type = workflow_type || (card ? card.getAttribute("data-workflow-type") || "file_summary" : "file_summary"); var key = workflowTimerKey(batchId, workflow_type); if (!batchId || workflowPollingTimers[key]) { return; } workflowPollingTimers[key] = window.setInterval(async function () { var status = await refreshWorkflowCard(batchId, workflow_type); if (isWorkflowTerminalStatus(status)) { refreshConversationMessages(); stopWorkflowPolling(batchId, workflow_type); } }, WORKFLOW_POLL_INTERVAL_MS); refreshWorkflowCard(batchId, workflow_type).then(function (status) { if (isWorkflowTerminalStatus(status)) { refreshConversationMessages(); stopWorkflowPolling(batchId, workflow_type); } }); } function refreshRunningWorkflowCards() { if (!workflowCardList) { return; } workflowCardList.querySelectorAll(".workflow-card").forEach(function (card) { var batchId = card.getAttribute("data-batch-id"); var workflow_type = card.getAttribute("data-workflow-type") || "file_summary"; var status = card.querySelector(".workflow-status"); var statusText = status ? status.textContent.trim() : ""; if (!isWorkflowTerminalStatus(statusText)) { startWorkflowPolling(batchId, workflow_type); } }); } function bindConditionConfirmForms() { document.querySelectorAll("[data-condition-confirm-form]").forEach(function (form) { if (form.dataset.bound === "true") { return; } form.dataset.bound = "true"; form.addEventListener("submit", async function (event) { event.preventDefault(); var batchId = form.getAttribute("data-batch-id"); var status = form.querySelector("[data-condition-confirm-status]"); var submitButton = form.querySelector('button[type="submit"]'); var formData = new FormData(form); var conditions = {}; formData.forEach(function (value, key) { if (key !== "csrfmiddlewaretoken") { conditions[key] = value; } }); if (submitButton) { submitButton.disabled = true; } if (status) { status.textContent = "正在恢复法规核查..."; } try { var response = await fetch(form.getAttribute("data-confirm-url"), { method: "POST", headers: { "Content-Type": "application/json", "X-CSRFToken": formData.get("csrfmiddlewaretoken"), }, body: JSON.stringify({ conditions: conditions }), }); if (!response.ok) { throw new Error("确认失败。"); } if (status) { status.textContent = "已确认,工作流继续执行。"; } form.classList.add("confirmed"); startWorkflowPolling(batchId, "regulatory_review"); await refreshWorkflowCard(batchId, "regulatory_review"); } catch (error) { if (status) { status.textContent = "确认失败,请稍后重试。"; } if (submitButton) { submitButton.disabled = false; } } }); }); } function bindRectificationActionButtons() { document.querySelectorAll("[data-rectification-action]").forEach(function (button) { if (button.dataset.bound === "true") { return; } button.dataset.bound = "true"; button.addEventListener("click", function () { if (!promptInput) { return; } var action = button.getAttribute("data-rectification-action"); var batchNo = button.getAttribute("data-batch-no") || ""; if (action === "full-review") { promptInput.value = "请基于新的文件汇总批次,对法规核查批次 " + batchNo + " 发起整包复核,并先确认使用哪个补充批次。"; } else { promptInput.value = "请对法规核查批次 " + batchNo + " 的缺失项发起复核,并先确认 issue_ids 和补充文件汇总批次。"; } promptInput.focus(); }); }); } function bindPromptTemplateButtons() { document.querySelectorAll("[data-prompt-template]").forEach(function (button) { if (button.dataset.bound === "true") { return; } button.dataset.bound = "true"; button.addEventListener("click", function () { if (!promptInput) { return; } var template = button.getAttribute("data-prompt-template") || ""; promptInput.value = template; promptInput.focus(); if (typeof promptInput.setSelectionRange === "function") { promptInput.setSelectionRange(promptInput.value.length, promptInput.value.length); } }); }); } 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, payload.workflow_type); } 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(); refreshWorkflowBatchCarousel(0); bindWorkflowBatchCarouselControls(); bindConditionConfirmForms(); bindRectificationActionButtons(); bindPromptTemplateButtons(); 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(); })();