${escapeHtml(item.title)}
+ function renderQuickActions(links) { + const html = links.map((link) => { + const external = String(link.url || "").startsWith("http"); + return ` + ${escapeHtml(link.label || "入口")} + ${escapeHtml(link.description || "")} + `; + }).join(""); + setHtml("quick-actions", html); + } + + function renderModuleCards(modules) { + const html = modules.map((item) => ` +
+
${escapeHtml(item.title)}
+ ${pill(item.enabled ? "启用" : "关闭", item.enabled ? "ok" : "warn")} +${escapeHtml(item.description || "")}
-
-
+ ${escapeHtml(item.severity === "ok" ? "OK" : item.severity === "action" ? "ACTION" : "WARN")}
+
`).join("");
+ setHtml("ai-insight-list", html);
}
- function renderErrors(errors) {
- if (!els["error-panel"]) return;
- const entries = Object.entries(errors || {});
- els["error-panel"].hidden = entries.length === 0;
- els["error-panel"].innerHTML = entries.map(([key, value]) => `
+ ${escapeHtml(key)}
+ ${escapeHtml(item.status || "unknown")}
+ ${escapeHtml(summarizeObject(item.detail || {}))}
+
+ `).join("");
+ setHtml("health-grid", healthHtml || empty("暂无健康检查数据"));
+
+ const functions = ((data.functions || {}).functions || []).slice(0, 20);
+ const fnHtml = functions.map((item) => `
+
+ `;
}
- function initSpringMotion() {
- const stage = document.querySelector(".spring-stage");
- if (!stage) return;
+ function styleReviewHtml(item) {
+ return `
+ `;
+ }
+
+ function jargonReviewHtml(item) {
+ return `
+ `;
+ }
+
+ function renderPersona(data) {
+ const current = data.current || {};
+ const persona = current.persona || {};
+ setHtml("persona-state-stats", statCards([
+ ["提示词字数", current.prompt_length],
+ ["开场对话", current.begin_dialog_count],
+ ["工具数量", current.tool_count],
+ ["当前群组", current.group_id || "default"],
+ ]));
+ setText("persona-prompt-preview", current.prompt_preview || persona.system_prompt || persona.prompt || "暂无人格提示词");
+
+ const personas = data.personas || [];
+ setHtml("persona-list", personas.map((item) => {
+ const id = item.persona_id || item.id || item.name;
+ return `
+
+ `).join("") || empty("暂无学习内容"));
+
+ const batches = ((data.batches || {}).batches || []);
+ setHtml("batch-list", batches.map((item) => `
+
+ `;
+ }
+
+ function renderSettings(data) {
+ const schema = data.schema || {};
+ const groups = schema.groups || [];
+ if (!state.settingsGroup && groups.length) state.settingsGroup = groups[0].key;
+ setHtml("settings-groups", groups.map((group) => `
+
+ `).join("") || empty("配置 schema 暂不可用"));
+
+ const active = groups.find((group) => group.key === state.settingsGroup) || groups[0] || { fields: [] };
+ setHtml("config-form", (active.fields || []).map(fieldHtml).join("") || empty("请选择配置分组"));
+ renderPipMirrors(data.pip_mirrors || {});
+ }
+
+ function fieldHtml(field) {
+ const value = state.dirtySettings.has(field.key) ? state.dirtySettings.get(field.key) : field.value;
+ const common = `data-config-field="${escapeAttr(field.key)}" data-config-type="${escapeAttr(field.type)}" ${field.editable ? "" : "disabled"}`;
+ let control = "";
+ if (field.widget === "toggle") {
+ control = ``;
+ } else if (field.widget === "select" || field.widget === "provider") {
+ const options = field.options || [];
+ control = ``;
+ } else if (field.widget === "textarea" || field.type === "list") {
+ const textValue = Array.isArray(value) ? value.join("\n") : value ?? "";
+ control = ``;
+ } else {
+ const inputType = field.widget === "number" || field.type === "int" || field.type === "float" ? "number" : "text";
+ const step = field.type === "float" ? "0.01" : "1";
+ control = ``;
+ }
+ return ``;
+ }
+
+ function renderPipMirrors(mirrors) {
+ const select = $("pip-mirror-select");
+ if (!select || select.childElementCount) return;
+ select.innerHTML = Object.entries(mirrors).map(([key, item]) => ``).join("");
+ }
+
+ function statCards(items) {
+ return items.map(([label, value]) => `
+ ${escapeHtml(label)}
+ ${escapeHtml(fmt(value, typeof value === "number" ? 1 : 0))}
+ `).join("");
+ }
+
+ function renderGenericBarChart(id, items) {
+ const maxValue = Math.max(1, ...items.map((item) => Number(item.metric || 0)));
+ setHtml(id, items.map((item) => {
+ const value = Math.max(4, Math.min(100, Number(item.metric || 0) / maxValue * 100));
+ return `
-
-
-
- ${item.enabled ? "启用" : "关闭"}
+ ${escapeHtml(fmt(item.metric))}
- ${escapeHtml(item.metric_label || "")}
-
+ ${escapeHtml(fmt(item.metric))}
+ ${escapeHtml(item.metric_label || "")}
`).join("");
-
- els["module-list"].querySelectorAll(".module-card").forEach((card) => {
- card.addEventListener("click", () => activateTarget(card.dataset.target || "overview"));
- });
+ setHtml("module-card-grid", html || empty());
}
- function renderCharts(modules) {
- if (!els["module-chart"]) return;
+ function renderModuleChart(modules) {
const maxValue = Math.max(1, ...modules.map((item) => Number(item.metric || 0)));
- els["module-chart"].innerHTML = modules.map((item) => {
+ const html = modules.map((item) => {
const value = Math.max(4, Math.min(100, (Number(item.metric || 0) / maxValue) * 100));
- return `
-
- ${escapeHtml(item.title)}
-
- `;
+ return `
- ${escapeHtml(fmt(item.metric))}
-
+ ${escapeHtml(item.title)}
+
`;
}).join("");
+ setHtml("module-chart", html || empty());
}
- function renderMetrics(metrics) {
- const rawScore = Number(metrics.overall_score || 0);
- const normalized = rawScore <= 1 ? rawScore * 100 : rawScore;
- const score = Math.max(0, Math.min(100, normalized));
- if (els["intelligence-ring"]) {
- els["intelligence-ring"].style.setProperty("--value", String(score));
- }
+ function renderIntelligence(metrics) {
+ const score = normalizeScore(metrics.overall_score);
+ $("intelligence-ring")?.style.setProperty("--value", String(score));
setText("intelligence-score", fmt(score));
- const dimensions = metrics.dimensions && typeof metrics.dimensions === "object"
+ const dimCount = metrics.dimensions && typeof metrics.dimensions === "object"
? Object.keys(metrics.dimensions).length
: 0;
- setText("metrics-summary", dimensions ? `已有 ${dimensions} 个维度参与评估。` : "智能指标服务暂未产生维度数据。");
- }
-
- function renderQuickLinks(links) {
- if (!els["quick-entry-list"]) return;
- els["quick-entry-list"].innerHTML = links.map((link) => `
-
-
- ${escapeHtml(link.label || "入口")}
- ${escapeHtml(link.description || "")}
-
- ›
-
+ setText("metrics-summary", dimCount ? `已有 ${dimCount} 个维度参与评估。` : "智能指标服务暂未产生维度数据。");
+ }
+
+ function buildInsights(data) {
+ const overview = data.overview || {};
+ const reviews = data.reviews || {};
+ const monitoring = data.monitoring || {};
+ const integrations = data.integrations || {};
+ const errors = data.errors || {};
+ const items = [];
+ const push = (severity, title, detail, target) => items.push({ severity, title, detail, target });
+
+ if ((overview.runtime || {}).database_degraded) {
+ push("warn", "数据库处于降级状态", (overview.runtime || {}).database_error || "数据库服务未完整启动。", "monitoring");
+ }
+ const pendingPersona = ((reviews.persona_pending || {}).updates || []).length;
+ const pendingStyle = ((reviews.style_reviews || {}).reviews || []).length;
+ const pendingJargon = (((reviews.jargon_pending || {}).jargon_list) || []).length;
+ const totalBacklog = pendingPersona + pendingStyle + pendingJargon;
+ if (totalBacklog > 0) {
+ push("action", "审查队列有积压", `当前有 ${totalBacklog} 条学习结果等待确认。`, "reviews");
+ }
+ const score = normalizeScore(((overview.metrics || {}).overall_score));
+ if (score > 0 && score < 60) {
+ push("warn", "智能评分偏低", `综合评分 ${fmt(score)},建议查看表达样本和学习批次。`, "metrics");
+ }
+ const health = (monitoring.health || {}).overall;
+ if (health && health !== "healthy") {
+ push("warn", "健康检查提示异常", `当前健康状态为 ${health}。`, "monitoring");
+ }
+ const delegation = integrations.delegation || {};
+ if (delegation.memory_delegated || delegation.reply_delegated) {
+ push("ok", "伴随插件委托已启用", `记忆委托: ${delegation.memory_delegated ? "是" : "否"},回复委托: ${delegation.reply_delegated ? "是" : "否"}。`, "integrations");
+ }
+ Object.entries(errors).forEach(([key, value]) => {
+ push("warn", `模块 ${key} 读取失败`, String(value), "monitoring");
+ });
+ if (!items.length) {
+ push("ok", "暂无高优先级问题", "核心学习、审查和监控模块均已返回可用数据。", "home");
+ }
+ return items;
+ }
+
+ function renderInsights(data) {
+ const insights = buildInsights(data || state.dashboard || {});
+ const html = insights.map((item) => `
+
+ ${escapeHtml(fmt(item.metric))}
+
${escapeHtml(item.title)}
+${escapeHtml(item.detail)}
+ ${button("前往", `data-route-card="${escapeAttr(item.target)}"`)} +${escapeHtml(key)}: ${escapeHtml(value)}
`).join(""); + function renderMonitoring(data) { + const health = data.health || {}; + const checks = health.checks || {}; + const healthHtml = Object.entries(checks).map(([key, item]) => ` +
+ ${escapeHtml(shortName(item.name))}
+ ${escapeHtml(fmt((item.duration || {}).avg || 0, 4))}s
+ ${escapeHtml(fmt(item.calls || 0, 0))} calls
+
+ `).join("");
+ setHtml("function-list", fnHtml || empty((data.functions || {}).debug_mode ? "暂无函数性能数据" : "debug_mode 未启用"));
+ showErrors(data.errors || {});
}
- function activateTarget(target) {
- state.activeTarget = target || "overview";
- document.querySelectorAll(".module-tab").forEach((tab) => {
- tab.classList.toggle("active", tab.dataset.target === state.activeTarget);
- });
- document.querySelectorAll(".module-card").forEach((card) => {
- card.classList.toggle("active", card.dataset.target === state.activeTarget);
- });
+ async function loadJargon(force) {
+ const confirmed = $("jargon-confirmed")?.value || "";
+ const filter = $("jargon-filter")?.value || "";
+ const keyword = $("jargon-keyword")?.value || "";
+ const params = { page: 1, page_size: 30 };
+ if (confirmed) params.confirmed = confirmed;
+ if (filter) params.filter = filter;
+ if (keyword) params.keyword = keyword;
+ const data = await cached(`jargon:${JSON.stringify(params)}`, () => apiGet("jargon", params), force);
+ renderJargon(data);
+ }
- const module = (state.data && (state.data.modules || []).find((item) => item.target === state.activeTarget)) || null;
- setText("detail-kicker", state.activeTarget === "overview" ? "Overview" : state.activeTarget);
- setText("detail-title", module ? module.title : "模块状态");
+ function renderJargon(data) {
+ const stats = data.stats || {};
+ setHtml("jargon-stat-grid", statCards([
+ ["候选词", stats.total_candidates],
+ ["已确认", stats.confirmed_jargon],
+ ["推断完成", stats.completed_inference],
+ ["活跃群组", stats.active_groups],
+ ]));
+ const items = ((data.list || {}).jargon_list || []);
+ const html = items.map((item) => `
+
+
+ `).join("");
+ setHtml("jargon-list", html || empty("暂无黑话数据"));
+ state.pageData.lastJargonItems = items;
+ showErrors(data.errors || {});
}
- function escapeHtml(value) {
- return String(value ?? "")
- .replace(/&/g, "&")
- .replace(//g, ">")
- .replace(/"/g, """)
- .replace(/'/g, "'");
+ function renderStyle(data) {
+ const stats = ((data.results || {}).statistics) || {};
+ setHtml("style-stat-grid", statCards([
+ ["风格样本", stats.unique_styles || stats.total_samples],
+ ["平均置信度", stats.avg_confidence],
+ ["总样本", stats.total_samples],
+ ["最近更新", stats.latest_update ? "有" : "无"],
+ ]));
+ const patterns = data.patterns || {};
+ const patternGroups = [
+ ["情绪模式", patterns.emotion_patterns || []],
+ ["语言模式", patterns.language_patterns || []],
+ ["话题模式", patterns.topic_patterns || []],
+ ];
+ setHtml("style-pattern-columns", patternGroups.map(([title, list]) => `
+
+ ${escapeHtml(item.term || item.content || `#${item.id}`)}
+ ${escapeHtml(item.meaning || item.definition || "暂无释义")}
+
+ ${escapeHtml(item.group_id || "global")}
+ ${pill(item.is_confirmed ? "已确认" : "待确认", item.is_confirmed ? "ok" : "warn")}
+ ${pill(item.is_global ? "全局" : "本地")}
+
+ ${button("编辑", `data-jargon-action="edit" data-id="${escapeAttr(item.id)}"`)}
+ ${button("确认", `data-jargon-action="approve" data-id="${escapeAttr(item.id)}"`)}
+ ${button("驳回", `data-jargon-action="reject" data-id="${escapeAttr(item.id)}"`)}
+ ${button(item.is_global ? "取消全局" : "设为全局", `data-jargon-action="toggle_global" data-id="${escapeAttr(item.id)}"`)}
+ ${button("删除", `data-jargon-action="delete" data-id="${escapeAttr(item.id)}"`, "danger-button")}
+
+
+
+ `).join(""));
+ const chartItems = patternGroups.map(([title, list]) => ({ title, metric: (list || []).length, accent: "#4169e1" }));
+ renderGenericBarChart("style-pattern-chart", chartItems);
+ const reviews = ((data.reviews || {}).reviews || []);
+ setHtml("expression-review-list", reviews.map((item) => styleReviewHtml(item)).join("") || empty("暂无表达审查"));
}
- function escapeAttr(value) {
- return escapeHtml(value).replace(/`/g, "`");
+ function renderReviews(data) {
+ const personaPending = ((data.persona_pending || {}).updates || []);
+ const personaReviewed = ((data.persona_reviewed || {}).updates || []);
+ const styleReviews = ((data.style_reviews || {}).reviews || []);
+ const pendingJargon = (((data.jargon_pending || {}).jargon_list) || []);
+ setText("persona-review-count", fmt(personaPending.length, 0));
+ setText("style-review-count", fmt(styleReviews.length, 0));
+ setText("jargon-review-count", fmt(pendingJargon.length, 0));
+ setText("reviewed-count", fmt(personaReviewed.length, 0));
+ setHtml("persona-review-list", personaPending.map((item) => personaReviewHtml(item)).join("") || empty("暂无人格更新"));
+ setHtml("style-review-list", styleReviews.map((item) => styleReviewHtml(item)).join("") || empty("暂无表达审查"));
+ setHtml("jargon-review-list", pendingJargon.map((item) => jargonReviewHtml(item)).join("") || empty("暂无黑话候选"));
+ setHtml("reviewed-persona-list", personaReviewed.slice(0, 12).map((item) => `
+ ${escapeHtml(title)}
+ ${(list || []).slice(0, 12).map((item) => `${escapeHtml(item.name || item.pattern || item.text || "")}`).join("") || empty("暂无模式")} +
+ ${escapeHtml(item.id)}
+ ${escapeHtml(item.status || item.review_status || "reviewed")}
+ ${escapeHtml(item.reason || item.update_type || item.review_source || "")}
+
+ `).join("") || empty("暂无已审查记录"));
+ showErrors(data.errors || {});
}
- function bindEvents() {
- if (els["refresh-button"]) {
- els["refresh-button"].addEventListener("click", loadOverview);
- }
- document.querySelectorAll(".module-tab").forEach((tab) => {
- tab.addEventListener("click", () => activateTarget(tab.dataset.target || "overview"));
- });
- initSpringMotion();
+ function personaReviewHtml(item) {
+ const id = item.id;
+ return `
+ ${escapeHtml(item.update_type || item.review_source || "人格更新")}
+ ${escapeHtml(item.group_id || "default")} · ${escapeHtml(item.reason || item.description || "")}
+
+ ${escapeHtml(item.proposed_content || item.new_content || item.incremental_content || "").slice(0, 220)}
+
+ ${button("详情", `data-review-action="detail" data-kind="persona" data-id="${escapeAttr(id)}"`)}
+ ${button("批准", `data-review-action="approve" data-kind="persona" data-id="${escapeAttr(id)}"`, "solid-button")}
+ ${button("拒绝", `data-review-action="reject" data-kind="persona" data-id="${escapeAttr(id)}"`)}
+ ${button("删除", `data-review-action="delete" data-kind="persona" data-id="${escapeAttr(id)}"`, "danger-button")}
+
+
+ ${escapeHtml(item.description || "表达方式学习")}
+ ${escapeHtml(item.group_id || "default")} · ${escapeHtml(item.status || "pending")}
+
+ ${escapeHtml(item.few_shots_content || item.learned_patterns || "").slice(0, 220)}
+
+ ${button("详情", `data-review-action="detail" data-kind="style" data-id="${escapeAttr(item.id)}"`)}
+ ${button("批准", `data-review-action="approve" data-kind="style" data-id="${escapeAttr(item.id)}"`, "solid-button")}
+ ${button("拒绝", `data-review-action="reject" data-kind="style" data-id="${escapeAttr(item.id)}"`)}
+
+
+ ${escapeHtml(item.term || item.content || `#${item.id}`)}
+ ${escapeHtml(item.group_id || "global")} · ${escapeHtml(fmt(item.occurrences || item.count, 0))} 次
+
+ ${escapeHtml(item.meaning || item.definition || item.review_detail || "暂无释义")}
+
+ ${button("确认", `data-review-action="approve" data-kind="jargon" data-id="${escapeAttr(item.id)}"`, "solid-button")}
+ ${button("驳回", `data-review-action="reject" data-kind="jargon" data-id="${escapeAttr(item.id)}"`)}
+ ${button("删除", `data-review-action="delete" data-kind="jargon" data-id="${escapeAttr(item.id)}"`, "danger-button")}
+
+
+ ${escapeHtml(id)}
+ ${escapeHtml(item.name || id)}
+
`;
+ }).join("") || empty("暂无人格列表"));
+
+ const backups = ((data.backups || {}).backups || []);
+ setText("persona-backup-count", fmt(backups.length, 0));
+ setHtml("persona-backup-list", backups.map((item) => `
+
+ ${button("导出", `data-persona-action="export" data-persona-id="${escapeAttr(id)}"`)}
+ ${button("删除", `data-persona-action="delete" data-persona-id="${escapeAttr(id)}"`, "danger-button")}
+
+
+
+ `).join("") || empty("暂无人格备份"));
+ showErrors(data.errors || {});
+ }
+
+ function renderContent(data) {
+ const content = data.content || {};
+ const items = content[state.contentType] || [];
+ qsa("#content-tabs button").forEach((btn) => btn.classList.toggle("active", btn.dataset.contentType === state.contentType));
+ setHtml("learning-content-list", items.map((item) => `
+
+ ${escapeHtml(item.backup_name || `备份 ${item.id}`)}
+ ${escapeHtml(item.reason_short || item.reason || "无备注")}
+
+ ${escapeHtml(item.group_id || "default")}
+ ${escapeHtml(item.timestamp || item.created_at || "")}
+
+ ${button("查看", `data-persona-action="backup_detail" data-id="${escapeAttr(item.id)}" data-group-id="${escapeAttr(item.group_id || "")}"`)}
+ ${button("恢复", `data-persona-action="backup_restore" data-id="${escapeAttr(item.id)}" data-group-id="${escapeAttr(item.group_id || "")}"`, "solid-button")}
+ ${button("删除", `data-persona-action="backup_delete" data-id="${escapeAttr(item.id)}" data-group-id="${escapeAttr(item.group_id || "")}"`, "danger-button")}
+
+
+ ${escapeHtml(item.title || item.type || `#${item.id}`)}
+ ${escapeHtml(item.timestamp || "")} ${escapeHtml(item.metadata || "")}
+
+ ${button("删除", `data-content-action="delete_content" data-bucket="${escapeAttr(state.contentType)}" data-id="${escapeAttr(item.id)}"`, "danger-button")}
+ ${escapeHtml(item.text || item.detail || "").slice(0, 360)}
+
+ ${escapeHtml(item.batch_name || item.batch_id || item.id)}
+ ${escapeHtml(item.status || (item.success ? "success" : "unknown"))}
+ ${escapeHtml(fmt(item.quality_score || 0, 3))}
+ ${button("删除", `data-content-action="delete_batch" data-id="${escapeAttr(item.id)}"`, "danger-button")}
+
+ `).join("") || empty("暂无批次历史"));
+ showErrors(data.errors || {});
+ }
- physics.points = Array.from(stage.querySelectorAll(".spring-node:not(.node-core)")).map((el, index) => ({
- el,
- x: 0,
- y: 0,
+ async function loadGraphs(force) {
+ const type = $("graph-type")?.value || "memory";
+ state.graph.type = type;
+ const data = await cached(`graphs:${type}`, () => apiGet("graphs", { type, limit: 140 }), force);
+ renderGraphs(data);
+ }
+
+ function renderGraphs(data) {
+ const graph = data[state.graph.type] || data.memory || data.knowledge || {};
+ const canvas = $("graph-canvas");
+ const size = canvas
+ ? syncGraphCanvasSize(canvas, { force: true })
+ : { width: 960, height: 520 };
+ const rawNodes = Array.isArray(graph.nodes) ? graph.nodes : [];
+ state.graph.nodes = rawNodes.map((node, index) =>
+ createGraphNode(node, index, rawNodes.length, size.width, size.height),
+ );
+ state.graph.links = normalizeGraphLinks(graph.links || []);
+ state.graph.dragged = null;
+ state.graph.hovered = null;
+ setHtml("graph-stat-grid", statCards([
+ ["节点", (graph.stats || {}).nodes || state.graph.nodes.length],
+ ["连线", (graph.stats || {}).links || state.graph.links.length],
+ ["群组", (graph.stats || {}).groups || (graph.groups || []).length],
+ ["来源", graph.data_source || "self_learning"],
+ ]));
+ setHtml("graph-node-list", state.graph.nodes.slice(0, 18).map((node) => `
+
+ ${escapeHtml(node.name || node.id)}
+ ${escapeHtml(node.category_name || node.category || "节点")}
+ ${escapeHtml(node.detail || "")}
+
+ `).join("") || empty("暂无图谱节点"));
+ startGraphRender();
+ }
+
+ function createGraphNode(node, index, total, width, height) {
+ const id = graphValueKey(node.id ?? node.name ?? node.label ?? `${state.graph.type}-${index}`);
+ const radius = graphNodeRadius(node);
+ const safeWidth = Math.max(320, width || 960);
+ const safeHeight = Math.max(320, height || 520);
+ const centerX = safeWidth / 2;
+ const centerY = safeHeight / 2;
+ const spread = Math.max(64, Math.min(safeWidth, safeHeight) * 0.38);
+ const angle = index * 2.399963229728653 + (state.graph.type === "knowledge" ? 0.72 : 0);
+ const ring = Math.sqrt((index + 0.5) / Math.max(1, total));
+ const x = centerX + Math.cos(angle) * spread * ring;
+ const y = centerY + Math.sin(angle) * spread * ring;
+ const min = radius + GRAPH_SAFE_PADDING;
+ const maxX = safeWidth - radius - GRAPH_SAFE_PADDING;
+ const maxY = safeHeight - radius - GRAPH_SAFE_PADDING;
+ return {
+ ...node,
+ id,
+ label: node.name || node.label || id,
+ radius,
+ x: clamp(x, min, maxX),
+ y: clamp(y, min, maxY),
+ homeX: clamp(x, min, maxX),
+ homeY: clamp(y, min, maxY),
vx: 0,
vy: 0,
- seed: index * 2.1,
- }));
- if (!physics.points.length) return;
+ pinned: false,
+ };
+ }
+
+ function normalizeGraphLinks(links) {
+ if (!Array.isArray(links)) return [];
+ return links.map((link) => ({
+ ...link,
+ source: graphValueKey(link.source ?? link.from),
+ target: graphValueKey(link.target ?? link.to),
+ })).filter((link) => link.source && link.target);
+ }
+
+ function renderReplyStrategy(data) {
+ const cards = (data.dashboards || []).filter((item) => item.id === "group_chat_plus");
+ setHtml("reply-strategy-cards", cards.map(integrationCardHtml).join("") || empty("未检测到 Group Chat Plus"));
+ }
+
+ function renderIntegrations(data) {
+ setHtml("integration-cards", (data.dashboards || []).map(integrationCardHtml).join("") || empty("暂无融合状态"));
+ const settings = data.settings || {};
+ setHtml("integration-settings", Object.entries(settings).map(([key, value]) => `
+
+ ${escapeHtml(key)}
+ ${escapeHtml(value === true ? "开启" : value === false ? "关闭" : value ?? "未设置")}
+
+ `).join("") || empty("暂无融合设置"));
+ }
+
+ function integrationCardHtml(item) {
+ const dash = item.dashboard || {};
+ const url = dash.external_url || dash.official_page_url || dash.url || "#";
+ const disabled = !dash.available || !url;
+ return `
+ ${escapeHtml(item.role || "")}
+
+ ${escapeHtml(dash.label || "打开")}
+ ${escapeHtml((item.dev_api || {}).mode || "")}
+ ${escapeHtml(item.title || item.id)}
+${escapeHtml(item.delegated ? "已委托" : item.active ? "可用" : "未启用")}
+
+ ${escapeHtml(item.title)}
+
`;
+ }).join("") || empty());
+ }
+
+ function summarizeObject(obj) {
+ const entries = Object.entries(obj || {}).slice(0, 3);
+ return entries.map(([key, value]) => `${key}: ${typeof value === "object" ? JSON.stringify(value) : value}`).join(" · ");
+ }
+
+ function shortName(name) {
+ const text = String(name || "");
+ return text.length > 58 ? `...${text.slice(-55)}` : text;
+ }
+
+ function findReviewItem(kind, id) {
+ const reviews = state.pageData.reviews || {};
+ const style = state.pageData.style || {};
+ if (kind === "persona") return ((reviews.persona_pending || {}).updates || []).find((item) => String(item.id) === String(id));
+ if (kind === "style") {
+ return (
+ ((reviews.style_reviews || {}).reviews || []).find((item) => String(item.id) === String(id))
+ || ((style.reviews || {}).reviews || []).find((item) => String(item.id) === String(id))
+ );
+ }
+ return (((reviews.jargon_pending || {}).jargon_list || []).find((item) => String(item.id) === String(id)));
+ }
+
+ async function handleReviewAction(kind, id, action) {
+ if (action === "detail") {
+ showModal("审查详情", `
+ ${escapeHtml(fmt(item.metric, 0))}
+
${escapeHtml(JSON.stringify(findReviewItem(kind, id) || {}, null, 2))}`);
+ return;
+ }
+ let payload;
+ if (kind === "persona") {
+ payload = action === "delete"
+ ? { action: "delete", id }
+ : { action: "review", id, decision: action };
+ } else if (kind === "style") {
+ payload = { action: `style_${action}`, id };
+ } else {
+ payload = { action: `jargon_${action}`, id };
+ }
+ const result = await apiPost("reviews/action", payload);
+ showToast(result.message || "操作完成", result.success ? "ok" : "error");
+ state.pageData.reviews = null;
+ await loadPageData(state.page, { force: true });
+ }
+
+ async function handleJargonAction(action, id) {
+ if (action === "edit") {
+ const item = (state.pageData.lastJargonItems || []).find((entry) => String(entry.id) === String(id)) || {};
+ showModal("编辑黑话", `
+
+
+
+ `);
+ return;
+ }
+ const result = await apiPost("jargon/action", { action, id });
+ showToast(result.message || "操作完成", result.success ? "ok" : "error");
+ state.pageData = {};
+ await loadPageData(state.page, { force: true });
+ }
+
+ async function handlePersonaAction(buttonEl) {
+ const action = buttonEl.dataset.personaAction;
+ const body = {
+ action,
+ id: buttonEl.dataset.id,
+ group_id: buttonEl.dataset.groupId,
+ persona_id: buttonEl.dataset.personaId,
+ };
+ const result = await apiPost("persona/action", body);
+ if (action === "backup_detail" || action === "export") {
+ showModal(action === "export" ? "人格导出" : "备份详情", `${escapeHtml(JSON.stringify(result.persona || result.backup || result, null, 2))}`);
+ return;
+ }
+ showToast(result.message || "操作完成", result.success ? "ok" : "error");
+ state.pageData.persona = null;
+ await loadPageData("persona-learning", { force: true });
+ }
+
+ async function handleContentAction(buttonEl) {
+ const result = await apiPost("content/action", {
+ action: buttonEl.dataset.contentAction,
+ bucket: buttonEl.dataset.bucket,
+ id: buttonEl.dataset.id,
+ });
+ showToast(result.message || "操作完成", result.success ? "ok" : "error");
+ state.pageData.content = null;
+ await loadPageData("content", { force: true });
+ }
+
+ function collectConfigPayload() {
+ const payload = Object.fromEntries(state.dirtySettings.entries());
+ qsa("[data-config-field]").forEach((field) => {
+ const key = field.dataset.configField;
+ const type = field.dataset.configType;
+ let value;
+ if (field.type === "checkbox") value = field.checked;
+ else if (type === "int") value = Number.parseInt(field.value || "0", 10);
+ else if (type === "float") value = Number.parseFloat(field.value || "0");
+ else if (type === "list") {
+ const raw = field.value.trim();
+ try {
+ value = raw.startsWith("[") ? JSON.parse(raw) : raw.split(/\n+/).map((line) => line.trim()).filter(Boolean);
+ } catch (_) {
+ value = raw.split(/\n+/).map((line) => line.trim()).filter(Boolean);
+ }
+ } else value = field.value;
+ payload[key] = value;
+ });
+ return payload;
+ }
+ function bindEvents() {
+ $("refresh-button")?.addEventListener("click", () => loadPageData(state.page, { force: true }));
+ $("modal-close")?.addEventListener("click", closeModal);
+ $("jargon-search-button")?.addEventListener("click", () => {
+ Object.keys(state.pageData).filter((key) => key.startsWith("jargon:")).forEach((key) => delete state.pageData[key]);
+ loadJargon(true);
+ });
+ $("copy-insight-context")?.addEventListener("click", async () => {
+ const text = JSON.stringify(state.dashboard || {}, null, 2);
+ try {
+ await navigator.clipboard.writeText(text);
+ showToast("巡检上下文已复制");
+ } catch (_) {
+ showModal("巡检上下文", `${escapeHtml(text)}`);
+ }
+ });
+ $("relearn-button")?.addEventListener("click", async () => {
+ const result = await apiPost("content/action", { action: "relearn", group_id: "default" });
+ showToast(result.message || "重新学习已提交", result.success ? "ok" : "error");
+ });
+ $("graph-type")?.addEventListener("change", () => loadGraphs(true));
+ $("config-save-button")?.addEventListener("click", async () => {
+ const result = await apiPost("settings/action", { action: "save", config: collectConfigPayload() });
+ showToast(result.message || "设置已保存", result.success ? "ok" : "error");
+ state.pageData.settings = null;
+ await loadPageData("settings", { force: true });
+ });
+ $("dependency-install-button")?.addEventListener("click", async () => {
+ const result = await apiPost("settings/action", {
+ action: "install_dependencies",
+ manual_confirmed: true,
+ source: "system_settings",
+ tier: $("dependency-tier")?.value || "full",
+ pip_mirror: $("pip-mirror-select")?.value || "default",
+ });
+ setText("dependency-output", (result.result || result).output || result.message || "");
+ showToast(result.message || "依赖安装任务结束", result.success ? "ok" : "error");
+ });
+
+ document.addEventListener("click", async (event) => {
+ const target = event.target.closest("[data-route-card],[data-refresh-page],[data-review-action],[data-jargon-action],[data-persona-action],[data-content-action],[data-settings-group]");
+ if (!target) return;
+ if (target.dataset.routeCard) navigateToPage(target.dataset.routeCard);
+ if (target.dataset.refreshPage) loadPageData(target.dataset.refreshPage, { force: true });
+ if (target.dataset.reviewAction) await handleReviewAction(target.dataset.kind, target.dataset.id, target.dataset.reviewAction);
+ if (target.dataset.jargonAction) await handleJargonAction(target.dataset.jargonAction, target.dataset.id);
+ if (target.dataset.personaAction) await handlePersonaAction(target);
+ if (target.dataset.contentAction) await handleContentAction(target);
+ if (target.dataset.settingsGroup) {
+ state.settingsGroup = target.dataset.settingsGroup;
+ renderSettings(state.pageData.settings || {});
+ }
+ });
+
+ document.addEventListener("change", (event) => {
+ const field = event.target.closest("[data-config-field]");
+ if (!field) return;
+ state.dirtySettings.set(field.dataset.configField, field.type === "checkbox" ? field.checked : field.value);
+ });
+
+ document.addEventListener("click", async (event) => {
+ const save = event.target.closest("#modal-jargon-save");
+ if (!save) return;
+ const result = await apiPost("jargon/action", {
+ action: "update",
+ id: save.dataset.id,
+ content: $("modal-jargon-content")?.value,
+ meaning: $("modal-jargon-meaning")?.value,
+ });
+ closeModal();
+ showToast(result.message || "黑话已更新", result.success ? "ok" : "error");
+ state.pageData = {};
+ await loadPageData("jargon-learning", { force: true });
+ });
+
+ qsa(".nav-item").forEach((item) => {
+ item.addEventListener("click", (event) => {
+ event.preventDefault();
+ navigateToPage(item.dataset.page || "home");
+ });
+ });
+ qsa("#content-tabs button").forEach((buttonEl) => {
+ buttonEl.addEventListener("click", () => {
+ state.contentType = buttonEl.dataset.contentType || "dialogues";
+ renderContent(state.pageData.content || {});
+ });
+ });
+ window.addEventListener("hashchange", () => navigateToPage(resolvePageFromHash(), { skipHash: true }));
+ }
+
+ function setThemeFromBridge() {
+ try {
+ const bridge = window.AstrBotPluginPage;
+ const apply = (ctx) => {
+ if (ctx && typeof ctx.isDark === "boolean") {
+ document.documentElement.setAttribute("data-theme", ctx.isDark ? "dark" : "light");
+ }
+ };
+ apply(bridge && bridge.getContext && bridge.getContext());
+ if (bridge && bridge.onContextChange) bridge.onContextChange(apply);
+ if (bridge && bridge.onContext) bridge.onContext(apply);
+ } catch (_) {}
+ }
+
+ function initSpringMotion() {
+ const stage = qs(".spring-stage");
+ const canvas = $("physics-canvas");
+ if (!stage || !canvas) return;
+ const ctx = canvas.getContext("2d");
+ if (!ctx) return;
+ const resize = () => {
+ const rect = stage.getBoundingClientRect();
+ canvas.width = Math.max(1, Math.floor(rect.width * devicePixelRatio));
+ canvas.height = Math.max(1, Math.floor(rect.height * devicePixelRatio));
+ canvas.style.width = `${rect.width}px`;
+ canvas.style.height = `${rect.height}px`;
+ ctx.setTransform(devicePixelRatio, 0, 0, devicePixelRatio, 0, 0);
+ };
+ resize();
+ window.addEventListener("resize", resize);
+ physics.particles = qsa(".spring-node:not(.node-core)", stage).map((el, index) => ({
+ el, x: 0, y: 0, vx: 0, vy: 0, seed: index * 2.3,
+ }));
stage.addEventListener("pointermove", (event) => {
const rect = stage.getBoundingClientRect();
physics.pointer.x = event.clientX - rect.left;
physics.pointer.y = event.clientY - rect.top;
physics.pointer.active = true;
});
- stage.addEventListener("pointerleave", () => {
- physics.pointer.active = false;
- });
-
+ stage.addEventListener("pointerleave", () => { physics.pointer.active = false; });
if (!physics.running) {
physics.running = true;
physics.last = performance.now();
@@ -289,50 +1046,289 @@
}
function tickSpringMotion(now) {
- if (!physics.running) return;
+ const stage = qs(".spring-stage");
+ const canvas = $("physics-canvas");
+ if (!stage || !canvas) return;
+ const ctx = canvas.getContext("2d");
+ const rect = stage.getBoundingClientRect();
const dt = Math.min(0.033, Math.max(0.001, (now - physics.last) / 1000));
physics.last = now;
+ ctx.clearRect(0, 0, rect.width, rect.height);
+ ctx.strokeStyle = "rgba(65, 105, 225, 0.22)";
+ ctx.lineWidth = 1.5;
- physics.points.forEach((point) => {
- const rect = point.el.parentElement.getBoundingClientRect();
+ const core = { x: rect.width / 2, y: rect.height / 2 };
+ physics.particles.forEach((point) => {
const own = point.el.getBoundingClientRect();
- const breatheX = Math.sin(now / 1200 + point.seed) * 14;
- const breatheY = Math.cos(now / 1350 + point.seed) * 12;
- let targetX = breatheX;
- let targetY = breatheY;
-
+ const baseX = own.left - rect.left + own.width / 2 - point.x;
+ const baseY = own.top - rect.top + own.height / 2 - point.y;
+ let targetX = Math.sin(now / 1100 + point.seed) * 16;
+ let targetY = Math.cos(now / 1250 + point.seed) * 14;
if (physics.pointer.active) {
- const cx = own.left - rect.left + own.width / 2 + point.x;
- const cy = own.top - rect.top + own.height / 2 + point.y;
+ const cx = baseX + point.x;
+ const cy = baseY + point.y;
const dx = cx - physics.pointer.x;
const dy = cy - physics.pointer.y;
- const dist = Math.max(1, Math.sqrt(dx * dx + dy * dy));
- const force = Math.max(0, 110 - dist) / 110;
- targetX += (dx / dist) * force * 46;
- targetY += (dy / dist) * force * 46;
+ const dist = Math.max(1, Math.hypot(dx, dy));
+ const force = Math.max(0, 120 - dist) / 120;
+ targetX += dx / dist * force * 52;
+ targetY += dy / dist * force * 52;
}
-
- const spring = 46;
- const damping = 12;
- point.vx += (targetX - point.x) * spring * dt;
- point.vy += (targetY - point.y) * spring * dt;
- point.vx *= Math.max(0, 1 - damping * dt);
- point.vy *= Math.max(0, 1 - damping * dt);
+ point.vx += (targetX - point.x) * 42 * dt;
+ point.vy += (targetY - point.y) * 42 * dt;
+ point.vx *= Math.max(0, 1 - 12 * dt);
+ point.vy *= Math.max(0, 1 - 12 * dt);
point.x += point.vx * dt * 60;
point.y += point.vy * dt * 60;
+ const px = baseX + point.x;
+ const py = baseY + point.y;
+ ctx.beginPath();
+ ctx.moveTo(core.x, core.y);
+ ctx.quadraticCurveTo((core.x + px) / 2, (core.y + py) / 2 - 16, px, py);
+ ctx.stroke();
+ point.el.style.transform = `translate3d(${point.x.toFixed(2)}px, ${point.y.toFixed(2)}px, 0)`;
+ });
+ requestAnimationFrame(tickSpringMotion);
+ }
+
+ function startGraphRender() {
+ const canvas = $("graph-canvas");
+ if (!canvas) return;
+ bindGraphCanvas(canvas);
+ syncGraphCanvasSize(canvas, { force: true });
+ if (!state.graph.running) {
+ state.graph.running = true;
+ requestAnimationFrame(tickGraph);
+ }
+ }
+
+ function bindGraphCanvas(canvas) {
+ if (state.graph.canvasBound) return;
+ state.graph.canvasBound = true;
- const scale = 1 + Math.min(0.12, Math.hypot(point.vx, point.vy) / 900);
- point.el.style.transform = `translate3d(${point.x.toFixed(2)}px, ${point.y.toFixed(2)}px, 0) scale(${scale.toFixed(3)})`;
+ canvas.addEventListener("pointerdown", (event) => {
+ const point = graphPointer(event, canvas);
+ const node = hitGraphNode(point.x, point.y);
+ if (!node) return;
+ event.preventDefault();
+ canvas.setPointerCapture?.(event.pointerId);
+ node.pinned = true;
+ node.vx = 0;
+ node.vy = 0;
+ state.graph.dragged = {
+ node,
+ pointerId: event.pointerId,
+ offsetX: node.x - point.x,
+ offsetY: node.y - point.y,
+ };
+ canvas.classList.add("is-dragging");
});
- requestAnimationFrame(tickSpringMotion);
+ canvas.addEventListener("pointermove", (event) => {
+ const point = graphPointer(event, canvas);
+ const drag = state.graph.dragged;
+ if (drag && drag.pointerId === event.pointerId) {
+ const min = drag.node.radius + GRAPH_SAFE_PADDING;
+ drag.node.x = clamp(point.x + drag.offsetX, min, state.graph.width - min);
+ drag.node.y = clamp(point.y + drag.offsetY, min, state.graph.height - min);
+ drag.node.homeX = drag.node.x;
+ drag.node.homeY = drag.node.y;
+ drag.node.vx = 0;
+ drag.node.vy = 0;
+ event.preventDefault();
+ return;
+ }
+ state.graph.hovered = hitGraphNode(point.x, point.y);
+ canvas.classList.toggle("has-hover", Boolean(state.graph.hovered));
+ });
+
+ const releaseDrag = (event) => {
+ const drag = state.graph.dragged;
+ if (drag && drag.pointerId === event.pointerId) {
+ drag.node.vx = 0;
+ drag.node.vy = 0;
+ state.graph.dragged = null;
+ canvas.classList.remove("is-dragging");
+ canvas.releasePointerCapture?.(event.pointerId);
+ }
+ };
+ canvas.addEventListener("pointerup", releaseDrag);
+ canvas.addEventListener("pointercancel", releaseDrag);
+ canvas.addEventListener("pointerleave", () => {
+ state.graph.hovered = null;
+ canvas.classList.remove("has-hover");
+ });
+
+ window.addEventListener("resize", () => {
+ syncGraphCanvasSize(canvas, { force: true });
+ });
+ }
+
+ function tickGraph() {
+ const canvas = $("graph-canvas");
+ if (!canvas) {
+ state.graph.running = false;
+ return;
+ }
+ const ctx = canvas.getContext("2d");
+ const { width, height, ratio } = syncGraphCanvasSize(canvas);
+ const nodes = state.graph.nodes;
+ const links = state.graph.links;
+ ctx.setTransform(ratio, 0, 0, ratio, 0, 0);
+ ctx.clearRect(0, 0, width, height);
+ const byId = new Map(nodes.map((node) => [String(node.id), node]));
+
+ links.slice(0, 260).forEach((link) => {
+ const source = byId.get(String(link.source));
+ const target = byId.get(String(link.target));
+ if (!source || !target) return;
+ const dx = target.x - source.x;
+ const dy = target.y - source.y;
+ const dist = Math.max(1, Math.hypot(dx, dy));
+ const desired = Math.max(98, Math.min(168, width * 0.18));
+ const force = (dist - desired) * 0.00032;
+ if (!source.pinned) {
+ source.vx += dx * force;
+ source.vy += dy * force;
+ }
+ if (!target.pinned) {
+ target.vx -= dx * force;
+ target.vy -= dy * force;
+ }
+ ctx.strokeStyle = "rgba(100, 116, 139, 0.28)";
+ ctx.lineWidth = Math.max(1, Math.min(4, Number(link.value || 1)));
+ ctx.beginPath();
+ ctx.moveTo(source.x, source.y);
+ ctx.lineTo(target.x, target.y);
+ ctx.stroke();
+ });
+
+ for (let i = 0; i < nodes.length; i += 1) {
+ for (let j = i + 1; j < Math.min(nodes.length, i + 45); j += 1) {
+ const a = nodes[i];
+ const b = nodes[j];
+ const dx = b.x - a.x;
+ const dy = b.y - a.y;
+ const dist = Math.max(18, Math.hypot(dx, dy));
+ const repel = Math.min(0.2, 260 / (dist * dist));
+ if (!a.pinned) {
+ a.vx -= dx * repel;
+ a.vy -= dy * repel;
+ }
+ if (!b.pinned) {
+ b.vx += dx * repel;
+ b.vy += dy * repel;
+ }
+ }
+ }
+
+ nodes.forEach((node, index) => {
+ const cx = width / 2 + Math.sin(index) * 30;
+ const cy = height / 2 + Math.cos(index) * 24;
+ if (!node.pinned) {
+ node.vx += ((node.homeX || cx) - node.x) * 0.0011 + (cx - node.x) * 0.00012;
+ node.vy += ((node.homeY || cy) - node.y) * 0.0011 + (cy - node.y) * 0.00012;
+ node.vx *= 0.86;
+ node.vy *= 0.86;
+ node.x += node.vx;
+ node.y += node.vy;
+ }
+ const radius = node.radius || graphNodeRadius(node);
+ const min = radius + GRAPH_SAFE_PADDING;
+ node.x = clamp(node.x, min, width - min);
+ node.y = clamp(node.y, min, height - min);
+ const isHovered = state.graph.hovered === node || state.graph.dragged?.node === node;
+ if (isHovered) {
+ ctx.fillStyle = "rgba(15, 159, 143, 0.14)";
+ ctx.beginPath();
+ ctx.arc(node.x, node.y, radius + 10, 0, Math.PI * 2);
+ ctx.fill();
+ }
+ ctx.fillStyle = node.source === "livingmemory" ? "#0f9f8f" : index % 3 === 0 ? "#4169e1" : index % 3 === 1 ? "#d97706" : "#e11d48";
+ ctx.beginPath();
+ ctx.arc(node.x, node.y, isHovered ? radius + 2 : radius, 0, Math.PI * 2);
+ ctx.fill();
+ ctx.fillStyle = getComputedStyle(document.documentElement).getPropertyValue("--text").trim() || "#162033";
+ ctx.font = "12px system-ui";
+ ctx.fillText(String(node.name || "").slice(0, 12), node.x + radius + 4, node.y + 4);
+ });
+ requestAnimationFrame(tickGraph);
+ }
+
+ function syncGraphCanvasSize(canvas, options = {}) {
+ const rect = canvas.getBoundingClientRect();
+ const width = Math.max(320, Math.floor(rect.width || canvas.clientWidth || state.graph.width || 960));
+ const height = Math.max(320, Math.floor(rect.height || canvas.clientHeight || state.graph.height || 520));
+ const ratio = Math.max(1, Math.min(2, window.devicePixelRatio || 1));
+ const nextWidth = Math.floor(width * ratio);
+ const nextHeight = Math.floor(height * ratio);
+ const resized = canvas.width !== nextWidth || canvas.height !== nextHeight;
+ if (resized || options.force) {
+ const oldWidth = state.graph.width || width;
+ const oldHeight = state.graph.height || height;
+ canvas.width = nextWidth;
+ canvas.height = nextHeight;
+ state.graph.nodes.forEach((node) => {
+ const min = node.radius + GRAPH_SAFE_PADDING;
+ node.x = clamp((node.x / oldWidth) * width, min, width - min);
+ node.y = clamp((node.y / oldHeight) * height, min, height - min);
+ node.homeX = clamp(((node.homeX || node.x) / oldWidth) * width, min, width - min);
+ node.homeY = clamp(((node.homeY || node.y) / oldHeight) * height, min, height - min);
+ });
+ }
+ state.graph.width = width;
+ state.graph.height = height;
+ return { width, height, ratio };
+ }
+
+ function graphPointer(event, canvas) {
+ const rect = canvas.getBoundingClientRect();
+ return {
+ x: clamp(event.clientX - rect.left, 0, state.graph.width || rect.width),
+ y: clamp(event.clientY - rect.top, 0, state.graph.height || rect.height),
+ };
+ }
+
+ function hitGraphNode(x, y) {
+ for (let index = state.graph.nodes.length - 1; index >= 0; index -= 1) {
+ const node = state.graph.nodes[index];
+ const radius = (node.radius || graphNodeRadius(node)) + 8;
+ if (Math.hypot(node.x - x, node.y - y) <= radius) {
+ return node;
+ }
+ }
+ return null;
+ }
+
+ function graphNodeRadius(node) {
+ const raw = Number(node.symbolSize || node.value || node.weight || 12);
+ return Math.max(9, Math.min(24, Number.isFinite(raw) ? raw : 12));
+ }
+
+ function graphValueKey(value) {
+ if (value && typeof value === "object") {
+ return String(value.id ?? value.name ?? value.label ?? "");
+ }
+ return String(value ?? "");
+ }
+
+ function clamp(value, min, max) {
+ if (max < min) return min;
+ return Math.max(min, Math.min(max, value));
}
async function init() {
- collectElements();
setThemeFromBridge();
bindEvents();
- await loadOverview();
+ initSpringMotion();
+ try {
+ await bridgeReady();
+ navigateToPage(resolvePageFromHash(), { skipHash: true, force: true });
+ } catch (error) {
+ showToast(error.message || String(error), "error");
+ setText("runtime-status", "桥接失败");
+ setText("runtime-summary", error.message || String(error));
+ }
}
if (document.readyState === "loading") {
diff --git a/pages/dashboard/index.html b/pages/dashboard/index.html
index d06070c2..817008d1 100644
--- a/pages/dashboard/index.html
+++ b/pages/dashboard/index.html
@@ -8,93 +8,388 @@
-
-
+
AstrBot Embedded WebUI
-