(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 += "" + renderInlineMarkdown(cell) + " ";
});
html += " ";
var index = startIndex + 2;
while (index < lines.length && lines[index].trim().charAt(0) === "|") {
html += "";
cells(lines[index]).forEach(function (cell) {
html += "" + renderInlineMarkdown(cell || "-") + " ";
});
html += " ";
index += 1;
}
html += "
";
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 statusUrlForWorkflow(workflow_type, batchId) {
var attributeName =
workflow_type === "regulatory_review" ? "data-regulatory-status-url-template" : "data-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 += "" + escapeHtml(config.label || field) + " ";
if (config.input_type === "select") {
html += '';
(config.options || []).forEach(function (option) {
var selected = option === config.suggested ? " selected" : "";
html += '" + escapeHtml(option) + " ";
});
html += " ";
} else {
html +=
' ';
}
html += " ";
});
return html;
}
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();
}
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 === "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();
});
});
}
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();
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();
})();