diff --git a/.github/workflows/plugin-api-v3.yml b/.github/workflows/plugin-api-v3.yml index b4ae1de..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: 9da3a988a2bf62b0f550bd4f6bb98c4eeb1f56f5 + ref: d5e5177092d74de03588d8675f3504db36b7bacb path: .akashic-core - uses: actions/checkout@v4 with: diff --git a/akashic.plugin.toml b/akashic.plugin.toml index 56d0962..d6cba39 100644 --- a/akashic.plugin.toml +++ b/akashic.plugin.toml @@ -1,5 +1,5 @@ schema_version = 1 name = "emotion" -version = "3.0.2" +version = "3.0.4" api_version = 3 entrypoint = "plugin.py" diff --git a/dashboard_panel.css b/dashboard_panel.css index ca13718..5c7a0cc 100644 --- a/dashboard_panel.css +++ b/dashboard_panel.css @@ -1,3 +1,22 @@ +.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; diff --git a/dashboard_panel.js b/dashboard_panel.js deleted file mode 100644 index fb40852..0000000 --- a/dashboard_panel.js +++ /dev/null @@ -1,147 +0,0 @@ -// ../emotion/dashboard_panel.tsx -import { Chip, api } from "@akashic/dashboard-ui"; -import { jsx, jsxs } from "react/jsx-runtime"; -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: "detail-empty", children: [ - /* @__PURE__ */ jsx("div", { className: "detail-empty-title", children: "\u60C5\u7EEA\u5F71\u54CD\u8BE6\u60C5" }), - /* @__PURE__ */ jsx("div", { className: "detail-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 || "-" }) - ] }); -} -window.AkashicDashboard.registerPlugin({ - id: "emotion", - label: "\u60C5\u7EEA\u51B3\u7B56", - viewLabel: "\u60C5\u7EEA\u51B3\u7B56", - 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() { - try { - const overview = await api("/api/dashboard/emotion/overview"); - return overview.effect_count || 0; - } catch { - return null; - } - }, - async fetchPage({ page, pageSize }) { - 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()}`); - return { items: data.items || [], total: data.total || 0 }; - }, - async fetchDetail(item) { - return api(`/api/dashboard/emotion/effects/${item.id}`); - }, - Detail: EmotionDetail, - formatters: { - score: (value) => _score(value), - delta: (value) => _delta(value), - "mono-time": (value) => _shortTs(value) - } -}); diff --git a/dashboard_panel.tsx b/dashboard_panel.tsx index c0e7a43..8c0f580 100644 --- a/dashboard_panel.tsx +++ b/dashboard_panel.tsx @@ -1,6 +1,18 @@ -/// import { type ReactElement } from "react"; -import { Chip, api } 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 { WorkbenchDispatch, WorkbenchPanelEntry, WorkbenchUi } 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; @@ -48,13 +60,14 @@ 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
情绪影响详情
选择一条记录,查看这次主动任务的情绪影响。
; + return
情绪影响详情
选择一条记录,查看这次主动任务的情绪影响。
; } const delta = typeof item.threshold_delta === "number" ? item.threshold_delta : null; return ( @@ -114,10 +127,11 @@ function TextDisclosure(props: { title: string; text: string }): ReactElement { ); } -window.AkashicDashboard.registerPlugin({ +const panel = { id: "emotion", label: "情绪决策", viewLabel: "情绪决策", + order: 30, pageSize: 50, rowKey: "id", @@ -135,32 +149,46 @@ window.AkashicDashboard.registerPlugin({ { key: "tick_id", label: "任务", flex: true, cellClass: "mono content-preview", rawTitle: true }, ], - async getCount(): Promise { + async getCount({ signal }: { signal: AbortSignal }): Promise { try { - const overview = await api("/api/dashboard/emotion/overview"); + const overview = await api("/api/dashboard/emotion/overview", { signal }); return overview.effect_count || 0; - } catch { + } catch (error) { + if (signal.aborted) throw error; return null; } }, - async fetchPage({ page, pageSize }: { page: number; pageSize: number }) { + 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()}`); + const data = await api(`/api/dashboard/emotion/effects?${params.toString()}`, { signal }); return { items: data.items || [], total: data.total || 0 }; }, - async fetchDetail(item: Record) { - return api>(`/api/dashboard/emotion/effects/${item.id}`); + async fetchDetail(item: Record, { signal }: { signal: AbortSignal }) { + 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), 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/db.py b/db.py index 3e20679..2630ced 100644 --- a/db.py +++ b/db.py @@ -449,7 +449,7 @@ def prepare_drift_proposal( # 1. Replay a locally prepared/submitted proposal before admitting new evidence. existing = conn.execute( """ - SELECT proposal_id, revision, payload_json + SELECT proposal_id, revision, payload_json, created_at FROM emotion_drift_runs WHERE status IN ('prepared', 'submitted') ORDER BY created_at, proposal_id, revision LIMIT 1 @@ -546,6 +546,7 @@ def prepare_drift_proposal( "proposal_id": proposal_id, "revision": revision, "payload": payload, + "due_at": _aware_datetime(now), } @@ -722,6 +723,7 @@ def _drift_proposal_from_row(row: sqlite3.Row) -> dict[str, object]: "proposal_id": str(row["proposal_id"]), "revision": str(row["revision"]), "payload": payload, + "due_at": _aware_datetime(datetime.fromisoformat(str(row["created_at"]))), } diff --git a/plugin.py b/plugin.py index 2ba01ee..a46d6db 100644 --- a/plugin.py +++ b/plugin.py @@ -32,7 +32,7 @@ api_version = 3 name = "emotion" -version = "3.0.2" +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") @@ -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.v2",) +web_provides = () +web_contract_digests = { + "workbench.panels.v2": "fb6417c9bf532c1fdb344767d06065d5d3293da85deb64eff1e8088889a33bcb", +} _v3_emotion_root: Path | None = None _v3_emotion_runtime: EmotionRuntime | None = None diff --git a/runtime.py b/runtime.py index 1aac0c8..ee7ef1a 100644 --- a/runtime.py +++ b/runtime.py @@ -112,12 +112,13 @@ async def tick_once(self) -> None: proposal_id = cast(str, proposal["proposal_id"]) revision = cast(str, proposal["revision"]) payload = cast(Mapping[str, object], proposal["payload"]) + due_at = cast(datetime, proposal["due_at"]) _ = self._proposals.propose( proposal_id, revision, payload, - now, - next_due=now + _REFRESH_INTERVAL, + due_at, + next_due=due_at + _REFRESH_INTERVAL, ) db = open_db(self._db_path) try: diff --git a/tests/test_plugin.py b/tests/test_plugin.py index e5e31d5..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) @@ -129,6 +132,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 +172,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=(), ) @@ -278,6 +280,33 @@ async def test_empty_tick_overwrites_current_without_appending_history(tmp_path: conn.close() +@pytest.mark.asyncio +async def test_submitted_proposal_replay_keeps_original_due_time(tmp_path: Path) -> None: + emotion_root = tmp_path / "emotion" + module._on_turn_committed(_feedback_turn(), root=emotion_root) + drift = DriftServices(tmp_path / "drift.sqlite3") + clock = [NOW] + runtime = module.EmotionRuntime( + cast(Any, object()), + emotion_root, + PluginTimers.candidate_validation(), + drift, + drift, + now=lambda: clock[0], + ) + + await runtime.tick_once() + clock[0] += timedelta(minutes=5) + await runtime.tick_once() + + proposals = cast( + tuple[dict[str, Any], ...], + drift.store.snapshot(clock[0])["proposals"], + ) + assert len(proposals) == 1 + assert proposals[0]["due_at"] == NOW.isoformat() + + def _create_formal_legacy_fixture(path: Path) -> None: """Create the exact original three-table formal schema without new migration code.""" @@ -805,6 +834,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..08a03da --- /dev/null +++ b/web_module.css @@ -0,0 +1 @@ +.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 new file mode 100644 index 0000000..95e1371 --- /dev/null +++ b/web_module.js @@ -0,0 +1 @@ +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};