>(`/api/dashboard/emotion/effects/${item.id}`);
- },
-
- Detail: EmotionDetail,
-
- formatters: {
- score: (value: unknown) => _score(value),
- delta: (value: unknown) => _delta(value),
- "mono-time": (value: unknown) => _shortTs(value),
- },
-});
diff --git a/plugin.py b/plugin.py
index 88cf96f..6860524 100644
--- a/plugin.py
+++ b/plugin.py
@@ -40,6 +40,12 @@
workspace_roots = ("emotion",)
drift_skill_roots = ("drift/skills",)
dashboard_module = "dashboard.py"
+web_module = "web_module.js"
+web_requires = ("workbench.panels.v1",)
+web_provides = ()
+web_contract_digests = {
+ "workbench.panels.v1": "724b282c22c4b3f3a36967ab664c4dfd8bce4257665f99459000306938caf527",
+}
_v3_emotion_root: Path | None = None
_v3_emotion_runtime: EmotionRuntime | None = None
diff --git a/tests/test_plugin.py b/tests/test_plugin.py
index 0e3da11..c082333 100644
--- a/tests/test_plugin.py
+++ b/tests/test_plugin.py
@@ -129,6 +129,7 @@ async def _mount_candidate(
inject=module.inject,
runtime=PluginRuntime(
plugin_id="emotion",
+ generation_id=root.generation_id,
plugin_dir=Path(__file__).parents[1],
data_dir=tmp_path / "plugin-data",
workspace=emotion_root.parent,
@@ -168,8 +169,6 @@ def _before_turn(channel: str, at: datetime) -> BeforeTurnCtx:
chat_id="chat",
content="tick",
timestamp=at,
- retrieved_memory_block="",
- retrieval_trace_raw=None,
history_messages=(),
)
@@ -832,6 +831,7 @@ async def mount_emotion() -> None:
inject=module.inject,
runtime=PluginRuntime(
plugin_id="emotion",
+ generation_id=root.generation_id,
plugin_dir=Path(__file__).parents[1],
data_dir=tmp_path / "plugin-data" / "emotion",
workspace=emotion_root.parent,
diff --git a/web_module.css b/web_module.css
new file mode 100644
index 0000000..0b82c53
--- /dev/null
+++ b/web_module.css
@@ -0,0 +1,26 @@
+.emotion-workbench-panel { max-width: 1180px; margin-inline: auto; }
+.emotion-workbench-panel > header { display: flex; align-items: end; justify-content: space-between; gap: 20px; }
+.emotion-workbench-panel h1, .emotion-workbench-panel h2, .emotion-workbench-panel h3, .emotion-workbench-panel p { margin: 0; }
+.emotion-workbench-panel > header p, .emotion-panel-row span, .emotion-panel-row small, .emotion-workbench-panel article > header span { color: var(--ak-ink-secondary); }
+.emotion-workbench-panel button { min-height: 44px; }
+.emotion-overview { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 1px; margin-block: 16px; background: var(--ak-rule-subtle); }
+.emotion-overview > div, .emotion-overview > p { margin: 0; padding: 14px; background: var(--ak-paper-quiet); }
+.emotion-overview span, .emotion-overview small { display: block; color: var(--ak-ink-secondary); font-size: 0.78rem; }
+.emotion-overview strong { display: block; margin-block: 4px; }
+.emotion-panel-grid { display: grid; grid-template-columns: minmax(260px, 0.8fr) minmax(340px, 1.2fr); gap: 24px; margin-block-start: 16px; }
+.emotion-panel-row { display: grid; width: 100%; gap: 4px; border: 0; border-block-end: 1px solid var(--ak-rule-subtle); padding: 12px; color: inherit; background: transparent; text-align: start; cursor: pointer; }
+@media (hover: hover) { .emotion-panel-row:hover { background: var(--ak-paper-quiet); } }
+.emotion-panel-grid footer { display: flex; align-items: center; justify-content: center; gap: 12px; padding-block: 16px; }
+.emotion-panel-grid > article { min-width: 0; border-inline-start: 1px solid var(--ak-rule-subtle); padding-inline-start: 24px; }
+.emotion-panel-grid article > header { display: flex; align-items: start; justify-content: space-between; gap: 16px; }
+.emotion-panel-grid article > header p { color: var(--ak-ink-secondary); font-size: 0.8rem; }
+.emotion-detail-metrics, .emotion-detail-coordinates { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; }
+.emotion-detail-metrics div, .emotion-detail-coordinates div { padding: 12px; background: var(--ak-paper-quiet); }
+.emotion-detail-metrics dt, .emotion-detail-coordinates dt { color: var(--ak-ink-secondary); font-size: 0.8rem; }
+.emotion-detail-metrics dd, .emotion-detail-coordinates dd { margin: 4px 0 0; font-family: ui-monospace, monospace; }
+.emotion-panel-grid article > section { margin-block: 18px; }
+.emotion-panel-grid details { margin-block-start: 12px; border-block-start: 1px solid var(--ak-rule-subtle); padding-block-start: 12px; }
+.emotion-panel-grid summary { min-height: 44px; cursor: pointer; }
+.emotion-panel-grid pre { overflow: auto; white-space: pre-wrap; }
+.emotion-workbench-panel :focus-visible { outline: 2px solid var(--ak-rule-focus); outline-offset: 2px; }
+@media (max-width: 820px) { .emotion-workbench-panel > header { align-items: start; flex-direction: column; } .emotion-overview, .emotion-detail-metrics, .emotion-detail-coordinates, .emotion-panel-grid { grid-template-columns: 1fr; } .emotion-panel-grid > article { border-inline-start: 0; padding-inline-start: 0; } }
diff --git a/web_module.js b/web_module.js
new file mode 100644
index 0000000..8935bce
--- /dev/null
+++ b/web_module.js
@@ -0,0 +1,175 @@
+export function activate(ctx) {
+ return ctx.ui.inject("workbench.panels.v1", (mount) => mount.register({
+ id: "emotion-decisions",
+ label: "情绪决策",
+ order: 40,
+ render(host) {
+ const panel = document.createElement("section");
+ panel.className = "emotion-workbench-panel";
+ panel.innerHTML = ``;
+ host.replaceChildren(panel);
+ const overview = panel.querySelector("[data-overview]");
+ const refresh = panel.querySelector("[data-refresh]");
+ const status = panel.querySelector("[data-status]");
+ const list = panel.querySelector("[data-list]");
+ const detail = panel.querySelector("[data-detail]");
+ const pageText = panel.querySelector("[data-page]");
+ const previous = panel.querySelector("[data-previous]");
+ const next = panel.querySelector("[data-next]");
+ let page = 1;
+ let total = 0;
+ let disposed = false;
+ let overviewRequest = new AbortController();
+ let listRequest = new AbortController();
+ let detailRequest = new AbortController();
+
+ const loadOverview = async () => {
+ overviewRequest.abort();
+ overviewRequest = new AbortController();
+ const request = overviewRequest;
+ overview.textContent = "正在读取当前状态…";
+ try {
+ const data = await json(ctx, "/api/dashboard/emotion/overview", request.signal);
+ if (disposed || request.signal.aborted) return;
+ overview.innerHTML = renderOverview(data);
+ } catch (reason) {
+ if (!disposed && !request.signal.aborted) showError(overview, reason);
+ }
+ };
+
+ const loadList = async () => {
+ listRequest.abort();
+ listRequest = new AbortController();
+ const request = listRequest;
+ const requestedPage = page;
+ status.textContent = "正在读取情绪影响…";
+ try {
+ const data = await json(ctx, `/api/dashboard/emotion/effects?page=${requestedPage}&page_size=25`, request.signal);
+ if (disposed || request.signal.aborted) return;
+ total = finiteNumber(data.total);
+ renderRows(list, data.items, openDetail);
+ const pages = Math.max(1, Math.ceil(total / 25));
+ pageText.textContent = `${requestedPage} / ${pages}`;
+ previous.disabled = requestedPage <= 1;
+ next.disabled = requestedPage >= pages;
+ status.textContent = total ? `共 ${total} 条情绪影响` : "还没有改变主动决策的情绪影响。";
+ } catch (reason) {
+ if (!disposed && !request.signal.aborted) showError(status, reason);
+ }
+ };
+
+ const openDetail = async (effectId) => {
+ detailRequest.abort();
+ detailRequest = new AbortController();
+ const request = detailRequest;
+ detail.innerHTML = "正在读取详情…
";
+ try {
+ const item = await json(ctx, `/api/dashboard/emotion/effects/${encodeURIComponent(effectId)}`, request.signal);
+ if (disposed || request.signal.aborted) return;
+ detail.innerHTML = renderDetail(item);
+ } catch (reason) {
+ if (!disposed && !request.signal.aborted) showError(detail, reason);
+ }
+ };
+
+ refresh.addEventListener("click", () => {
+ void loadOverview();
+ void loadList();
+ });
+ previous.addEventListener("click", () => {
+ if (page > 1) {
+ page -= 1;
+ void loadList();
+ }
+ });
+ next.addEventListener("click", () => {
+ if (page * 25 < total) {
+ page += 1;
+ void loadList();
+ }
+ });
+ void loadOverview();
+ void loadList();
+ return () => {
+ disposed = true;
+ overviewRequest.abort();
+ listRequest.abort();
+ detailRequest.abort();
+ host.replaceChildren();
+ };
+ },
+ }));
+}
+
+function renderOverview(data) {
+ const state = data && typeof data.state === "object" && data.state ? data.state : null;
+ const behavior = data && typeof data.current_behavior === "object" && data.current_behavior
+ ? data.current_behavior
+ : null;
+ if (!state || !behavior) {
+ return `还没有可用的情绪状态。已记录 ${finiteNumber(data && data.effect_count)} 条情绪影响。
`;
+ }
+ return `当前语气${escapeHtml(behavior.tone_label || "未标注")}
发送阈值${escapeHtml(effectLabel(behavior.expected_effect))}${escapeHtml(deltaText(behavior.threshold_delta))}
情绪坐标愉悦 ${escapeHtml(score(state.valence))} · 唤醒 ${escapeHtml(score(state.arousal))} · 支配 ${escapeHtml(score(state.dominance))}
`;
+}
+
+function renderRows(target, items, openDetail) {
+ target.replaceChildren();
+ if (!Array.isArray(items) || !items.length) {
+ target.innerHTML = "没有可展示的情绪影响。
";
+ return;
+ }
+ for (const item of items) {
+ const button = document.createElement("button");
+ button.type = "button";
+ button.className = "emotion-panel-row";
+ button.innerHTML = `${escapeHtml(effectLabel(item.expected_effect))}${escapeHtml(shortTime(item.created_at))} · ${escapeHtml(String(item.tone_label || "未标注"))}愉悦 ${escapeHtml(score(item.valence))} · 唤醒 ${escapeHtml(score(item.arousal))} · 阈值 ${escapeHtml(deltaText(item.threshold_delta))}`;
+ button.addEventListener("click", () => void openDetail(item.id));
+ target.append(button);
+ }
+}
+
+function renderDetail(item) {
+ const delta = deltaText(item.threshold_delta);
+ return `主动决策输入
这次情绪如何改变发送阈值
${escapeHtml(String(item.tick_id || "未关联任务"))} ${escapeHtml(effectLabel(item.expected_effect))}- 原始阈值
- ${escapeHtml(score(item.base_threshold))}
- 应用情绪后
- ${escapeHtml(score(item.final_threshold))}
- 变化
- ${escapeHtml(delta)}
情绪坐标
语气:${escapeHtml(String(item.tone_label || "未标注"))}
- 愉悦度
- ${escapeHtml(score(item.valence))}
- 唤醒度
- ${escapeHtml(score(item.arousal))}
- 支配度
- ${escapeHtml(score(item.dominance))}
查看写入主动流程的提示词
${escapeHtml(String(item.prompt_section || "-"))}查看技术元数据
${escapeHtml(JSON.stringify(item.metadata || {}, null, 2))} `;
+}
+
+async function json(ctx, path, signal) {
+ const response = await ctx.http.request(path, {method: "GET", signal});
+ const body = await response.json();
+ if (!response.ok) throw new Error(body?.detail || body?.message || `HTTP ${response.status}`);
+ return body;
+}
+
+function effectLabel(value) {
+ return ({raise_send_bar: "提高发送阈值", lower_send_bar: "降低发送阈值"})[value] || String(value || "-");
+}
+
+function score(value) {
+ const number = Number(value);
+ return Number.isFinite(number) ? number.toFixed(3) : "-";
+}
+
+function deltaText(value) {
+ const number = Number(value);
+ if (!Number.isFinite(number)) return "-";
+ return number > 0 ? `+${number.toFixed(3)}` : number.toFixed(3);
+}
+
+function finiteNumber(value) {
+ const number = Number(value);
+ return Number.isFinite(number) ? number : 0;
+}
+
+function shortTime(value) {
+ const date = new Date(String(value || ""));
+ return Number.isNaN(date.getTime()) ? "-" : new Intl.DateTimeFormat("zh-CN", {month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", hour12: false}).format(date);
+}
+
+function showError(target, reason) {
+ target.setAttribute("role", "alert");
+ target.textContent = reason instanceof Error ? reason.message : String(reason);
+}
+
+function escapeHtml(value) {
+ return String(value).replace(/[&<>"']/g, (character) => ({"&": "&", "<": "<", ">": ">", '"': """, "'": "'"})[character]);
+}
From d4589dbd56aa420a17eaa7011f02f827cfe05c4a Mon Sep 17 00:00:00 2001
From: huashen <2494946808@qq.com>
Date: Sun, 30 Aug 2026 08:35:19 +0800
Subject: [PATCH 3/9] ci: validate Web UI composition
---
.github/workflows/plugin-api-v3.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/plugin-api-v3.yml b/.github/workflows/plugin-api-v3.yml
index b4ae1de..d1e0bcf 100644
--- a/.github/workflows/plugin-api-v3.yml
+++ b/.github/workflows/plugin-api-v3.yml
@@ -36,7 +36,7 @@ jobs:
- uses: actions/checkout@v4
with:
repository: kachofugetsu09/akashic-agent
- ref: 9da3a988a2bf62b0f550bd4f6bb98c4eeb1f56f5
+ ref: 14221ff778d660ab0e9c9249a8a8ff772861dbe9
path: .akashic-core
- uses: actions/checkout@v4
with:
From 282159cd31f1395474de08d593cd546290cf7f22 Mon Sep 17 00:00:00 2001
From: huashen <2494946808@qq.com>
Date: Sun, 30 Aug 2026 08:41:49 +0800
Subject: [PATCH 4/9] ci: follow final Core head
---
.github/workflows/plugin-api-v3.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/plugin-api-v3.yml b/.github/workflows/plugin-api-v3.yml
index d1e0bcf..1ff50b4 100644
--- a/.github/workflows/plugin-api-v3.yml
+++ b/.github/workflows/plugin-api-v3.yml
@@ -36,7 +36,7 @@ jobs:
- uses: actions/checkout@v4
with:
repository: kachofugetsu09/akashic-agent
- ref: 14221ff778d660ab0e9c9249a8a8ff772861dbe9
+ ref: a4e87422a2448c4b148b03777da349b51c77ed16
path: .akashic-core
- uses: actions/checkout@v4
with:
From 4519d717baa3ae037c2129994ffe42bb4ac08a1b Mon Sep 17 00:00:00 2001
From: huashen <2494946808@qq.com>
Date: Sun, 30 Aug 2026 08:47:28 +0800
Subject: [PATCH 5/9] ci: validate final Core release
---
.github/workflows/plugin-api-v3.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/plugin-api-v3.yml b/.github/workflows/plugin-api-v3.yml
index 1ff50b4..d4119e5 100644
--- a/.github/workflows/plugin-api-v3.yml
+++ b/.github/workflows/plugin-api-v3.yml
@@ -36,7 +36,7 @@ jobs:
- uses: actions/checkout@v4
with:
repository: kachofugetsu09/akashic-agent
- ref: a4e87422a2448c4b148b03777da349b51c77ed16
+ ref: f72b9e2ce2031133462e68aeaa0379fa883daeb2
path: .akashic-core
- uses: actions/checkout@v4
with:
From 800544b8a8b2053ce96ddb505cc808f59b3a957e Mon Sep 17 00:00:00 2001
From: huashen <2494946808@qq.com>
Date: Sun, 30 Aug 2026 09:00:07 +0800
Subject: [PATCH 6/9] ci: validate deployed Core release
---
.github/workflows/plugin-api-v3.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/plugin-api-v3.yml b/.github/workflows/plugin-api-v3.yml
index d4119e5..796f4dc 100644
--- a/.github/workflows/plugin-api-v3.yml
+++ b/.github/workflows/plugin-api-v3.yml
@@ -36,7 +36,7 @@ jobs:
- uses: actions/checkout@v4
with:
repository: kachofugetsu09/akashic-agent
- ref: f72b9e2ce2031133462e68aeaa0379fa883daeb2
+ ref: 69a9616f6f48f19dc109a6b3eef8fc1000825829
path: .akashic-core
- uses: actions/checkout@v4
with:
From 60c7a216b82857ae5c6a8f0d3f116acb22cf70a2 Mon Sep 17 00:00:00 2001
From: huashen <2494946808@qq.com>
Date: Sun, 30 Aug 2026 19:17:44 +0800
Subject: [PATCH 7/9] fix(web-ui): compose emotion panel through workbench
---
akashic.plugin.toml | 2 +-
dashboard_panel.css | 185 +++++++++++++++++++++++++
dashboard_panel.tsx | 189 ++++++++++++++++++++++++++
plugin.py | 6 +-
web_module.css | 221 ++++++++++++++++++++++++++----
web_module.js | 321 ++++++++++++++++++++++----------------------
6 files changed, 730 insertions(+), 194 deletions(-)
create mode 100644 dashboard_panel.css
create mode 100644 dashboard_panel.tsx
diff --git a/akashic.plugin.toml b/akashic.plugin.toml
index 572c99e..d6cba39 100644
--- a/akashic.plugin.toml
+++ b/akashic.plugin.toml
@@ -1,5 +1,5 @@
schema_version = 1
name = "emotion"
-version = "3.0.3"
+version = "3.0.4"
api_version = 3
entrypoint = "plugin.py"
diff --git a/dashboard_panel.css b/dashboard_panel.css
new file mode 100644
index 0000000..5c7a0cc
--- /dev/null
+++ b/dashboard_panel.css
@@ -0,0 +1,185 @@
+.emotion-empty {
+ padding: 24px 16px;
+}
+
+.emotion-empty__title {
+ color: var(--ak-color-text-primary);
+ font-size: 20px;
+ font-weight: 600;
+ letter-spacing: -0.025em;
+ line-height: 1.25;
+}
+
+.emotion-empty__text {
+ margin-block-start: 8px;
+ color: var(--ak-color-text-secondary);
+ font-size: 13px;
+ line-height: 1.625;
+}
+
+.emotion-detail {
+ display: grid;
+ gap: 18px;
+ padding: 24px;
+ color: var(--ak-color-text-primary);
+}
+
+.emotion-detail__header,
+.emotion-section-heading {
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ gap: 16px;
+}
+
+.emotion-detail__header p,
+.emotion-section-heading p {
+ margin: 0 0 4px;
+ color: var(--ak-color-action-primary);
+ font-size: 11px;
+ font-weight: 700;
+ letter-spacing: 0.08em;
+}
+
+.emotion-detail__header h2,
+.emotion-section-heading h3 {
+ margin: 0;
+ letter-spacing: -0.02em;
+}
+
+.emotion-detail__header h2 { font-size: 20px; }
+.emotion-section-heading h3 { font-size: 16px; }
+
+.emotion-detail__header span,
+.emotion-section-heading > span {
+ color: var(--ak-color-text-muted);
+ font-size: 12px;
+}
+
+.emotion-detail__header > div > span {
+ display: block;
+ margin-block-start: 5px;
+}
+
+.emotion-threshold {
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr) minmax(92px, auto);
+ align-items: center;
+ gap: 14px;
+ padding: 16px;
+ border: 1px solid var(--ak-color-border-default);
+ border-radius: 7px;
+ background: var(--ak-color-bg-surface-low);
+}
+
+.emotion-threshold > div {
+ display: grid;
+ gap: 4px;
+}
+
+.emotion-threshold span { color: var(--ak-color-text-muted); font-size: 11px; }
+.emotion-threshold strong { font-family: var(--mono); font-size: 22px; }
+.emotion-threshold__arrow { font-size: 20px !important; }
+
+.emotion-threshold__delta {
+ padding-inline-start: 14px;
+ border-inline-start: 1px solid var(--ak-color-border-default);
+}
+
+.emotion-threshold__delta.is-up strong { color: var(--ak-color-status-warning); }
+.emotion-threshold__delta.is-down strong { color: var(--ak-color-status-success); }
+
+.emotion-coordinates {
+ display: grid;
+ gap: 16px;
+ padding-block: 4px;
+}
+
+.emotion-gauge {
+ display: grid;
+ grid-template-columns: 88px minmax(0, 1fr);
+ gap: 4px 14px;
+ align-items: center;
+}
+
+.emotion-gauge__label {
+ display: flex;
+ justify-content: space-between;
+ gap: 8px;
+ font-size: 12px;
+}
+
+.emotion-gauge__label code { color: var(--ak-color-text-muted); }
+
+.emotion-gauge__track {
+ position: relative;
+ height: 6px;
+ border-radius: 999px;
+ background: linear-gradient(90deg, var(--ak-color-bg-surface-high), var(--ak-color-action-soft));
+}
+
+.emotion-gauge__track::after {
+ position: absolute;
+ inset-block: -3px;
+ left: 50%;
+ width: 1px;
+ content: "";
+ background: var(--ak-color-border-strong);
+}
+
+.emotion-gauge__track i {
+ position: absolute;
+ top: 50%;
+ width: 12px;
+ height: 12px;
+ border: 2px solid var(--ak-color-bg-canvas);
+ border-radius: 50%;
+ background: var(--ak-color-action-primary);
+ box-shadow: 0 0 0 1px var(--ak-color-action-primary);
+ transform: translate(-50%, -50%);
+}
+
+.emotion-gauge__ends {
+ display: flex;
+ grid-column: 2;
+ justify-content: space-between;
+ color: var(--ak-color-text-muted);
+ font-size: 10px;
+}
+
+.emotion-disclosure {
+ border-block-start: 1px solid var(--ak-color-border-default);
+}
+
+.emotion-disclosure summary {
+ padding: 14px 2px;
+ color: var(--ak-color-text-primary);
+ font-size: 13px;
+ font-weight: 650;
+ cursor: pointer;
+}
+
+.emotion-disclosure summary:focus-visible {
+ outline: 2px solid var(--ak-color-action-primary);
+ outline-offset: 2px;
+}
+
+.emotion-disclosure pre {
+ overflow: auto;
+ max-height: 320px;
+ margin: 0 0 14px;
+ padding: 14px;
+ border-radius: 5px;
+ color: var(--ak-color-text-secondary);
+ background: var(--ak-color-bg-surface-low);
+ font: 12px/1.65 var(--mono);
+ white-space: pre-wrap;
+}
+
+@media (max-width: 720px) {
+ .emotion-detail { padding: 18px; }
+ .emotion-detail__header { align-items: flex-start; flex-direction: column; }
+ .emotion-threshold { grid-template-columns: 1fr auto 1fr; }
+ .emotion-threshold__delta { grid-column: 1 / -1; padding: 12px 0 0; border-inline-start: 0; border-block-start: 1px solid var(--ak-color-border-default); }
+ .emotion-gauge { grid-template-columns: 78px minmax(0, 1fr); }
+}
diff --git a/dashboard_panel.tsx b/dashboard_panel.tsx
new file mode 100644
index 0000000..ee0a74d
--- /dev/null
+++ b/dashboard_panel.tsx
@@ -0,0 +1,189 @@
+import { type ReactElement } from "react";
+import { Chip, chipClass } from "@akashic/dashboard-ui";
+import "./dashboard_panel.css";
+import type { WebHostContextV1, WebUiDisposer } from "@akashic/web-ui-v1";
+import type { WorkbenchPanelEntry } from "@akashic/workbench-ui-v2";
+
+let dashboardRequest: WebHostContextV1["http"]["request"] | null = null;
+
+async function api(path: string, init?: RequestInit): Promise {
+ if (!dashboardRequest) throw new Error("Emotion 工作台面板未激活");
+ const response = await dashboardRequest(path, init);
+ const body = await response.json() as T & { detail?: unknown; message?: unknown };
+ if (!response.ok) throw new Error(String(body.detail ?? body.message ?? `HTTP ${response.status}`));
+ return body;
+}
+
+interface Overview {
+ state: Record | null;
+ effect_count: number;
+}
+
+interface FetchPage {
+ items: Record[];
+ total: number;
+}
+
+function _score(value: unknown): string {
+ return typeof value === "number" ? value.toFixed(3) : "-";
+}
+
+function _delta(value: unknown): string {
+ if (typeof value !== "number") return "-";
+ return value > 0 ? `+${value.toFixed(3)}` : value.toFixed(3);
+}
+
+function _shortTs(value: unknown): string {
+ const text = String(value || "");
+ if (!text) return "-";
+ const d = new Date(text);
+ if (Number.isNaN(d.getTime())) return text;
+ return `${d.getMonth() + 1}-${String(d.getDate()).padStart(2, "0")} ${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
+}
+
+function _escape(value: unknown): string {
+ return String(value ?? "")
+ .replaceAll("&", "&")
+ .replaceAll("<", "<")
+ .replaceAll(">", ">")
+ .replaceAll('"', """)
+ .replaceAll("'", "'");
+}
+
+function _effectLabel(value: unknown): string {
+ const effect = String(value || "");
+ if (effect === "raise_send_bar") return "提高发送阈值";
+ if (effect === "lower_send_bar") return "降低发送阈值";
+ return effect || "-";
+}
+
+function _toneCell(value: unknown): string {
+ const text = String(value || "-");
+ const tone = text === "raise_send_bar" ? "warning" : text === "lower_send_bar" ? "success" : "muted";
+ return `${_escape(_effectLabel(text))}`;
+}
+
+function EmotionDetail(props: { item: Record | null }): ReactElement {
+ const item = props.item;
+ if (!item) {
+ return 情绪影响详情
选择一条记录,查看这次主动任务的情绪影响。
;
+ }
+ const delta = typeof item.threshold_delta === "number" ? item.threshold_delta : null;
+ return (
+
+
+
+
主动决策输入
+
这次情绪如何改变发送阈值
+
{String(item.tick_id || "未关联任务")}
+
+ {_effectLabel(item.expected_effect)}
+
+
+
+ 原始阈值{_score(item.base_threshold)}
+ →
+ 应用情绪后{_score(item.final_threshold)}
+ 0 ? " is-up" : " is-down"}`}>
+ 变化{_delta(delta)}
+
+
+
+
+
+
+
语气:{String(item.tone_label || "未标注")}
+
+
+
+
+
+
+
+
+
+ );
+}
+
+function VadGauge(props: { label: string; low: string; high: string; value: unknown }): ReactElement {
+ const numeric = typeof props.value === "number" ? props.value : 0;
+ const position = Math.max(0, Math.min(100, ((numeric + 1) / 2) * 100));
+ return (
+
+
{props.label}{_score(props.value)}
+
+
{props.low}{props.high}
+
+ );
+}
+
+function TextDisclosure(props: { title: string; text: string }): ReactElement {
+ return (
+
+ {props.title}
+ {props.text || "-"}
+
+ );
+}
+
+const panel = {
+ id: "emotion",
+ label: "情绪决策",
+ viewLabel: "情绪决策",
+ order: 30,
+ pageSize: 50,
+ rowKey: "id",
+
+ countTitle(total: number): string {
+ return `共 ${total} 条情绪影响`;
+ },
+
+ columns: [
+ { key: "created_at", label: "时间", width: 96, fmt: "mono-time", cellClass: "mono cell-time", rawTitle: true },
+ { key: "expected_effect", label: "影响", width: 132, renderCell: _toneCell },
+ { key: "tone_label", label: "语气", width: 112 },
+ { key: "valence", label: "愉悦", width: 66, fmt: "score", cellClass: "mono cell-metric", align: "right" },
+ { key: "arousal", label: "唤醒", width: 66, fmt: "score", cellClass: "mono cell-metric", align: "right" },
+ { key: "threshold_delta", label: "阈值变化", width: 82, fmt: "delta", cellClass: "mono cell-metric", align: "right" },
+ { key: "tick_id", label: "任务", flex: true, cellClass: "mono content-preview", rawTitle: true },
+ ],
+
+ async getCount({ signal }: { signal: AbortSignal }): Promise {
+ try {
+ const overview = await api("/api/dashboard/emotion/overview", { signal });
+ return overview.effect_count || 0;
+ } catch (error) {
+ if (signal.aborted) throw error;
+ return null;
+ }
+ },
+
+ async fetchPage({ page, pageSize, signal }: { page: number; pageSize: number; signal: AbortSignal }) {
+ const params = new URLSearchParams();
+ params.set("page", String(page));
+ params.set("page_size", String(pageSize));
+ const data = await api(`/api/dashboard/emotion/effects?${params.toString()}`, { signal });
+ return { items: data.items || [], total: data.total || 0 };
+ },
+
+ async fetchDetail(item: Record, { signal }: { signal: AbortSignal }) {
+ return api>(`/api/dashboard/emotion/effects/${item.id}`, { signal });
+ },
+
+ Detail: EmotionDetail,
+
+ formatters: {
+ score: (value: unknown) => _score(value),
+ delta: (value: unknown) => _delta(value),
+ "mono-time": (value: unknown) => _shortTs(value),
+ },
+} satisfies WorkbenchPanelEntry;
+
+export function activate(ctx: WebHostContextV1): WebUiDisposer {
+ dashboardRequest = ctx.http.request;
+ const release = ctx.ui.inject("workbench.panels.v2", (mount) => mount.register(panel));
+ return () => {
+ release();
+ dashboardRequest = null;
+ };
+}
diff --git a/plugin.py b/plugin.py
index 6860524..8f54ee3 100644
--- a/plugin.py
+++ b/plugin.py
@@ -32,7 +32,7 @@
api_version = 3
name = "emotion"
-version = "3.0.3"
+version = "3.0.4"
desc = "Timer-refreshed Emotion context and ordinary Drift preference projection."
DRIFT_PROPOSALS = ServiceKey[DriftProposalServices]("drift.proposals.v1")
DRIFT_WAKE = ServiceKey[DriftWakeServices]("drift.wake.v1")
@@ -41,10 +41,10 @@
drift_skill_roots = ("drift/skills",)
dashboard_module = "dashboard.py"
web_module = "web_module.js"
-web_requires = ("workbench.panels.v1",)
+web_requires = ("workbench.panels.v2",)
web_provides = ()
web_contract_digests = {
- "workbench.panels.v1": "724b282c22c4b3f3a36967ab664c4dfd8bce4257665f99459000306938caf527",
+ "workbench.panels.v2": "17a005a381b362ae25a0499dbf95bf7a2c3ff0bb4e9b415e7357db458de6b5db",
}
_v3_emotion_root: Path | None = None
_v3_emotion_runtime: EmotionRuntime | None = None
diff --git a/web_module.css b/web_module.css
index 0b82c53..a36fc0d 100644
--- a/web_module.css
+++ b/web_module.css
@@ -1,26 +1,195 @@
-.emotion-workbench-panel { max-width: 1180px; margin-inline: auto; }
-.emotion-workbench-panel > header { display: flex; align-items: end; justify-content: space-between; gap: 20px; }
-.emotion-workbench-panel h1, .emotion-workbench-panel h2, .emotion-workbench-panel h3, .emotion-workbench-panel p { margin: 0; }
-.emotion-workbench-panel > header p, .emotion-panel-row span, .emotion-panel-row small, .emotion-workbench-panel article > header span { color: var(--ak-ink-secondary); }
-.emotion-workbench-panel button { min-height: 44px; }
-.emotion-overview { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 1px; margin-block: 16px; background: var(--ak-rule-subtle); }
-.emotion-overview > div, .emotion-overview > p { margin: 0; padding: 14px; background: var(--ak-paper-quiet); }
-.emotion-overview span, .emotion-overview small { display: block; color: var(--ak-ink-secondary); font-size: 0.78rem; }
-.emotion-overview strong { display: block; margin-block: 4px; }
-.emotion-panel-grid { display: grid; grid-template-columns: minmax(260px, 0.8fr) minmax(340px, 1.2fr); gap: 24px; margin-block-start: 16px; }
-.emotion-panel-row { display: grid; width: 100%; gap: 4px; border: 0; border-block-end: 1px solid var(--ak-rule-subtle); padding: 12px; color: inherit; background: transparent; text-align: start; cursor: pointer; }
-@media (hover: hover) { .emotion-panel-row:hover { background: var(--ak-paper-quiet); } }
-.emotion-panel-grid footer { display: flex; align-items: center; justify-content: center; gap: 12px; padding-block: 16px; }
-.emotion-panel-grid > article { min-width: 0; border-inline-start: 1px solid var(--ak-rule-subtle); padding-inline-start: 24px; }
-.emotion-panel-grid article > header { display: flex; align-items: start; justify-content: space-between; gap: 16px; }
-.emotion-panel-grid article > header p { color: var(--ak-ink-secondary); font-size: 0.8rem; }
-.emotion-detail-metrics, .emotion-detail-coordinates { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; }
-.emotion-detail-metrics div, .emotion-detail-coordinates div { padding: 12px; background: var(--ak-paper-quiet); }
-.emotion-detail-metrics dt, .emotion-detail-coordinates dt { color: var(--ak-ink-secondary); font-size: 0.8rem; }
-.emotion-detail-metrics dd, .emotion-detail-coordinates dd { margin: 4px 0 0; font-family: ui-monospace, monospace; }
-.emotion-panel-grid article > section { margin-block: 18px; }
-.emotion-panel-grid details { margin-block-start: 12px; border-block-start: 1px solid var(--ak-rule-subtle); padding-block-start: 12px; }
-.emotion-panel-grid summary { min-height: 44px; cursor: pointer; }
-.emotion-panel-grid pre { overflow: auto; white-space: pre-wrap; }
-.emotion-workbench-panel :focus-visible { outline: 2px solid var(--ak-rule-focus); outline-offset: 2px; }
-@media (max-width: 820px) { .emotion-workbench-panel > header { align-items: start; flex-direction: column; } .emotion-overview, .emotion-detail-metrics, .emotion-detail-coordinates, .emotion-panel-grid { grid-template-columns: 1fr; } .emotion-panel-grid > article { border-inline-start: 0; padding-inline-start: 0; } }
+/* dashboard_panel.css */
+.emotion-empty {
+ padding: 24px 16px;
+}
+.emotion-empty__title {
+ color: var(--ak-color-text-primary);
+ font-size: 20px;
+ font-weight: 600;
+ letter-spacing: -0.025em;
+ line-height: 1.25;
+}
+.emotion-empty__text {
+ margin-block-start: 8px;
+ color: var(--ak-color-text-secondary);
+ font-size: 13px;
+ line-height: 1.625;
+}
+.emotion-detail {
+ display: grid;
+ gap: 18px;
+ padding: 24px;
+ color: var(--ak-color-text-primary);
+}
+.emotion-detail__header,
+.emotion-section-heading {
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ gap: 16px;
+}
+.emotion-detail__header p,
+.emotion-section-heading p {
+ margin: 0 0 4px;
+ color: var(--ak-color-action-primary);
+ font-size: 11px;
+ font-weight: 700;
+ letter-spacing: 0.08em;
+}
+.emotion-detail__header h2,
+.emotion-section-heading h3 {
+ margin: 0;
+ letter-spacing: -0.02em;
+}
+.emotion-detail__header h2 {
+ font-size: 20px;
+}
+.emotion-section-heading h3 {
+ font-size: 16px;
+}
+.emotion-detail__header span,
+.emotion-section-heading > span {
+ color: var(--ak-color-text-muted);
+ font-size: 12px;
+}
+.emotion-detail__header > div > span {
+ display: block;
+ margin-block-start: 5px;
+}
+.emotion-threshold {
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr) minmax(92px, auto);
+ align-items: center;
+ gap: 14px;
+ padding: 16px;
+ border: 1px solid var(--ak-color-border-default);
+ border-radius: 7px;
+ background: var(--ak-color-bg-surface-low);
+}
+.emotion-threshold > div {
+ display: grid;
+ gap: 4px;
+}
+.emotion-threshold span {
+ color: var(--ak-color-text-muted);
+ font-size: 11px;
+}
+.emotion-threshold strong {
+ font-family: var(--mono);
+ font-size: 22px;
+}
+.emotion-threshold__arrow {
+ font-size: 20px !important;
+}
+.emotion-threshold__delta {
+ padding-inline-start: 14px;
+ border-inline-start: 1px solid var(--ak-color-border-default);
+}
+.emotion-threshold__delta.is-up strong {
+ color: var(--ak-color-status-warning);
+}
+.emotion-threshold__delta.is-down strong {
+ color: var(--ak-color-status-success);
+}
+.emotion-coordinates {
+ display: grid;
+ gap: 16px;
+ padding-block: 4px;
+}
+.emotion-gauge {
+ display: grid;
+ grid-template-columns: 88px minmax(0, 1fr);
+ gap: 4px 14px;
+ align-items: center;
+}
+.emotion-gauge__label {
+ display: flex;
+ justify-content: space-between;
+ gap: 8px;
+ font-size: 12px;
+}
+.emotion-gauge__label code {
+ color: var(--ak-color-text-muted);
+}
+.emotion-gauge__track {
+ position: relative;
+ height: 6px;
+ border-radius: 999px;
+ background:
+ linear-gradient(
+ 90deg,
+ var(--ak-color-bg-surface-high),
+ var(--ak-color-action-soft));
+}
+.emotion-gauge__track::after {
+ position: absolute;
+ inset-block: -3px;
+ left: 50%;
+ width: 1px;
+ content: "";
+ background: var(--ak-color-border-strong);
+}
+.emotion-gauge__track i {
+ position: absolute;
+ top: 50%;
+ width: 12px;
+ height: 12px;
+ border: 2px solid var(--ak-color-bg-canvas);
+ border-radius: 50%;
+ background: var(--ak-color-action-primary);
+ box-shadow: 0 0 0 1px var(--ak-color-action-primary);
+ transform: translate(-50%, -50%);
+}
+.emotion-gauge__ends {
+ display: flex;
+ grid-column: 2;
+ justify-content: space-between;
+ color: var(--ak-color-text-muted);
+ font-size: 10px;
+}
+.emotion-disclosure {
+ border-block-start: 1px solid var(--ak-color-border-default);
+}
+.emotion-disclosure summary {
+ padding: 14px 2px;
+ color: var(--ak-color-text-primary);
+ font-size: 13px;
+ font-weight: 650;
+ cursor: pointer;
+}
+.emotion-disclosure summary:focus-visible {
+ outline: 2px solid var(--ak-color-action-primary);
+ outline-offset: 2px;
+}
+.emotion-disclosure pre {
+ overflow: auto;
+ max-height: 320px;
+ margin: 0 0 14px;
+ padding: 14px;
+ border-radius: 5px;
+ color: var(--ak-color-text-secondary);
+ background: var(--ak-color-bg-surface-low);
+ font: 12px/1.65 var(--mono);
+ white-space: pre-wrap;
+}
+@media (max-width: 720px) {
+ .emotion-detail {
+ padding: 18px;
+ }
+ .emotion-detail__header {
+ align-items: flex-start;
+ flex-direction: column;
+ }
+ .emotion-threshold {
+ grid-template-columns: 1fr auto 1fr;
+ }
+ .emotion-threshold__delta {
+ grid-column: 1 / -1;
+ padding: 12px 0 0;
+ border-inline-start: 0;
+ border-block-start: 1px solid var(--ak-color-border-default);
+ }
+ .emotion-gauge {
+ grid-template-columns: 78px minmax(0, 1fr);
+ }
+}
diff --git a/web_module.js b/web_module.js
index 8935bce..ba39a0b 100644
--- a/web_module.js
+++ b/web_module.js
@@ -1,175 +1,168 @@
-export function activate(ctx) {
- return ctx.ui.inject("workbench.panels.v1", (mount) => mount.register({
- id: "emotion-decisions",
- label: "情绪决策",
- order: 40,
- render(host) {
- const panel = document.createElement("section");
- panel.className = "emotion-workbench-panel";
- panel.innerHTML = ``;
- host.replaceChildren(panel);
- const overview = panel.querySelector("[data-overview]");
- const refresh = panel.querySelector("[data-refresh]");
- const status = panel.querySelector("[data-status]");
- const list = panel.querySelector("[data-list]");
- const detail = panel.querySelector("[data-detail]");
- const pageText = panel.querySelector("[data-page]");
- const previous = panel.querySelector("[data-previous]");
- const next = panel.querySelector("[data-next]");
- let page = 1;
- let total = 0;
- let disposed = false;
- let overviewRequest = new AbortController();
- let listRequest = new AbortController();
- let detailRequest = new AbortController();
-
- const loadOverview = async () => {
- overviewRequest.abort();
- overviewRequest = new AbortController();
- const request = overviewRequest;
- overview.textContent = "正在读取当前状态…";
- try {
- const data = await json(ctx, "/api/dashboard/emotion/overview", request.signal);
- if (disposed || request.signal.aborted) return;
- overview.innerHTML = renderOverview(data);
- } catch (reason) {
- if (!disposed && !request.signal.aborted) showError(overview, reason);
- }
- };
-
- const loadList = async () => {
- listRequest.abort();
- listRequest = new AbortController();
- const request = listRequest;
- const requestedPage = page;
- status.textContent = "正在读取情绪影响…";
- try {
- const data = await json(ctx, `/api/dashboard/emotion/effects?page=${requestedPage}&page_size=25`, request.signal);
- if (disposed || request.signal.aborted) return;
- total = finiteNumber(data.total);
- renderRows(list, data.items, openDetail);
- const pages = Math.max(1, Math.ceil(total / 25));
- pageText.textContent = `${requestedPage} / ${pages}`;
- previous.disabled = requestedPage <= 1;
- next.disabled = requestedPage >= pages;
- status.textContent = total ? `共 ${total} 条情绪影响` : "还没有改变主动决策的情绪影响。";
- } catch (reason) {
- if (!disposed && !request.signal.aborted) showError(status, reason);
- }
- };
-
- const openDetail = async (effectId) => {
- detailRequest.abort();
- detailRequest = new AbortController();
- const request = detailRequest;
- detail.innerHTML = "正在读取详情…
";
- try {
- const item = await json(ctx, `/api/dashboard/emotion/effects/${encodeURIComponent(effectId)}`, request.signal);
- if (disposed || request.signal.aborted) return;
- detail.innerHTML = renderDetail(item);
- } catch (reason) {
- if (!disposed && !request.signal.aborted) showError(detail, reason);
- }
- };
-
- refresh.addEventListener("click", () => {
- void loadOverview();
- void loadList();
- });
- previous.addEventListener("click", () => {
- if (page > 1) {
- page -= 1;
- void loadList();
- }
- });
- next.addEventListener("click", () => {
- if (page * 25 < total) {
- page += 1;
- void loadList();
- }
- });
- void loadOverview();
- void loadList();
- return () => {
- disposed = true;
- overviewRequest.abort();
- listRequest.abort();
- detailRequest.abort();
- host.replaceChildren();
- };
- },
- }));
-}
-
-function renderOverview(data) {
- const state = data && typeof data.state === "object" && data.state ? data.state : null;
- const behavior = data && typeof data.current_behavior === "object" && data.current_behavior
- ? data.current_behavior
- : null;
- if (!state || !behavior) {
- return `还没有可用的情绪状态。已记录 ${finiteNumber(data && data.effect_count)} 条情绪影响。
`;
- }
- return `当前语气${escapeHtml(behavior.tone_label || "未标注")}
发送阈值${escapeHtml(effectLabel(behavior.expected_effect))}${escapeHtml(deltaText(behavior.threshold_delta))}
情绪坐标愉悦 ${escapeHtml(score(state.valence))} · 唤醒 ${escapeHtml(score(state.arousal))} · 支配 ${escapeHtml(score(state.dominance))}
`;
+// dashboard_panel.tsx
+import { Chip, chipClass } from "@akashic/dashboard-ui";
+import { jsx, jsxs } from "react/jsx-runtime";
+var dashboardRequest = null;
+async function api(path, init) {
+ if (!dashboardRequest) throw new Error("Emotion \u5DE5\u4F5C\u53F0\u9762\u677F\u672A\u6FC0\u6D3B");
+ const response = await dashboardRequest(path, init);
+ const body = await response.json();
+ if (!response.ok) throw new Error(String(body.detail ?? body.message ?? `HTTP ${response.status}`));
+ return body;
}
-
-function renderRows(target, items, openDetail) {
- target.replaceChildren();
- if (!Array.isArray(items) || !items.length) {
- target.innerHTML = "没有可展示的情绪影响。
";
- return;
- }
- for (const item of items) {
- const button = document.createElement("button");
- button.type = "button";
- button.className = "emotion-panel-row";
- button.innerHTML = `${escapeHtml(effectLabel(item.expected_effect))}${escapeHtml(shortTime(item.created_at))} · ${escapeHtml(String(item.tone_label || "未标注"))}愉悦 ${escapeHtml(score(item.valence))} · 唤醒 ${escapeHtml(score(item.arousal))} · 阈值 ${escapeHtml(deltaText(item.threshold_delta))}`;
- button.addEventListener("click", () => void openDetail(item.id));
- target.append(button);
- }
+function _score(value) {
+ return typeof value === "number" ? value.toFixed(3) : "-";
}
-
-function renderDetail(item) {
- const delta = deltaText(item.threshold_delta);
- return `主动决策输入
这次情绪如何改变发送阈值
${escapeHtml(String(item.tick_id || "未关联任务"))} ${escapeHtml(effectLabel(item.expected_effect))}- 原始阈值
- ${escapeHtml(score(item.base_threshold))}
- 应用情绪后
- ${escapeHtml(score(item.final_threshold))}
- 变化
- ${escapeHtml(delta)}
情绪坐标
语气:${escapeHtml(String(item.tone_label || "未标注"))}
- 愉悦度
- ${escapeHtml(score(item.valence))}
- 唤醒度
- ${escapeHtml(score(item.arousal))}
- 支配度
- ${escapeHtml(score(item.dominance))}
查看写入主动流程的提示词
${escapeHtml(String(item.prompt_section || "-"))}查看技术元数据
${escapeHtml(JSON.stringify(item.metadata || {}, null, 2))} `;
+function _delta(value) {
+ if (typeof value !== "number") return "-";
+ return value > 0 ? `+${value.toFixed(3)}` : value.toFixed(3);
}
-
-async function json(ctx, path, signal) {
- const response = await ctx.http.request(path, {method: "GET", signal});
- const body = await response.json();
- if (!response.ok) throw new Error(body?.detail || body?.message || `HTTP ${response.status}`);
- return body;
+function _shortTs(value) {
+ const text = String(value || "");
+ if (!text) return "-";
+ const d = new Date(text);
+ if (Number.isNaN(d.getTime())) return text;
+ return `${d.getMonth() + 1}-${String(d.getDate()).padStart(2, "0")} ${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
}
-
-function effectLabel(value) {
- return ({raise_send_bar: "提高发送阈值", lower_send_bar: "降低发送阈值"})[value] || String(value || "-");
+function _escape(value) {
+ return String(value ?? "").replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
}
-
-function score(value) {
- const number = Number(value);
- return Number.isFinite(number) ? number.toFixed(3) : "-";
+function _effectLabel(value) {
+ const effect = String(value || "");
+ if (effect === "raise_send_bar") return "\u63D0\u9AD8\u53D1\u9001\u9608\u503C";
+ if (effect === "lower_send_bar") return "\u964D\u4F4E\u53D1\u9001\u9608\u503C";
+ return effect || "-";
}
-
-function deltaText(value) {
- const number = Number(value);
- if (!Number.isFinite(number)) return "-";
- return number > 0 ? `+${number.toFixed(3)}` : number.toFixed(3);
+function _toneCell(value) {
+ const text = String(value || "-");
+ const tone = text === "raise_send_bar" ? "warning" : text === "lower_send_bar" ? "success" : "muted";
+ return `${_escape(_effectLabel(text))}`;
}
-
-function finiteNumber(value) {
- const number = Number(value);
- return Number.isFinite(number) ? number : 0;
+function EmotionDetail(props) {
+ const item = props.item;
+ if (!item) {
+ return /* @__PURE__ */ jsxs("div", { className: "emotion-empty", children: [
+ /* @__PURE__ */ jsx("div", { className: "emotion-empty__title", children: "\u60C5\u7EEA\u5F71\u54CD\u8BE6\u60C5" }),
+ /* @__PURE__ */ jsx("div", { className: "emotion-empty__text", children: "\u9009\u62E9\u4E00\u6761\u8BB0\u5F55\uFF0C\u67E5\u770B\u8FD9\u6B21\u4E3B\u52A8\u4EFB\u52A1\u7684\u60C5\u7EEA\u5F71\u54CD\u3002" })
+ ] });
+ }
+ const delta = typeof item.threshold_delta === "number" ? item.threshold_delta : null;
+ return /* @__PURE__ */ jsxs("main", { className: "emotion-detail", "aria-labelledby": "emotion-detail-title", children: [
+ /* @__PURE__ */ jsxs("header", { className: "emotion-detail__header", children: [
+ /* @__PURE__ */ jsxs("div", { children: [
+ /* @__PURE__ */ jsx("p", { children: "\u4E3B\u52A8\u51B3\u7B56\u8F93\u5165" }),
+ /* @__PURE__ */ jsx("h2", { id: "emotion-detail-title", children: "\u8FD9\u6B21\u60C5\u7EEA\u5982\u4F55\u6539\u53D8\u53D1\u9001\u9608\u503C" }),
+ /* @__PURE__ */ jsx("span", { children: String(item.tick_id || "\u672A\u5173\u8054\u4EFB\u52A1") })
+ ] }),
+ /* @__PURE__ */ jsx(Chip, { tone: String(item.expected_effect) === "raise_send_bar" ? "warning" : "success", children: _effectLabel(item.expected_effect) })
+ ] }),
+ /* @__PURE__ */ jsxs("section", { className: "emotion-threshold", "aria-label": "\u9608\u503C\u53D8\u5316", children: [
+ /* @__PURE__ */ jsxs("div", { children: [
+ /* @__PURE__ */ jsx("span", { children: "\u539F\u59CB\u9608\u503C" }),
+ /* @__PURE__ */ jsx("strong", { children: _score(item.base_threshold) })
+ ] }),
+ /* @__PURE__ */ jsx("span", { className: "emotion-threshold__arrow", "aria-hidden": "true", children: "\u2192" }),
+ /* @__PURE__ */ jsxs("div", { children: [
+ /* @__PURE__ */ jsx("span", { children: "\u5E94\u7528\u60C5\u7EEA\u540E" }),
+ /* @__PURE__ */ jsx("strong", { children: _score(item.final_threshold) })
+ ] }),
+ /* @__PURE__ */ jsxs("div", { className: `emotion-threshold__delta${delta !== null && delta > 0 ? " is-up" : " is-down"}`, children: [
+ /* @__PURE__ */ jsx("span", { children: "\u53D8\u5316" }),
+ /* @__PURE__ */ jsx("strong", { children: _delta(delta) })
+ ] })
+ ] }),
+ /* @__PURE__ */ jsxs("section", { className: "emotion-coordinates", "aria-labelledby": "emotion-coordinates-title", children: [
+ /* @__PURE__ */ jsxs("div", { className: "emotion-section-heading", children: [
+ /* @__PURE__ */ jsxs("div", { children: [
+ /* @__PURE__ */ jsx("p", { children: "VAD \u6A21\u578B" }),
+ /* @__PURE__ */ jsx("h3", { id: "emotion-coordinates-title", children: "\u60C5\u7EEA\u5750\u6807" })
+ ] }),
+ /* @__PURE__ */ jsxs("span", { children: [
+ "\u8BED\u6C14\uFF1A",
+ String(item.tone_label || "\u672A\u6807\u6CE8")
+ ] })
+ ] }),
+ /* @__PURE__ */ jsx(VadGauge, { label: "\u6109\u60A6\u5EA6", low: "\u6D88\u6781", high: "\u79EF\u6781", value: item.valence }),
+ /* @__PURE__ */ jsx(VadGauge, { label: "\u5524\u9192\u5EA6", low: "\u5E73\u9759", high: "\u6FC0\u6D3B", value: item.arousal }),
+ /* @__PURE__ */ jsx(VadGauge, { label: "\u652F\u914D\u5EA6", low: "\u53D7\u63A7", high: "\u4E3B\u5BFC", value: item.dominance })
+ ] }),
+ /* @__PURE__ */ jsx(TextDisclosure, { title: "\u67E5\u770B\u5199\u5165\u4E3B\u52A8\u6D41\u7A0B\u7684\u63D0\u793A\u8BCD", text: String(item.prompt_section || "") }),
+ /* @__PURE__ */ jsx(TextDisclosure, { title: "\u67E5\u770B\u6280\u672F\u5143\u6570\u636E", text: JSON.stringify(item.metadata || {}, null, 2) })
+ ] });
}
-
-function shortTime(value) {
- const date = new Date(String(value || ""));
- return Number.isNaN(date.getTime()) ? "-" : new Intl.DateTimeFormat("zh-CN", {month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", hour12: false}).format(date);
+function VadGauge(props) {
+ const numeric = typeof props.value === "number" ? props.value : 0;
+ const position = Math.max(0, Math.min(100, (numeric + 1) / 2 * 100));
+ return /* @__PURE__ */ jsxs("div", { className: "emotion-gauge", children: [
+ /* @__PURE__ */ jsxs("div", { className: "emotion-gauge__label", children: [
+ /* @__PURE__ */ jsx("strong", { children: props.label }),
+ /* @__PURE__ */ jsx("code", { children: _score(props.value) })
+ ] }),
+ /* @__PURE__ */ jsx("div", { className: "emotion-gauge__track", "aria-hidden": "true", children: /* @__PURE__ */ jsx("i", { style: { left: `${position}%` } }) }),
+ /* @__PURE__ */ jsxs("div", { className: "emotion-gauge__ends", children: [
+ /* @__PURE__ */ jsx("span", { children: props.low }),
+ /* @__PURE__ */ jsx("span", { children: props.high })
+ ] })
+ ] });
}
-
-function showError(target, reason) {
- target.setAttribute("role", "alert");
- target.textContent = reason instanceof Error ? reason.message : String(reason);
+function TextDisclosure(props) {
+ return /* @__PURE__ */ jsxs("details", { className: "emotion-disclosure", children: [
+ /* @__PURE__ */ jsx("summary", { children: props.title }),
+ /* @__PURE__ */ jsx("pre", { children: props.text || "-" })
+ ] });
}
-
-function escapeHtml(value) {
- return String(value).replace(/[&<>"']/g, (character) => ({"&": "&", "<": "<", ">": ">", '"': """, "'": "'"})[character]);
+var panel = {
+ id: "emotion",
+ label: "\u60C5\u7EEA\u51B3\u7B56",
+ viewLabel: "\u60C5\u7EEA\u51B3\u7B56",
+ order: 30,
+ pageSize: 50,
+ rowKey: "id",
+ countTitle(total) {
+ return `\u5171 ${total} \u6761\u60C5\u7EEA\u5F71\u54CD`;
+ },
+ columns: [
+ { key: "created_at", label: "\u65F6\u95F4", width: 96, fmt: "mono-time", cellClass: "mono cell-time", rawTitle: true },
+ { key: "expected_effect", label: "\u5F71\u54CD", width: 132, renderCell: _toneCell },
+ { key: "tone_label", label: "\u8BED\u6C14", width: 112 },
+ { key: "valence", label: "\u6109\u60A6", width: 66, fmt: "score", cellClass: "mono cell-metric", align: "right" },
+ { key: "arousal", label: "\u5524\u9192", width: 66, fmt: "score", cellClass: "mono cell-metric", align: "right" },
+ { key: "threshold_delta", label: "\u9608\u503C\u53D8\u5316", width: 82, fmt: "delta", cellClass: "mono cell-metric", align: "right" },
+ { key: "tick_id", label: "\u4EFB\u52A1", flex: true, cellClass: "mono content-preview", rawTitle: true }
+ ],
+ async getCount({ signal }) {
+ try {
+ const overview = await api("/api/dashboard/emotion/overview", { signal });
+ return overview.effect_count || 0;
+ } catch (error) {
+ if (signal.aborted) throw error;
+ return null;
+ }
+ },
+ async fetchPage({ page, pageSize, signal }) {
+ const params = new URLSearchParams();
+ params.set("page", String(page));
+ params.set("page_size", String(pageSize));
+ const data = await api(`/api/dashboard/emotion/effects?${params.toString()}`, { signal });
+ return { items: data.items || [], total: data.total || 0 };
+ },
+ async fetchDetail(item, { signal }) {
+ return api(`/api/dashboard/emotion/effects/${item.id}`, { signal });
+ },
+ Detail: EmotionDetail,
+ formatters: {
+ score: (value) => _score(value),
+ delta: (value) => _delta(value),
+ "mono-time": (value) => _shortTs(value)
+ }
+};
+function activate(ctx) {
+ dashboardRequest = ctx.http.request;
+ const release = ctx.ui.inject("workbench.panels.v2", (mount) => mount.register(panel));
+ return () => {
+ release();
+ dashboardRequest = null;
+ };
}
+export {
+ activate
+};
From 946d005826b70bf452a2935e4399b43979d484fa Mon Sep 17 00:00:00 2001
From: huashen <2494946808@qq.com>
Date: Sun, 30 Aug 2026 19:50:31 +0800
Subject: [PATCH 8/9] test: sync Web UI Core contract
---
.github/workflows/plugin-api-v3.yml | 2 +-
tests/test_plugin.py | 5 ++++-
2 files changed, 5 insertions(+), 2 deletions(-)
diff --git a/.github/workflows/plugin-api-v3.yml b/.github/workflows/plugin-api-v3.yml
index 796f4dc..e626b76 100644
--- a/.github/workflows/plugin-api-v3.yml
+++ b/.github/workflows/plugin-api-v3.yml
@@ -36,7 +36,7 @@ jobs:
- uses: actions/checkout@v4
with:
repository: kachofugetsu09/akashic-agent
- ref: 69a9616f6f48f19dc109a6b3eef8fc1000825829
+ ref: dab4137c19fd1c5ef01ab69bacbc918d214f85c9
path: .akashic-core
- uses: actions/checkout@v4
with:
diff --git a/tests/test_plugin.py b/tests/test_plugin.py
index c082333..ae01188 100644
--- a/tests/test_plugin.py
+++ b/tests/test_plugin.py
@@ -9,6 +9,7 @@
import sqlite3
import sys
import warnings
+from collections.abc import Mapping
from contextlib import closing
from datetime import UTC, datetime, timedelta
from pathlib import Path
@@ -96,7 +97,9 @@ def __init__(self, path: Path) -> None:
def propose(self, *args: object, **kwargs: object) -> dict[str, object]:
return self.store.propose(*args, **kwargs) # pyright: ignore[reportArgumentType]
- def selection(self, accepted_turn: dict[str, object]) -> dict[str, object] | None:
+ def selection(
+ self, accepted_turn: dict[str, object]
+ ) -> Mapping[str, object] | None:
return self.store.selection(accepted_turn)
From 5d4ea418900317cd68b5821f39bf2aeec8911aea Mon Sep 17 00:00:00 2001
From: huashen <2494946808@qq.com>
Date: Sun, 30 Aug 2026 22:21:57 +0800
Subject: [PATCH 9/9] refactor(web-ui): adopt workbench mount lifecycle
---
.github/workflows/plugin-api-v3.yml | 2 +-
dashboard_panel.tsx | 15 ++-
plugin.py | 2 +-
web_module.css | 196 +---------------------------
web_module.js | 169 +-----------------------
5 files changed, 14 insertions(+), 370 deletions(-)
diff --git a/.github/workflows/plugin-api-v3.yml b/.github/workflows/plugin-api-v3.yml
index e626b76..9b90101 100644
--- a/.github/workflows/plugin-api-v3.yml
+++ b/.github/workflows/plugin-api-v3.yml
@@ -36,7 +36,7 @@ jobs:
- uses: actions/checkout@v4
with:
repository: kachofugetsu09/akashic-agent
- ref: dab4137c19fd1c5ef01ab69bacbc918d214f85c9
+ ref: d5e5177092d74de03588d8675f3504db36b7bacb
path: .akashic-core
- uses: actions/checkout@v4
with:
diff --git a/dashboard_panel.tsx b/dashboard_panel.tsx
index ee0a74d..8c0f580 100644
--- a/dashboard_panel.tsx
+++ b/dashboard_panel.tsx
@@ -1,8 +1,8 @@
import { type ReactElement } from "react";
-import { Chip, chipClass } from "@akashic/dashboard-ui";
+import { createRoot } from "react-dom/client";
import "./dashboard_panel.css";
import type { WebHostContextV1, WebUiDisposer } from "@akashic/web-ui-v1";
-import type { WorkbenchPanelEntry } from "@akashic/workbench-ui-v2";
+import type { WorkbenchDispatch, WorkbenchPanelEntry, WorkbenchUi } from "@akashic/workbench-ui-v2";
let dashboardRequest: WebHostContextV1["http"]["request"] | null = null;
@@ -60,11 +60,12 @@ function _effectLabel(value: unknown): string {
function _toneCell(value: unknown): string {
const text = String(value || "-");
const tone = text === "raise_send_bar" ? "warning" : text === "lower_send_bar" ? "success" : "muted";
- return `${_escape(_effectLabel(text))}`;
+ return `${_escape(_effectLabel(text))}`;
}
-function EmotionDetail(props: { item: Record | null }): ReactElement {
+function EmotionDetail(props: { item: Record | null; ui: WorkbenchUi }): ReactElement {
const item = props.item;
+ const Chip = props.ui.Chip;
if (!item) {
return 情绪影响详情
选择一条记录,查看这次主动任务的情绪影响。
;
}
@@ -170,7 +171,11 @@ const panel = {
return api>(`/api/dashboard/emotion/effects/${item.id}`, { signal });
},
- Detail: EmotionDetail,
+ renderDetail(item: Record | null, container: HTMLElement, dispatch: WorkbenchDispatch): WebUiDisposer {
+ const root = createRoot(container);
+ root.render();
+ return () => root.unmount();
+ },
formatters: {
score: (value: unknown) => _score(value),
diff --git a/plugin.py b/plugin.py
index 8f54ee3..a46d6db 100644
--- a/plugin.py
+++ b/plugin.py
@@ -44,7 +44,7 @@
web_requires = ("workbench.panels.v2",)
web_provides = ()
web_contract_digests = {
- "workbench.panels.v2": "17a005a381b362ae25a0499dbf95bf7a2c3ff0bb4e9b415e7357db458de6b5db",
+ "workbench.panels.v2": "fb6417c9bf532c1fdb344767d06065d5d3293da85deb64eff1e8088889a33bcb",
}
_v3_emotion_root: Path | None = None
_v3_emotion_runtime: EmotionRuntime | None = None
diff --git a/web_module.css b/web_module.css
index a36fc0d..08a03da 100644
--- a/web_module.css
+++ b/web_module.css
@@ -1,195 +1 @@
-/* dashboard_panel.css */
-.emotion-empty {
- padding: 24px 16px;
-}
-.emotion-empty__title {
- color: var(--ak-color-text-primary);
- font-size: 20px;
- font-weight: 600;
- letter-spacing: -0.025em;
- line-height: 1.25;
-}
-.emotion-empty__text {
- margin-block-start: 8px;
- color: var(--ak-color-text-secondary);
- font-size: 13px;
- line-height: 1.625;
-}
-.emotion-detail {
- display: grid;
- gap: 18px;
- padding: 24px;
- color: var(--ak-color-text-primary);
-}
-.emotion-detail__header,
-.emotion-section-heading {
- display: flex;
- align-items: flex-start;
- justify-content: space-between;
- gap: 16px;
-}
-.emotion-detail__header p,
-.emotion-section-heading p {
- margin: 0 0 4px;
- color: var(--ak-color-action-primary);
- font-size: 11px;
- font-weight: 700;
- letter-spacing: 0.08em;
-}
-.emotion-detail__header h2,
-.emotion-section-heading h3 {
- margin: 0;
- letter-spacing: -0.02em;
-}
-.emotion-detail__header h2 {
- font-size: 20px;
-}
-.emotion-section-heading h3 {
- font-size: 16px;
-}
-.emotion-detail__header span,
-.emotion-section-heading > span {
- color: var(--ak-color-text-muted);
- font-size: 12px;
-}
-.emotion-detail__header > div > span {
- display: block;
- margin-block-start: 5px;
-}
-.emotion-threshold {
- display: grid;
- grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr) minmax(92px, auto);
- align-items: center;
- gap: 14px;
- padding: 16px;
- border: 1px solid var(--ak-color-border-default);
- border-radius: 7px;
- background: var(--ak-color-bg-surface-low);
-}
-.emotion-threshold > div {
- display: grid;
- gap: 4px;
-}
-.emotion-threshold span {
- color: var(--ak-color-text-muted);
- font-size: 11px;
-}
-.emotion-threshold strong {
- font-family: var(--mono);
- font-size: 22px;
-}
-.emotion-threshold__arrow {
- font-size: 20px !important;
-}
-.emotion-threshold__delta {
- padding-inline-start: 14px;
- border-inline-start: 1px solid var(--ak-color-border-default);
-}
-.emotion-threshold__delta.is-up strong {
- color: var(--ak-color-status-warning);
-}
-.emotion-threshold__delta.is-down strong {
- color: var(--ak-color-status-success);
-}
-.emotion-coordinates {
- display: grid;
- gap: 16px;
- padding-block: 4px;
-}
-.emotion-gauge {
- display: grid;
- grid-template-columns: 88px minmax(0, 1fr);
- gap: 4px 14px;
- align-items: center;
-}
-.emotion-gauge__label {
- display: flex;
- justify-content: space-between;
- gap: 8px;
- font-size: 12px;
-}
-.emotion-gauge__label code {
- color: var(--ak-color-text-muted);
-}
-.emotion-gauge__track {
- position: relative;
- height: 6px;
- border-radius: 999px;
- background:
- linear-gradient(
- 90deg,
- var(--ak-color-bg-surface-high),
- var(--ak-color-action-soft));
-}
-.emotion-gauge__track::after {
- position: absolute;
- inset-block: -3px;
- left: 50%;
- width: 1px;
- content: "";
- background: var(--ak-color-border-strong);
-}
-.emotion-gauge__track i {
- position: absolute;
- top: 50%;
- width: 12px;
- height: 12px;
- border: 2px solid var(--ak-color-bg-canvas);
- border-radius: 50%;
- background: var(--ak-color-action-primary);
- box-shadow: 0 0 0 1px var(--ak-color-action-primary);
- transform: translate(-50%, -50%);
-}
-.emotion-gauge__ends {
- display: flex;
- grid-column: 2;
- justify-content: space-between;
- color: var(--ak-color-text-muted);
- font-size: 10px;
-}
-.emotion-disclosure {
- border-block-start: 1px solid var(--ak-color-border-default);
-}
-.emotion-disclosure summary {
- padding: 14px 2px;
- color: var(--ak-color-text-primary);
- font-size: 13px;
- font-weight: 650;
- cursor: pointer;
-}
-.emotion-disclosure summary:focus-visible {
- outline: 2px solid var(--ak-color-action-primary);
- outline-offset: 2px;
-}
-.emotion-disclosure pre {
- overflow: auto;
- max-height: 320px;
- margin: 0 0 14px;
- padding: 14px;
- border-radius: 5px;
- color: var(--ak-color-text-secondary);
- background: var(--ak-color-bg-surface-low);
- font: 12px/1.65 var(--mono);
- white-space: pre-wrap;
-}
-@media (max-width: 720px) {
- .emotion-detail {
- padding: 18px;
- }
- .emotion-detail__header {
- align-items: flex-start;
- flex-direction: column;
- }
- .emotion-threshold {
- grid-template-columns: 1fr auto 1fr;
- }
- .emotion-threshold__delta {
- grid-column: 1 / -1;
- padding: 12px 0 0;
- border-inline-start: 0;
- border-block-start: 1px solid var(--ak-color-border-default);
- }
- .emotion-gauge {
- grid-template-columns: 78px minmax(0, 1fr);
- }
-}
+.emotion-empty{padding:24px 16px}.emotion-empty__title{color:var(--ak-color-text-primary);font-size:20px;font-weight:600;letter-spacing:-.025em;line-height:1.25}.emotion-empty__text{margin-block-start:8px;color:var(--ak-color-text-secondary);font-size:13px;line-height:1.625}.emotion-detail{display:grid;gap:18px;padding:24px;color:var(--ak-color-text-primary)}.emotion-detail__header,.emotion-section-heading{display:flex;align-items:flex-start;justify-content:space-between;gap:16px}.emotion-detail__header p,.emotion-section-heading p{margin:0 0 4px;color:var(--ak-color-action-primary);font-size:11px;font-weight:700;letter-spacing:.08em}.emotion-detail__header h2,.emotion-section-heading h3{margin:0;letter-spacing:-.02em}.emotion-detail__header h2{font-size:20px}.emotion-section-heading h3{font-size:16px}.emotion-detail__header span,.emotion-section-heading>span{color:var(--ak-color-text-muted);font-size:12px}.emotion-detail__header>div>span{display:block;margin-block-start:5px}.emotion-threshold{display:grid;grid-template-columns:minmax(0,1fr) auto minmax(0,1fr) minmax(92px,auto);align-items:center;gap:14px;padding:16px;border:1px solid var(--ak-color-border-default);border-radius:7px;background:var(--ak-color-bg-surface-low)}.emotion-threshold>div{display:grid;gap:4px}.emotion-threshold span{color:var(--ak-color-text-muted);font-size:11px}.emotion-threshold strong{font-family:var(--mono);font-size:22px}.emotion-threshold__arrow{font-size:20px!important}.emotion-threshold__delta{padding-inline-start:14px;border-inline-start:1px solid var(--ak-color-border-default)}.emotion-threshold__delta.is-up strong{color:var(--ak-color-status-warning)}.emotion-threshold__delta.is-down strong{color:var(--ak-color-status-success)}.emotion-coordinates{display:grid;gap:16px;padding-block:4px}.emotion-gauge{display:grid;grid-template-columns:88px minmax(0,1fr);gap:4px 14px;align-items:center}.emotion-gauge__label{display:flex;justify-content:space-between;gap:8px;font-size:12px}.emotion-gauge__label code{color:var(--ak-color-text-muted)}.emotion-gauge__track{position:relative;height:6px;border-radius:999px;background:linear-gradient(90deg,var(--ak-color-bg-surface-high),var(--ak-color-action-soft))}.emotion-gauge__track:after{position:absolute;inset-block:-3px;left:50%;width:1px;content:"";background:var(--ak-color-border-strong)}.emotion-gauge__track i{position:absolute;top:50%;width:12px;height:12px;border:2px solid var(--ak-color-bg-canvas);border-radius:50%;background:var(--ak-color-action-primary);box-shadow:0 0 0 1px var(--ak-color-action-primary);transform:translate(-50%,-50%)}.emotion-gauge__ends{display:flex;grid-column:2;justify-content:space-between;color:var(--ak-color-text-muted);font-size:10px}.emotion-disclosure{border-block-start:1px solid var(--ak-color-border-default)}.emotion-disclosure summary{padding:14px 2px;color:var(--ak-color-text-primary);font-size:13px;font-weight:650;cursor:pointer}.emotion-disclosure summary:focus-visible{outline:2px solid var(--ak-color-action-primary);outline-offset:2px}.emotion-disclosure pre{overflow:auto;max-height:320px;margin:0 0 14px;padding:14px;border-radius:5px;color:var(--ak-color-text-secondary);background:var(--ak-color-bg-surface-low);font:12px/1.65 var(--mono);white-space:pre-wrap}@media(max-width:720px){.emotion-detail{padding:18px}.emotion-detail__header{align-items:flex-start;flex-direction:column}.emotion-threshold{grid-template-columns:1fr auto 1fr}.emotion-threshold__delta{grid-column:1 / -1;padding:12px 0 0;border-inline-start:0;border-block-start:1px solid var(--ak-color-border-default)}.emotion-gauge{grid-template-columns:78px minmax(0,1fr)}}
diff --git a/web_module.js b/web_module.js
index ba39a0b..95e1371 100644
--- a/web_module.js
+++ b/web_module.js
@@ -1,168 +1 @@
-// dashboard_panel.tsx
-import { Chip, chipClass } from "@akashic/dashboard-ui";
-import { jsx, jsxs } from "react/jsx-runtime";
-var dashboardRequest = null;
-async function api(path, init) {
- if (!dashboardRequest) throw new Error("Emotion \u5DE5\u4F5C\u53F0\u9762\u677F\u672A\u6FC0\u6D3B");
- const response = await dashboardRequest(path, init);
- const body = await response.json();
- if (!response.ok) throw new Error(String(body.detail ?? body.message ?? `HTTP ${response.status}`));
- return body;
-}
-function _score(value) {
- return typeof value === "number" ? value.toFixed(3) : "-";
-}
-function _delta(value) {
- if (typeof value !== "number") return "-";
- return value > 0 ? `+${value.toFixed(3)}` : value.toFixed(3);
-}
-function _shortTs(value) {
- const text = String(value || "");
- if (!text) return "-";
- const d = new Date(text);
- if (Number.isNaN(d.getTime())) return text;
- return `${d.getMonth() + 1}-${String(d.getDate()).padStart(2, "0")} ${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
-}
-function _escape(value) {
- return String(value ?? "").replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
-}
-function _effectLabel(value) {
- const effect = String(value || "");
- if (effect === "raise_send_bar") return "\u63D0\u9AD8\u53D1\u9001\u9608\u503C";
- if (effect === "lower_send_bar") return "\u964D\u4F4E\u53D1\u9001\u9608\u503C";
- return effect || "-";
-}
-function _toneCell(value) {
- const text = String(value || "-");
- const tone = text === "raise_send_bar" ? "warning" : text === "lower_send_bar" ? "success" : "muted";
- return `${_escape(_effectLabel(text))}`;
-}
-function EmotionDetail(props) {
- const item = props.item;
- if (!item) {
- return /* @__PURE__ */ jsxs("div", { className: "emotion-empty", children: [
- /* @__PURE__ */ jsx("div", { className: "emotion-empty__title", children: "\u60C5\u7EEA\u5F71\u54CD\u8BE6\u60C5" }),
- /* @__PURE__ */ jsx("div", { className: "emotion-empty__text", children: "\u9009\u62E9\u4E00\u6761\u8BB0\u5F55\uFF0C\u67E5\u770B\u8FD9\u6B21\u4E3B\u52A8\u4EFB\u52A1\u7684\u60C5\u7EEA\u5F71\u54CD\u3002" })
- ] });
- }
- const delta = typeof item.threshold_delta === "number" ? item.threshold_delta : null;
- return /* @__PURE__ */ jsxs("main", { className: "emotion-detail", "aria-labelledby": "emotion-detail-title", children: [
- /* @__PURE__ */ jsxs("header", { className: "emotion-detail__header", children: [
- /* @__PURE__ */ jsxs("div", { children: [
- /* @__PURE__ */ jsx("p", { children: "\u4E3B\u52A8\u51B3\u7B56\u8F93\u5165" }),
- /* @__PURE__ */ jsx("h2", { id: "emotion-detail-title", children: "\u8FD9\u6B21\u60C5\u7EEA\u5982\u4F55\u6539\u53D8\u53D1\u9001\u9608\u503C" }),
- /* @__PURE__ */ jsx("span", { children: String(item.tick_id || "\u672A\u5173\u8054\u4EFB\u52A1") })
- ] }),
- /* @__PURE__ */ jsx(Chip, { tone: String(item.expected_effect) === "raise_send_bar" ? "warning" : "success", children: _effectLabel(item.expected_effect) })
- ] }),
- /* @__PURE__ */ jsxs("section", { className: "emotion-threshold", "aria-label": "\u9608\u503C\u53D8\u5316", children: [
- /* @__PURE__ */ jsxs("div", { children: [
- /* @__PURE__ */ jsx("span", { children: "\u539F\u59CB\u9608\u503C" }),
- /* @__PURE__ */ jsx("strong", { children: _score(item.base_threshold) })
- ] }),
- /* @__PURE__ */ jsx("span", { className: "emotion-threshold__arrow", "aria-hidden": "true", children: "\u2192" }),
- /* @__PURE__ */ jsxs("div", { children: [
- /* @__PURE__ */ jsx("span", { children: "\u5E94\u7528\u60C5\u7EEA\u540E" }),
- /* @__PURE__ */ jsx("strong", { children: _score(item.final_threshold) })
- ] }),
- /* @__PURE__ */ jsxs("div", { className: `emotion-threshold__delta${delta !== null && delta > 0 ? " is-up" : " is-down"}`, children: [
- /* @__PURE__ */ jsx("span", { children: "\u53D8\u5316" }),
- /* @__PURE__ */ jsx("strong", { children: _delta(delta) })
- ] })
- ] }),
- /* @__PURE__ */ jsxs("section", { className: "emotion-coordinates", "aria-labelledby": "emotion-coordinates-title", children: [
- /* @__PURE__ */ jsxs("div", { className: "emotion-section-heading", children: [
- /* @__PURE__ */ jsxs("div", { children: [
- /* @__PURE__ */ jsx("p", { children: "VAD \u6A21\u578B" }),
- /* @__PURE__ */ jsx("h3", { id: "emotion-coordinates-title", children: "\u60C5\u7EEA\u5750\u6807" })
- ] }),
- /* @__PURE__ */ jsxs("span", { children: [
- "\u8BED\u6C14\uFF1A",
- String(item.tone_label || "\u672A\u6807\u6CE8")
- ] })
- ] }),
- /* @__PURE__ */ jsx(VadGauge, { label: "\u6109\u60A6\u5EA6", low: "\u6D88\u6781", high: "\u79EF\u6781", value: item.valence }),
- /* @__PURE__ */ jsx(VadGauge, { label: "\u5524\u9192\u5EA6", low: "\u5E73\u9759", high: "\u6FC0\u6D3B", value: item.arousal }),
- /* @__PURE__ */ jsx(VadGauge, { label: "\u652F\u914D\u5EA6", low: "\u53D7\u63A7", high: "\u4E3B\u5BFC", value: item.dominance })
- ] }),
- /* @__PURE__ */ jsx(TextDisclosure, { title: "\u67E5\u770B\u5199\u5165\u4E3B\u52A8\u6D41\u7A0B\u7684\u63D0\u793A\u8BCD", text: String(item.prompt_section || "") }),
- /* @__PURE__ */ jsx(TextDisclosure, { title: "\u67E5\u770B\u6280\u672F\u5143\u6570\u636E", text: JSON.stringify(item.metadata || {}, null, 2) })
- ] });
-}
-function VadGauge(props) {
- const numeric = typeof props.value === "number" ? props.value : 0;
- const position = Math.max(0, Math.min(100, (numeric + 1) / 2 * 100));
- return /* @__PURE__ */ jsxs("div", { className: "emotion-gauge", children: [
- /* @__PURE__ */ jsxs("div", { className: "emotion-gauge__label", children: [
- /* @__PURE__ */ jsx("strong", { children: props.label }),
- /* @__PURE__ */ jsx("code", { children: _score(props.value) })
- ] }),
- /* @__PURE__ */ jsx("div", { className: "emotion-gauge__track", "aria-hidden": "true", children: /* @__PURE__ */ jsx("i", { style: { left: `${position}%` } }) }),
- /* @__PURE__ */ jsxs("div", { className: "emotion-gauge__ends", children: [
- /* @__PURE__ */ jsx("span", { children: props.low }),
- /* @__PURE__ */ jsx("span", { children: props.high })
- ] })
- ] });
-}
-function TextDisclosure(props) {
- return /* @__PURE__ */ jsxs("details", { className: "emotion-disclosure", children: [
- /* @__PURE__ */ jsx("summary", { children: props.title }),
- /* @__PURE__ */ jsx("pre", { children: props.text || "-" })
- ] });
-}
-var panel = {
- id: "emotion",
- label: "\u60C5\u7EEA\u51B3\u7B56",
- viewLabel: "\u60C5\u7EEA\u51B3\u7B56",
- order: 30,
- pageSize: 50,
- rowKey: "id",
- countTitle(total) {
- return `\u5171 ${total} \u6761\u60C5\u7EEA\u5F71\u54CD`;
- },
- columns: [
- { key: "created_at", label: "\u65F6\u95F4", width: 96, fmt: "mono-time", cellClass: "mono cell-time", rawTitle: true },
- { key: "expected_effect", label: "\u5F71\u54CD", width: 132, renderCell: _toneCell },
- { key: "tone_label", label: "\u8BED\u6C14", width: 112 },
- { key: "valence", label: "\u6109\u60A6", width: 66, fmt: "score", cellClass: "mono cell-metric", align: "right" },
- { key: "arousal", label: "\u5524\u9192", width: 66, fmt: "score", cellClass: "mono cell-metric", align: "right" },
- { key: "threshold_delta", label: "\u9608\u503C\u53D8\u5316", width: 82, fmt: "delta", cellClass: "mono cell-metric", align: "right" },
- { key: "tick_id", label: "\u4EFB\u52A1", flex: true, cellClass: "mono content-preview", rawTitle: true }
- ],
- async getCount({ signal }) {
- try {
- const overview = await api("/api/dashboard/emotion/overview", { signal });
- return overview.effect_count || 0;
- } catch (error) {
- if (signal.aborted) throw error;
- return null;
- }
- },
- async fetchPage({ page, pageSize, signal }) {
- const params = new URLSearchParams();
- params.set("page", String(page));
- params.set("page_size", String(pageSize));
- const data = await api(`/api/dashboard/emotion/effects?${params.toString()}`, { signal });
- return { items: data.items || [], total: data.total || 0 };
- },
- async fetchDetail(item, { signal }) {
- return api(`/api/dashboard/emotion/effects/${item.id}`, { signal });
- },
- Detail: EmotionDetail,
- formatters: {
- score: (value) => _score(value),
- delta: (value) => _delta(value),
- "mono-time": (value) => _shortTs(value)
- }
-};
-function activate(ctx) {
- dashboardRequest = ctx.http.request;
- const release = ctx.ui.inject("workbench.panels.v2", (mount) => mount.register(panel));
- return () => {
- release();
- dashboardRequest = null;
- };
-}
-export {
- activate
-};
+import{createRoot as g}from"react-dom/client";import{jsx as o,jsxs as i}from"react/jsx-runtime";var r=null;async function s(e,t){if(!r)throw new Error("Emotion 工作台面板未激活");let n=await r(e,t),a=await n.json();if(!n.ok)throw new Error(String(a.detail??a.message??`HTTP ${n.status}`));return a}function l(e){return typeof e=="number"?e.toFixed(3):"-"}function p(e){return typeof e!="number"?"-":e>0?`+${e.toFixed(3)}`:e.toFixed(3)}function h(e){let t=String(e||"");if(!t)return"-";let n=new Date(t);return Number.isNaN(n.getTime())?t:`${n.getMonth()+1}-${String(n.getDate()).padStart(2,"0")} ${String(n.getHours()).padStart(2,"0")}:${String(n.getMinutes()).padStart(2,"0")}`}function f(e){return String(e??"").replaceAll("&","&").replaceAll("<","<").replaceAll(">",">").replaceAll('"',""").replaceAll("'","'")}function u(e){let t=String(e||"");return t==="raise_send_bar"?"提高发送阈值":t==="lower_send_bar"?"降低发送阈值":t||"-"}function b(e){let t=String(e||"-");return`${f(u(t))}`}function x(e){let t=e.item,n=e.ui.Chip;if(!t)return i("div",{className:"emotion-empty",children:[o("div",{className:"emotion-empty__title",children:"情绪影响详情"}),o("div",{className:"emotion-empty__text",children:"选择一条记录,查看这次主动任务的情绪影响。"})]});let a=typeof t.threshold_delta=="number"?t.threshold_delta:null;return i("main",{className:"emotion-detail","aria-labelledby":"emotion-detail-title",children:[i("header",{className:"emotion-detail__header",children:[i("div",{children:[o("p",{children:"主动决策输入"}),o("h2",{id:"emotion-detail-title",children:"这次情绪如何改变发送阈值"}),o("span",{children:String(t.tick_id||"未关联任务")})]}),o(n,{tone:String(t.expected_effect)==="raise_send_bar"?"warning":"success",children:u(t.expected_effect)})]}),i("section",{className:"emotion-threshold","aria-label":"阈值变化",children:[i("div",{children:[o("span",{children:"原始阈值"}),o("strong",{children:l(t.base_threshold)})]}),o("span",{className:"emotion-threshold__arrow","aria-hidden":"true",children:"→"}),i("div",{children:[o("span",{children:"应用情绪后"}),o("strong",{children:l(t.final_threshold)})]}),i("div",{className:`emotion-threshold__delta${a!==null&&a>0?" is-up":" is-down"}`,children:[o("span",{children:"变化"}),o("strong",{children:p(a)})]})]}),i("section",{className:"emotion-coordinates","aria-labelledby":"emotion-coordinates-title",children:[i("div",{className:"emotion-section-heading",children:[i("div",{children:[o("p",{children:"VAD 模型"}),o("h3",{id:"emotion-coordinates-title",children:"情绪坐标"})]}),i("span",{children:["语气:",String(t.tone_label||"未标注")]})]}),o(d,{label:"愉悦度",low:"消极",high:"积极",value:t.valence}),o(d,{label:"唤醒度",low:"平静",high:"激活",value:t.arousal}),o(d,{label:"支配度",low:"受控",high:"主导",value:t.dominance})]}),o(m,{title:"查看写入主动流程的提示词",text:String(t.prompt_section||"")}),o(m,{title:"查看技术元数据",text:JSON.stringify(t.metadata||{},null,2)})]})}function d(e){let t=typeof e.value=="number"?e.value:0,n=Math.max(0,Math.min(100,(t+1)/2*100));return i("div",{className:"emotion-gauge",children:[i("div",{className:"emotion-gauge__label",children:[o("strong",{children:e.label}),o("code",{children:l(e.value)})]}),o("div",{className:"emotion-gauge__track","aria-hidden":"true",children:o("i",{style:{left:`${n}%`}})}),i("div",{className:"emotion-gauge__ends",children:[o("span",{children:e.low}),o("span",{children:e.high})]})]})}function m(e){return i("details",{className:"emotion-disclosure",children:[o("summary",{children:e.title}),o("pre",{children:e.text||"-"})]})}var _={id:"emotion",label:"情绪决策",viewLabel:"情绪决策",order:30,pageSize:50,rowKey:"id",countTitle(e){return`共 ${e} 条情绪影响`},columns:[{key:"created_at",label:"时间",width:96,fmt:"mono-time",cellClass:"mono cell-time",rawTitle:!0},{key:"expected_effect",label:"影响",width:132,renderCell:b},{key:"tone_label",label:"语气",width:112},{key:"valence",label:"愉悦",width:66,fmt:"score",cellClass:"mono cell-metric",align:"right"},{key:"arousal",label:"唤醒",width:66,fmt:"score",cellClass:"mono cell-metric",align:"right"},{key:"threshold_delta",label:"阈值变化",width:82,fmt:"delta",cellClass:"mono cell-metric",align:"right"},{key:"tick_id",label:"任务",flex:!0,cellClass:"mono content-preview",rawTitle:!0}],async getCount({signal:e}){try{return(await s("/api/dashboard/emotion/overview",{signal:e})).effect_count||0}catch(t){if(e.aborted)throw t;return null}},async fetchPage({page:e,pageSize:t,signal:n}){let a=new URLSearchParams;a.set("page",String(e)),a.set("page_size",String(t));let c=await s(`/api/dashboard/emotion/effects?${a.toString()}`,{signal:n});return{items:c.items||[],total:c.total||0}},async fetchDetail(e,{signal:t}){return s(`/api/dashboard/emotion/effects/${e.id}`,{signal:t})},renderDetail(e,t,n){let a=g(t);return a.render(o(x,{item:e,ui:n.ui})),()=>a.unmount()},formatters:{score:e=>l(e),delta:e=>p(e),"mono-time":e=>h(e)}};function S(e){r=e.http.request;let t=e.ui.inject("workbench.panels.v2",n=>n.register(_));return()=>{t(),r=null}}export{S as activate};