753 lines
23 KiB
JavaScript
753 lines
23 KiB
JavaScript
(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;
|
|
|
|
if (!workspace) {
|
|
return;
|
|
}
|
|
|
|
function isMobile() {
|
|
return window.matchMedia("(max-width: 980px)").matches;
|
|
}
|
|
|
|
function toggleSidebar() {
|
|
var state = workspace.getAttribute("data-sidebar-state");
|
|
if (isMobile()) {
|
|
workspace.setAttribute("data-sidebar-state", state === "open" ? "closed" : "open");
|
|
return;
|
|
}
|
|
workspace.setAttribute("data-sidebar-state", state === "collapsed" ? "open" : "collapsed");
|
|
}
|
|
|
|
function syncSidebarState() {
|
|
if (isMobile()) {
|
|
if (workspace.getAttribute("data-sidebar-state") !== "closed") {
|
|
workspace.setAttribute("data-sidebar-state", "closed");
|
|
}
|
|
} else if (workspace.getAttribute("data-sidebar-state") === "closed") {
|
|
workspace.setAttribute("data-sidebar-state", "open");
|
|
}
|
|
}
|
|
|
|
function refreshNodeAnchors() {
|
|
nodeAnchors = Array.prototype.slice.call(document.querySelectorAll(".node-anchor"));
|
|
}
|
|
|
|
if (sidebarToggle) {
|
|
sidebarToggle.addEventListener("click", toggleSidebar);
|
|
}
|
|
|
|
if (mobileSidebarToggle) {
|
|
mobileSidebarToggle.addEventListener("click", toggleSidebar);
|
|
}
|
|
|
|
if (userMenu && userMenuTrigger) {
|
|
userMenuTrigger.addEventListener("click", function () {
|
|
var isOpen = userMenu.classList.toggle("open");
|
|
userMenuTrigger.setAttribute("aria-expanded", isOpen ? "true" : "false");
|
|
});
|
|
|
|
document.addEventListener("click", function (event) {
|
|
if (!userMenu.contains(event.target)) {
|
|
userMenu.classList.remove("open");
|
|
userMenuTrigger.setAttribute("aria-expanded", "false");
|
|
}
|
|
});
|
|
}
|
|
|
|
function setActiveNode() {
|
|
if (!chatScroll || !nodeAnchors.length) {
|
|
return;
|
|
}
|
|
|
|
var activeTarget = nodeAnchors[0].getAttribute("data-target");
|
|
var scrollTop = chatScroll.scrollTop;
|
|
var threshold = 80;
|
|
|
|
nodeAnchors.forEach(function (anchor) {
|
|
var targetId = anchor.getAttribute("data-target");
|
|
var target = document.getElementById(targetId);
|
|
if (!target) {
|
|
return;
|
|
}
|
|
|
|
if (target.offsetTop - threshold <= scrollTop) {
|
|
activeTarget = targetId;
|
|
}
|
|
});
|
|
|
|
nodeAnchors.forEach(function (anchor) {
|
|
anchor.classList.toggle("active", anchor.getAttribute("data-target") === activeTarget);
|
|
});
|
|
}
|
|
|
|
function bindNodeAnchorClicks() {
|
|
if (!chatScroll) {
|
|
return;
|
|
}
|
|
nodeAnchors.forEach(function (anchor) {
|
|
if (anchor.dataset.bound === "true") {
|
|
return;
|
|
}
|
|
anchor.dataset.bound = "true";
|
|
anchor.addEventListener("click", function (event) {
|
|
event.preventDefault();
|
|
var targetId = anchor.getAttribute("data-target");
|
|
var target = document.getElementById(targetId);
|
|
if (!target) {
|
|
return;
|
|
}
|
|
chatScroll.scrollTo({
|
|
top: Math.max(target.offsetTop - 20, 0),
|
|
behavior: "smooth",
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
function ensureNodeRailVisible() {
|
|
if (nodeRail) {
|
|
nodeRail.classList.remove("hidden");
|
|
}
|
|
}
|
|
|
|
function syncNodeRailVisibility() {
|
|
if (!nodeRail) {
|
|
return;
|
|
}
|
|
refreshNodeAnchors();
|
|
if (nodeAnchors.length) {
|
|
nodeRail.classList.remove("hidden");
|
|
} else {
|
|
nodeRail.classList.add("hidden");
|
|
}
|
|
}
|
|
|
|
function escapeHtml(text) {
|
|
return text
|
|
.replace(/&/g, "&")
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">")
|
|
.replace(/\"/g, """)
|
|
.replace(/'/g, "'");
|
|
}
|
|
|
|
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 (label) {
|
|
article.setAttribute("data-node-label", label);
|
|
}
|
|
|
|
var avatar = document.createElement("div");
|
|
avatar.className = "message-avatar" + (role === "user" ? " user-mark" : "");
|
|
avatar.textContent = role === "assistant" ? "AI" : userMenuTrigger.querySelector(".avatar").textContent.trim();
|
|
|
|
var bubble = document.createElement("div");
|
|
bubble.className = "message-bubble";
|
|
|
|
var text = document.createElement(role === "assistant" ? "div" : "p");
|
|
if (role === "assistant") {
|
|
text.className = "message-content markdown-content";
|
|
}
|
|
text.innerHTML = role === "assistant" ? renderAssistantContent(content) : nl2br(content);
|
|
bubble.appendChild(text);
|
|
|
|
article.appendChild(avatar);
|
|
article.appendChild(bubble);
|
|
chatScroll.appendChild(article);
|
|
return { article: article, bubble: bubble, text: text };
|
|
}
|
|
|
|
function appendNode(targetId, title, isLatest) {
|
|
if (!nodeRail) {
|
|
return;
|
|
}
|
|
ensureNodeRailVisible();
|
|
var anchor = document.createElement("a");
|
|
anchor.className = "node-anchor" + (isLatest ? " latest" : "");
|
|
anchor.href = "#" + targetId;
|
|
anchor.setAttribute("data-target", targetId);
|
|
anchor.title = title;
|
|
|
|
var dot = document.createElement("span");
|
|
dot.className = "node-dot";
|
|
anchor.appendChild(dot);
|
|
nodeRail.appendChild(anchor);
|
|
syncNodeRailVisibility();
|
|
bindNodeAnchorClicks();
|
|
setActiveNode();
|
|
}
|
|
|
|
function updateSidebarConversation(conversationId, title) {
|
|
if (!conversationId || !title) {
|
|
return;
|
|
}
|
|
var encodedTitle = title;
|
|
var existing = document.querySelector('.history-item[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 =
|
|
'<span class="history-title">' +
|
|
escapeHtml(encodedTitle) +
|
|
'</span><span class="history-meta">' +
|
|
meta +
|
|
"</span>";
|
|
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 =
|
|
'<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 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 card = workflowCardList.querySelector('[data-batch-id="' + batch.batch_id + '"]');
|
|
if (card) {
|
|
return card;
|
|
}
|
|
card = document.createElement("article");
|
|
card.className = "workflow-card";
|
|
card.setAttribute("data-batch-id", batch.batch_id);
|
|
card.innerHTML =
|
|
"<header><strong>" +
|
|
escapeHtml(batch.batch_no || "文件汇总") +
|
|
'</strong><span class="workflow-status status-running">running</span></header><ol></ol>';
|
|
workflowCardList.prepend(card);
|
|
return card;
|
|
}
|
|
|
|
async function refreshWorkflowCard(batchId) {
|
|
if (!summaryPanel || !batchId) {
|
|
return "";
|
|
}
|
|
var response;
|
|
try {
|
|
response = await fetch(templateUrl("data-status-url-template", "__batch_id__", batchId), {
|
|
cache: "no-store",
|
|
});
|
|
} catch (error) {
|
|
console.error("Workflow status refresh failed", { batchId: batchId, error: error });
|
|
return "";
|
|
}
|
|
if (!response.ok) {
|
|
console.error("Workflow status refresh returned non-OK", { batchId: batchId, status: response.status });
|
|
return "";
|
|
}
|
|
var payload = await response.json();
|
|
var card = ensureWorkflowCard({
|
|
batch_id: payload.batch.id,
|
|
batch_no: payload.batch.batch_no,
|
|
});
|
|
if (!card) {
|
|
return payload.batch.status || "";
|
|
}
|
|
var status = card.querySelector(".workflow-status");
|
|
status.textContent = payload.batch.status;
|
|
status.className = "workflow-status status-" + payload.batch.status;
|
|
var 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 = "<span>" + escapeHtml(node.node_name) + "</span><em>" + node.progress + "%</em>";
|
|
list.appendChild(item);
|
|
});
|
|
return payload.batch.status || "";
|
|
}
|
|
|
|
function isWorkflowTerminalStatus(status) {
|
|
return status === "success" || status === "failed";
|
|
}
|
|
|
|
function stopWorkflowPolling(batchId) {
|
|
if (!workflowPollingTimers[batchId]) {
|
|
return;
|
|
}
|
|
window.clearInterval(workflowPollingTimers[batchId]);
|
|
delete workflowPollingTimers[batchId];
|
|
}
|
|
|
|
function startWorkflowPolling(batchId) {
|
|
if (!batchId || workflowPollingTimers[batchId]) {
|
|
return;
|
|
}
|
|
workflowPollingTimers[batchId] = window.setInterval(async function () {
|
|
var status = await refreshWorkflowCard(batchId);
|
|
if (isWorkflowTerminalStatus(status)) {
|
|
stopWorkflowPolling(batchId);
|
|
}
|
|
}, WORKFLOW_POLL_INTERVAL_MS);
|
|
refreshWorkflowCard(batchId).then(function (status) {
|
|
if (isWorkflowTerminalStatus(status)) {
|
|
stopWorkflowPolling(batchId);
|
|
}
|
|
});
|
|
}
|
|
|
|
function refreshRunningWorkflowCards() {
|
|
if (!workflowCardList) {
|
|
return;
|
|
}
|
|
workflowCardList.querySelectorAll(".workflow-card").forEach(function (card) {
|
|
var batchId = card.getAttribute("data-batch-id");
|
|
var status = card.querySelector(".workflow-status");
|
|
var statusText = status ? status.textContent.trim() : "";
|
|
if (!isWorkflowTerminalStatus(statusText)) {
|
|
startWorkflowPolling(batchId);
|
|
}
|
|
});
|
|
}
|
|
|
|
async function streamChat(event) {
|
|
event.preventDefault();
|
|
if (!composer || !promptInput || !sendButton || !chatStage) {
|
|
return;
|
|
}
|
|
|
|
var prompt = promptInput.value.trim();
|
|
if (!prompt || sendButton.disabled) {
|
|
return;
|
|
}
|
|
|
|
sendButton.disabled = true;
|
|
sendButton.textContent = "生成中...";
|
|
|
|
var formData = new FormData(composer);
|
|
var csrfToken = formData.get("csrfmiddlewaretoken");
|
|
var streamUrl = chatStage.getAttribute("data-stream-url");
|
|
var tempUserId = "message-user-temp-" + Date.now();
|
|
var tempAssistantId = "message-ai-temp-" + (Date.now() + 1);
|
|
var userLabel = "用户 " + (document.querySelectorAll(".message").length + 1);
|
|
|
|
setConversationTitle((prompt || "").slice(0, 24));
|
|
var userMessage = createMessage("user", prompt, tempUserId, userLabel);
|
|
var assistantMessage = createMessage("assistant", "", tempAssistantId, "");
|
|
assistantMessage.bubble.classList.add("streaming");
|
|
appendNode(userMessage.article.id, userLabel, false);
|
|
scrollChatToBottom();
|
|
promptInput.value = "";
|
|
|
|
try {
|
|
var response = await fetch(streamUrl, {
|
|
method: "POST",
|
|
headers: {
|
|
"X-CSRFToken": csrfToken,
|
|
},
|
|
body: formData,
|
|
});
|
|
|
|
if (!response.ok || !response.body) {
|
|
throw new Error("流式请求失败。");
|
|
}
|
|
|
|
var reader = response.body.getReader();
|
|
var decoder = new TextDecoder("utf-8");
|
|
var buffer = "";
|
|
var assistantText = "";
|
|
|
|
while (true) {
|
|
var readResult = await reader.read();
|
|
if (readResult.done) {
|
|
break;
|
|
}
|
|
|
|
buffer += decoder.decode(readResult.value, { stream: true });
|
|
var events = buffer.split("\n\n");
|
|
buffer = events.pop();
|
|
|
|
events.forEach(function (frame) {
|
|
var eventName = "";
|
|
var dataText = "";
|
|
frame.split("\n").forEach(function (line) {
|
|
if (line.indexOf("event:") === 0) {
|
|
eventName = line.slice(6).trim();
|
|
}
|
|
if (line.indexOf("data:") === 0) {
|
|
dataText += line.slice(5).trim();
|
|
}
|
|
});
|
|
|
|
if (!eventName || !dataText) {
|
|
return;
|
|
}
|
|
|
|
var payload;
|
|
try {
|
|
payload = JSON.parse(dataText);
|
|
} catch (error) {
|
|
console.error("SSE frame parse failed", { error: error, frame: frame });
|
|
return;
|
|
}
|
|
if (eventName === "meta") {
|
|
if (payload.conversation_id) {
|
|
conversationIdInput.value = payload.conversation_id;
|
|
window.history.replaceState({}, "", "/?conversation=" + payload.conversation_id);
|
|
}
|
|
if (payload.title) {
|
|
setConversationTitle(payload.title);
|
|
updateSidebarConversation(payload.conversation_id, payload.title);
|
|
}
|
|
} else if (eventName === "chunk") {
|
|
assistantText += payload.delta || "";
|
|
assistantMessage.text.innerHTML = renderAssistantContent(assistantText);
|
|
scrollChatToBottom();
|
|
} else if (eventName === "error") {
|
|
assistantText = payload.message || "模型调用失败。";
|
|
assistantMessage.text.innerHTML = renderAssistantContent(assistantText);
|
|
} else if (eventName === "workflow_started") {
|
|
ensureWorkflowCard(payload);
|
|
startWorkflowPolling(payload.batch_id);
|
|
} else if (eventName === "done") {
|
|
if (payload.assistant_message_id) {
|
|
assistantMessage.article.id = "message-" + payload.assistant_message_id;
|
|
}
|
|
if (payload.title) {
|
|
setConversationTitle(payload.title);
|
|
updateSidebarConversation(payload.conversation_id, payload.title);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
assistantMessage.bubble.classList.remove("streaming");
|
|
syncNodeRailVisibility();
|
|
bindNodeAnchorClicks();
|
|
setActiveNode();
|
|
scrollChatToBottom();
|
|
} catch (error) {
|
|
assistantMessage.bubble.classList.remove("streaming");
|
|
assistantMessage.text.textContent = "请求失败,请稍后重试。";
|
|
} finally {
|
|
sendButton.disabled = false;
|
|
sendButton.textContent = "发送";
|
|
promptInput.focus();
|
|
}
|
|
}
|
|
|
|
syncNodeRailVisibility();
|
|
bindNodeAnchorClicks();
|
|
renderExistingAssistantMessages();
|
|
refreshRunningWorkflowCards();
|
|
|
|
if (chatScroll) {
|
|
chatScroll.addEventListener("scroll", setActiveNode, { passive: true });
|
|
setActiveNode();
|
|
}
|
|
|
|
if (composer) {
|
|
composer.addEventListener("submit", streamChat);
|
|
}
|
|
|
|
if (uploadDropzone && attachmentInput) {
|
|
uploadDropzone.addEventListener("click", function () {
|
|
attachmentInput.click();
|
|
});
|
|
uploadDropzone.addEventListener("dragover", function (event) {
|
|
event.preventDefault();
|
|
uploadDropzone.classList.add("dragging");
|
|
});
|
|
uploadDropzone.addEventListener("dragleave", function () {
|
|
uploadDropzone.classList.remove("dragging");
|
|
});
|
|
uploadDropzone.addEventListener("drop", function (event) {
|
|
event.preventDefault();
|
|
uploadDropzone.classList.remove("dragging");
|
|
uploadFiles(event.dataTransfer.files);
|
|
});
|
|
attachmentInput.addEventListener("change", function () {
|
|
uploadFiles(attachmentInput.files);
|
|
attachmentInput.value = "";
|
|
});
|
|
}
|
|
|
|
window.addEventListener("resize", syncSidebarState);
|
|
syncSidebarState();
|
|
})();
|