const state = {
agents: [],
lastTask: null,
activeTaskId: null,
mentionOptions: [],
activeMentionIndex: 0,
};
const els = {
agentList: document.getElementById("agentList"),
chatForm: document.getElementById("chatForm"),
messageInput: document.getElementById("messageInput"),
mentionMenu: document.getElementById("mentionMenu"),
messages: document.getElementById("messages"),
statusText: document.getElementById("statusText"),
refreshButton: document.getElementById("refreshButton"),
taskView: document.getElementById("taskView"),
eventList: document.getElementById("eventList"),
};
async function api(path, options = {}) {
const response = await fetch(path, {
headers: { "Content-Type": "application/json", ...(options.headers || {}) },
...options,
});
if (!response.ok) {
const text = await response.text();
throw new Error(text || `HTTP ${response.status}`);
}
return response.json();
}
function nowText() {
return new Date().toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit" });
}
function escapeHtml(value) {
return String(value)
.replaceAll("&", "&")
.replaceAll("<", "<")
.replaceAll(">", ">")
.replaceAll('"', """)
.replaceAll("'", "'");
}
function linkify(text) {
const escaped = escapeHtml(text);
return escaped.replace(/(https?:\/\/[^\s]+|\/rss-digests\/[^\s]+)/g, (url) => {
const href = url.startsWith("/") ? url : url;
return `${url}`;
});
}
function renderInlineMarkdown(text) {
let html = linkify(text);
html = html.replace(/`([^`]+)`/g, "$1");
html = html.replace(/\*\*([^*]+)\*\*/g, "$1");
html = html.replace(/\*([^*]+)\*/g, "$1");
return html;
}
function renderMarkdown(markdown) {
const lines = String(markdown || "").replace(/\r\n/g, "\n").split("\n");
const blocks = [];
let paragraph = [];
let list = null;
let code = null;
function flushParagraph() {
if (!paragraph.length) return;
blocks.push(`
${renderInlineMarkdown(paragraph.join(" "))}
`); paragraph = []; } function flushList() { if (!list) return; const tag = list.type === "ol" ? "ol" : "ul"; blocks.push(`<${tag}>${list.items.map((item) => `${escapeHtml(code.join("\n"))}`);
code = null;
}
for (const line of lines) {
if (line.trim().startsWith("```")) {
if (code === null) {
flushParagraph();
flushList();
code = [];
} else {
flushCode();
}
continue;
}
if (code !== null) {
code.push(line);
continue;
}
const trimmed = line.trim();
if (!trimmed) {
flushParagraph();
flushList();
continue;
}
const heading = trimmed.match(/^(#{1,3})\s+(.+)$/);
if (heading) {
flushParagraph();
flushList();
const level = heading[1].length;
blocks.push(`${renderInlineMarkdown(trimmed.slice(2))}`); continue; } flushList(); paragraph.push(trimmed); } flushCode(); flushParagraph(); flushList(); return blocks.join(""); } function appendMessage(kind, author, body) { const node = document.createElement("article"); node.className = `message ${kind}`; node.innerHTML = ` `; els.messages.appendChild(node); els.messages.scrollTop = els.messages.scrollHeight; } function insertMention(agentId) { const mention = `@${agentId} `; const current = els.messageInput.value.trimStart(); const withoutOldMention = current.replace(/^@[a-zA-Z0-9_\-]+\s*/, ""); els.messageInput.value = mention + withoutOldMention; hideMentionMenu(); els.messageInput.focus(); } function mentionChoices() { return state.agents.map((agent) => ({ ...agent, mention_id: agent.agent_id, })); } function mentionQuery() { const value = els.messageInput.value; const cursor = els.messageInput.selectionStart || 0; const beforeCursor = value.slice(0, cursor); const match = beforeCursor.match(/(^|\s)@([a-zA-Z0-9_\-]*)$/); return match ? match[2].toLowerCase() : null; } function hideMentionMenu() { els.mentionMenu.hidden = true; els.mentionMenu.innerHTML = ""; state.mentionOptions = []; state.activeMentionIndex = 0; } function chooseMention(option) { const value = els.messageInput.value; const cursor = els.messageInput.selectionStart || 0; const beforeCursor = value.slice(0, cursor); const afterCursor = value.slice(cursor); const replacement = `@${option.agent_id} `; const replacedBefore = beforeCursor.replace(/(^|\s)@[a-zA-Z0-9_\-]*$/, (prefix) => { const leadingSpace = prefix.startsWith(" ") ? " " : ""; return leadingSpace + replacement; }); els.messageInput.value = replacedBefore + afterCursor.trimStart(); hideMentionMenu(); els.messageInput.focus(); } function renderMentionMenu() { const query = mentionQuery(); if (query === null) { hideMentionMenu(); return; } state.mentionOptions = mentionChoices().filter((option) => { const haystack = `${option.agent_id} ${option.name}`.toLowerCase(); return haystack.includes(query); }); state.activeMentionIndex = Math.min(state.activeMentionIndex, Math.max(state.mentionOptions.length - 1, 0)); if (!state.mentionOptions.length) { hideMentionMenu(); return; } els.mentionMenu.innerHTML = ""; for (const [index, option] of state.mentionOptions.entries()) { const item = document.createElement("button"); item.type = "button"; item.className = `mention-option${index === state.activeMentionIndex ? " active" : ""}`; item.innerHTML = ` @${escapeHtml(option.agent_id)} · ${escapeHtml(option.name)} ${escapeHtml(option.description || "")} `; item.addEventListener("mousedown", (event) => { event.preventDefault(); chooseMention(option); }); els.mentionMenu.appendChild(item); } els.mentionMenu.hidden = false; } function renderAgents() { els.agentList.innerHTML = ""; for (const agent of state.agents) { const item = document.createElement("button"); item.type = "button"; item.className = "agent-item"; item.innerHTML = `