refactor: replace stat system with variable system

Replaces the specialized `stat` system with a more general-purpose
variable system. This includes:

- Renaming `ReactiveStatManager` to `ReactiveVariableManager`
- Changing template syntax from `${key}` to `{{$key}}`
- Replacing `StatsView` with `VariableView` to show declared, plain,
  and tag-typed variables
- Updating command `/stat` to `/set`
- Refactoring the reactivity engine to support variable declarations
  and tag modifiers via `csv role=declare` blocks
This commit is contained in:
2026-07-12 17:29:03 +08:00
parent 2983aa4440
commit 894f1735e5
18 changed files with 464 additions and 1063 deletions
+243
View File
@@ -0,0 +1,243 @@
/**
* VariableView — table view of all variable key-value pairs and active tags.
*
* Shows variables grouped by type (declared, plain, tag-typed) and an
* active tags section showing which variable activates each tag and
* what modifier effects are in play.
*/
import { Component, For, createMemo, Show } from "solid-js";
import { useJournalStream } from "../stores/journalStream";
import { useJournalCompletions } from "./completions";
import { computeActiveTags } from "./var-reactivity";
import { extractDependencies } from "./var-reactivity";
import type { VarDeclaration } from "./declare-parser";
export const VariableView: Component = () => {
const stream = useJournalStream();
const comp = useJournalCompletions();
const declarations = createMemo(() => comp.data.declarations);
const tagModifiers = createMemo(() => comp.data.tagModifiers);
const values = createMemo(() => stream.variables);
/** Build a map: $key → VarDeclaration */
const declMap = createMemo(() => {
const map = new Map<string, VarDeclaration>();
for (const d of declarations()) {
map.set(d.key, d);
}
return map;
});
/** Active tags */
const activeTags = createMemo(() => computeActiveTags(values()));
/** Tag modifier index: #tag → [{target, expression}] */
const tagModMap = createMemo(() => {
const map = new Map<string, Array<{ target: string; expression: string }>>();
for (const tm of tagModifiers()) {
let list = map.get(tm.tag);
if (!list) {
list = [];
map.set(tm.tag, list);
}
list.push({ target: tm.target, expression: tm.expression });
}
return map;
});
/** Which variable activates each tag */
const tagActivators = createMemo(() => {
const map = new Map<string, string>();
const vars = values();
for (const [key, val] of Object.entries(vars)) {
if (val.startsWith("#")) {
if (!map.has(val)) map.set(val, key);
}
}
return map;
});
/** Group variables: declared, plain (set directly), tag-typed */
const groups = createMemo(() => {
const vars = values();
const declKeys = new Set(declarations().map((d) => d.key));
const declared: Array<{ key: string; value: string; expr: string; deps: string[] }> = [];
const plain: Array<{ key: string; value: string }> = [];
const tagVars: Array<{ key: string; value: string }> = [];
for (const [key, val] of Object.entries(vars)) {
if (val.startsWith("#")) {
tagVars.push({ key, value: val });
} else if (declKeys.has(key)) {
const decl = declMap().get(key)!;
declared.push({
key,
value: val,
expr: decl.expression,
deps: extractDependencies(decl.expression),
});
} else {
plain.push({ key, value: val });
}
}
return { declared, plain, tagVars };
});
const hasAnyData = () =>
declarations().length > 0 ||
Object.keys(values()).length > 0 ||
activeTags().length > 0;
return (
<div class="h-full overflow-y-auto bg-gray-50">
<Show
when={hasAnyData()}
fallback={
<p class="text-center text-gray-400 text-xs py-8">
暂无变量定义。在文档中使用 ```csv role=declare 代码块定义变量。
</p>
}
>
{/* Active Tags */}
<Show when={activeTags().length > 0}>
<div class="mb-3">
<div class="sticky top-0 bg-gray-100 px-3 py-1.5 border-b border-gray-200">
<span class="text-xs font-semibold text-gray-600">激活标签</span>
</div>
<div class="divide-y divide-gray-100">
<For each={activeTags()}>
{(tag) => {
const activator = () => tagActivators().get(tag);
const mods = () => tagModMap().get(tag) ?? [];
return (
<div class="px-3 py-1.5">
<div class="flex items-center gap-1.5">
<span class="text-sm font-mono text-purple-700 font-semibold">
{tag}
</span>
<Show when={activator()}>
<span class="text-[10px] text-gray-400">
← {activator()}
</span>
</Show>
</div>
<Show when={mods().length > 0}>
<div class="mt-1 space-y-0.5">
<For each={mods()}>
{(mod) => {
const currentVal = () => values()[mod.target];
return (
<div class="text-[10px] text-gray-500 ml-2 flex items-center gap-1">
<span class="font-mono">{mod.target}</span>
<span>+{mod.expression}</span>
<Show when={currentVal() !== undefined}>
<span class="text-gray-400">
= {currentVal()}
</span>
</Show>
</div>
);
}}
</For>
</div>
</Show>
</div>
);
}}
</For>
</div>
</div>
</Show>
{/* Declared Variables */}
<Show when={groups().declared.length > 0}>
<div class="mb-3">
<div class="sticky top-0 bg-gray-100 px-3 py-1.5 border-b border-gray-200">
<span class="text-xs font-semibold text-gray-600">
声明变量
</span>
</div>
<div class="divide-y divide-gray-100">
<For each={groups().declared}>
{(item) => (
<div class="flex items-center px-3 py-1.5 hover:bg-white/50 transition-colors">
<div class="flex-1 min-w-0 flex items-center gap-1.5">
<span class="text-sm font-mono text-gray-800">
{item.key}
</span>
<span class="text-[10px] text-gray-400 bg-gray-100 px-1 rounded shrink-0">
{item.expr}
</span>
</div>
<div class="flex items-center gap-2 shrink-0">
<span class="text-sm font-mono text-gray-700 font-semibold">
{item.value}
</span>
<Show when={item.deps.length > 0}>
<span class="text-[10px] text-gray-400">
← {item.deps.join(", ")}
</span>
</Show>
</div>
</div>
)}
</For>
</div>
</div>
</Show>
{/* Plain Variables */}
<Show when={groups().plain.length > 0}>
<div class="mb-3">
<div class="sticky top-0 bg-gray-100 px-3 py-1.5 border-b border-gray-200">
<span class="text-xs font-semibold text-gray-600">
直接设置
</span>
</div>
<div class="divide-y divide-gray-100">
<For each={groups().plain}>
{(item) => (
<div class="flex items-center px-3 py-1.5 hover:bg-white/50 transition-colors">
<span class="flex-1 text-sm font-mono text-gray-800">
{item.key}
</span>
<span class="text-sm font-mono text-gray-700 font-semibold">
{item.value}
</span>
</div>
)}
</For>
</div>
</div>
</Show>
{/* Tag Variables */}
<Show when={groups().tagVars.length > 0}>
<div class="mb-3">
<div class="sticky top-0 bg-gray-100 px-3 py-1.5 border-b border-gray-200">
<span class="text-xs font-semibold text-gray-600">标签变量</span>
</div>
<div class="divide-y divide-gray-100">
<For each={groups().tagVars}>
{(item) => (
<div class="flex items-center px-3 py-1.5 hover:bg-white/50 transition-colors">
<span class="flex-1 text-sm font-mono text-gray-800">
{item.key}
</span>
<span class="text-sm font-mono text-purple-700 font-semibold">
{item.value}
</span>
</div>
)}
</For>
</div>
</div>
</Show>
</Show>
</div>
);
};