Files
DEMO-AGENT/static/js/app.js

1284 lines
43 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
(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 getCsrfToken() {
if (!composer) {
return "";
}
return new FormData(composer).get("csrfmiddlewaretoken") || "";
}
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, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/\"/g, "&quot;")
.replace(/'/g, "&#039;");
}
function nl2br(text) {
return escapeHtml(text).replace(/\n/g, "<br>");
}
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 '<a href="' + safeHref + '" target="_blank" rel="noopener">' + safeLabel + "</a>";
});
}
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 = "<table><thead><tr>";
cells(header).forEach(function (cell) {
html += "<th>" + renderInlineMarkdown(cell) + "</th>";
});
html += "</tr></thead><tbody>";
var index = startIndex + 2;
while (index < lines.length && lines[index].trim().charAt(0) === "|") {
html += "<tr>";
cells(lines[index]).forEach(function (cell) {
html += "<td>" + renderInlineMarkdown(cell || "-") + "</td>";
});
html += "</tr>";
index += 1;
}
html += "</tbody></table>";
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 += "<p>" + renderInlineMarkdown(paragraph.join("\n")).replace(/\n/g, "<br>") + "</p>";
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("div");
item.className = "history-item active";
item.setAttribute("data-conversation-id", conversationId);
item.setAttribute("data-delete-url", "/api/review-agent/conversations/" + conversationId + "/");
item.innerHTML =
'<a class="history-link" href="/?conversation=' +
encodeURIComponent(conversationId) +
'"><span class="history-title">' +
escapeHtml(encodedTitle) +
'</span><span class="history-meta">' +
meta +
'</span></a><button class="history-delete" type="button" data-conversation-delete aria-label="删除对话 ' +
escapeHtml(encodedTitle) +
'" title="删除对话">×</button>';
list.prepend(item);
}
async function deleteConversation(item) {
if (!item) {
return;
}
var url = item.getAttribute("data-delete-url");
var conversationId = item.getAttribute("data-conversation-id");
if (!url || !conversationId) {
return;
}
var titleNode = item.querySelector(".history-title");
var title = titleNode ? titleNode.textContent.trim() : "这个对话";
if (!window.confirm('确定删除对话“' + title + '”?')) {
return;
}
var response = await fetch(url, {
method: "DELETE",
headers: {
"X-CSRFToken": getCsrfToken(),
},
});
if (!response.ok) {
throw new Error("删除对话失败");
}
var isCurrent = currentConversationId() === conversationId;
item.remove();
var list = document.querySelector(".history-list");
if (list && !list.querySelector(".history-item")) {
var empty = document.createElement("div");
empty.className = "history-empty";
empty.innerHTML = "<p>暂无会话记录</p><span>点击上方“新对话”开始审核。</span>";
list.appendChild(empty);
}
if (isCurrent) {
window.location.href = "/";
}
}
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 =
'<div><p class="eyebrow">审核智能体</p><h1>' +
escapeHtml(title) +
'</h1></div><span class="conversation-meta">正在生成回复</span>';
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 = '<div class="panel-empty">暂无附件</div>';
return;
}
attachments.forEach(function (attachment) {
var item = document.createElement("div");
item.className = "attachment-item";
item.setAttribute("data-attachment-id", attachment.id);
item.innerHTML =
"<div><strong>" +
escapeHtml(attachment.original_name) +
"</strong><span>v" +
attachment.version_no +
" · " +
attachment.file_size +
" bytes · " +
escapeHtml(attachment.upload_status) +
"</span></div>" +
(attachment.is_active ? "<em>active</em>" : "");
attachmentList.appendChild(item);
});
}
async function refreshAttachments() {
var conversationId = currentConversationId();
if (!conversationId || !summaryPanel) {
return;
}
var response = await fetch(templateUrl("data-attachment-url-template", "__conversation_id__", conversationId));
if (!response.ok) {
return;
}
var payload = await response.json();
renderAttachments(payload.attachments || []);
}
async function uploadFiles(files) {
var conversationId = currentConversationId();
if (!conversationId || !files.length || !summaryPanel) {
if (uploadStatus) {
uploadStatus.textContent = "请先创建或选择一个对话。";
}
return;
}
var data = new FormData();
Array.prototype.forEach.call(files, function (file) {
data.append("files", file);
});
var csrf = new FormData(composer).get("csrfmiddlewaretoken");
if (uploadStatus) {
uploadStatus.textContent = "正在上传 " + files.length + " 个文件...";
}
try {
var response = await fetch(templateUrl("data-attachment-url-template", "__conversation_id__", conversationId), {
method: "POST",
headers: { "X-CSRFToken": csrf },
body: data,
});
if (!response.ok) {
throw new Error("上传失败。");
}
var payload = await response.json();
renderAttachments(payload.attachments || []);
if (uploadStatus) {
uploadStatus.textContent = "上传完成,可发送自动汇总提示词。";
}
await refreshAttachments();
} catch (error) {
if (uploadStatus) {
uploadStatus.textContent = "上传失败,请重试。";
}
}
}
function ensureWorkflowCard(batch) {
if (!workflowCardList || !batch) {
return null;
}
var empty = workflowCardList.querySelector(".panel-empty");
if (empty) {
empty.remove();
}
var 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 =
"<header><strong>" +
escapeHtml(batch.batch_no || "文件汇总") +
'</strong><span class="workflow-status status-running">running</span></header><ol></ol>';
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 =
'<button type="button" class="workflow-batch-btn" data-workflow-action="prev" aria-label="上一个工作流">&lsaquo;</button>' +
'<div class="workflow-batch-dots" aria-label="工作流批次"></div>' +
'<button type="button" class="workflow-batch-btn" data-workflow-action="next" aria-label="下一个工作流">&rsaquo;</button>';
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 =
'<input type="hidden" name="csrfmiddlewaretoken" value="' +
escapeHtml(new FormData(composer).get("csrfmiddlewaretoken") || "") +
'">' +
"<strong>适用条件确认</strong>" +
"<p>请确认 " +
escapeHtml(confirmation.batch_no || "") +
" 的产品类别、注册类型和临床评价路径,确认后我会继续法规核查。</p>" +
renderConditionFields(confirmation.candidates) +
'<button type="submit">确认并继续</button>' +
'<p class="condition-confirm-status" data-condition-confirm-status></p>';
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 += "<label><span>" + escapeHtml(config.label || field) + "</span>";
if (config.input_type === "select") {
html += '<select name="' + escapeHtml(field) + '">';
(config.options || []).forEach(function (option) {
var selected = option === config.suggested ? " selected" : "";
html += '<option value="' + escapeHtml(option) + '"' + selected + ">" + escapeHtml(option) + "</option>";
});
html += "</select>";
} else {
html +=
'<input type="text" name="' +
escapeHtml(field) +
'" value="' +
escapeHtml(config.suggested || "") +
'">';
}
html += "</label>";
});
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 =
'<div><span>' +
escapeHtml(node.node_name) +
"</span>" +
(node.message ? "<small>" + escapeHtml(node.message) + "</small>" : "") +
"</div><em>" +
node.progress +
"%</em>";
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 }));
}
}
});
}
function bindConversationDeleteButtons() {
var list = document.querySelector(".history-list");
if (!list) {
return;
}
list.addEventListener("click", function (event) {
var button = event.target.closest("[data-conversation-delete]");
if (!button) {
return;
}
event.preventDefault();
event.stopPropagation();
var item = button.closest(".history-item");
deleteConversation(item).catch(function () {
window.alert("删除对话失败,请稍后重试。");
});
});
}
syncNodeRailVisibility();
syncLatestMessageIdFromDom();
bindNodeAnchorClicks();
renderExistingAssistantMessages();
refreshWorkflowBatchCarousel(0);
bindWorkflowBatchCarouselControls();
bindConditionConfirmForms();
bindRectificationActionButtons();
bindPromptTemplateButtons();
bindConversationDeleteButtons();
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();
})();