mirror of
https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology.git
synced 2026-06-14 02:40:45 +08:00
feat(i18n): add UI-only memory label formatter
This commit is contained in:
@@ -278,4 +278,30 @@ export default {
|
|||||||
"llm.providerHelp.modelFetchUnsupported": "This provider cannot fetch models automatically yet; enter the model name manually.",
|
"llm.providerHelp.modelFetchUnsupported": "This provider cannot fetch models automatically yet; enter the model name manually.",
|
||||||
"llm.providerHelp.normalizedUrl": "Normalized URL: {apiUrl}",
|
"llm.providerHelp.normalizedUrl": "Normalized URL: {apiUrl}",
|
||||||
"llm.providerHelp.transport": "Transport: {transport}",
|
"llm.providerHelp.transport": "Transport: {transport}",
|
||||||
|
|
||||||
|
"memory.type.character": "Character",
|
||||||
|
"memory.type.event": "Event",
|
||||||
|
"memory.type.location": "Location",
|
||||||
|
"memory.type.pov_memory": "POV Memory",
|
||||||
|
"memory.type.reflection": "Reflection",
|
||||||
|
"memory.type.rule": "Rule",
|
||||||
|
"memory.type.synopsis": "Global Synopsis (legacy)",
|
||||||
|
"memory.type.thread": "Thread",
|
||||||
|
|
||||||
|
"scope.badge.characterPov": "Character POV · {owner}",
|
||||||
|
"scope.badge.objectiveGlobal": "Objective · Global",
|
||||||
|
"scope.badge.objectiveRegion": "Objective · {region}",
|
||||||
|
"scope.badge.userPov": "User POV · {owner}",
|
||||||
|
"scope.owner.character": "Character",
|
||||||
|
"scope.owner.unnamed": "Unnamed",
|
||||||
|
"scope.owner.user": "User",
|
||||||
|
"scope.meta.characterPov": "Character POV: {owner}",
|
||||||
|
"scope.meta.userPov": "User POV: {owner}",
|
||||||
|
"scope.region.path": "Region path: {path}",
|
||||||
|
"scope.region.primary": "Primary region: {region}",
|
||||||
|
"scope.region.secondary": "Secondary regions: {regions}",
|
||||||
|
|
||||||
|
"storyTime.meta": "Story time: {time}",
|
||||||
|
"storyTime.mixed": "mixed",
|
||||||
|
"storyTime.mixedTime": "Mixed time",
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -278,4 +278,30 @@ export default {
|
|||||||
"llm.providerHelp.modelFetchUnsupported": "该渠道暂不支持自动拉取模型,请手动填写模型名",
|
"llm.providerHelp.modelFetchUnsupported": "该渠道暂不支持自动拉取模型,请手动填写模型名",
|
||||||
"llm.providerHelp.normalizedUrl": "规范化地址:{apiUrl}",
|
"llm.providerHelp.normalizedUrl": "规范化地址:{apiUrl}",
|
||||||
"llm.providerHelp.transport": "请求通道:{transport}",
|
"llm.providerHelp.transport": "请求通道:{transport}",
|
||||||
|
|
||||||
|
"memory.type.character": "角色",
|
||||||
|
"memory.type.event": "事件",
|
||||||
|
"memory.type.location": "地点",
|
||||||
|
"memory.type.pov_memory": "主观记忆",
|
||||||
|
"memory.type.reflection": "反思",
|
||||||
|
"memory.type.rule": "规则",
|
||||||
|
"memory.type.synopsis": "全局概要(旧)",
|
||||||
|
"memory.type.thread": "主线",
|
||||||
|
|
||||||
|
"scope.badge.characterPov": "角色 POV · {owner}",
|
||||||
|
"scope.badge.objectiveGlobal": "客观 · 全局",
|
||||||
|
"scope.badge.objectiveRegion": "客观 · {region}",
|
||||||
|
"scope.badge.userPov": "用户 POV · {owner}",
|
||||||
|
"scope.owner.character": "角色",
|
||||||
|
"scope.owner.unnamed": "未命名",
|
||||||
|
"scope.owner.user": "用户",
|
||||||
|
"scope.meta.characterPov": "角色 POV: {owner}",
|
||||||
|
"scope.meta.userPov": "用户 POV: {owner}",
|
||||||
|
"scope.region.path": "地区路径: {path}",
|
||||||
|
"scope.region.primary": "主地区: {region}",
|
||||||
|
"scope.region.secondary": "次级地区: {regions}",
|
||||||
|
|
||||||
|
"storyTime.meta": "剧情时间: {time}",
|
||||||
|
"storyTime.mixed": "混合",
|
||||||
|
"storyTime.mixedTime": "混合时间",
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -32,7 +32,8 @@
|
|||||||
"test:i18n-dom": "node tests/i18n-dom.mjs",
|
"test:i18n-dom": "node tests/i18n-dom.mjs",
|
||||||
"test:i18n-boundary": "node tests/i18n-boundary.mjs",
|
"test:i18n-boundary": "node tests/i18n-boundary.mjs",
|
||||||
"test:i18n-status": "node tests/i18n-status.mjs",
|
"test:i18n-status": "node tests/i18n-status.mjs",
|
||||||
"test:i18n": "npm run test:i18n-catalog && npm run test:i18n-dom && npm run test:i18n-boundary && npm run test:i18n-status",
|
"test:ui-label-formatter": "node tests/ui-label-formatter.mjs",
|
||||||
|
"test:i18n": "npm run test:i18n-catalog && npm run test:i18n-dom && npm run test:i18n-boundary && npm run test:i18n-status && npm run test:ui-label-formatter",
|
||||||
"bench:graph-layout": "node tests/perf/graph-layout-bench.mjs",
|
"bench:graph-layout": "node tests/perf/graph-layout-bench.mjs",
|
||||||
"bench:persist-delta": "node tests/perf/persist-delta-bench.mjs",
|
"bench:persist-delta": "node tests/perf/persist-delta-bench.mjs",
|
||||||
"bench:persist-load": "node tests/perf/persist-load-bench.mjs",
|
"bench:persist-load": "node tests/perf/persist-load-bench.mjs",
|
||||||
|
|||||||
88
tests/ui-label-formatter.mjs
Normal file
88
tests/ui-label-formatter.mjs
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
|
||||||
|
import { setLocale } from "../i18n/index.js";
|
||||||
|
import {
|
||||||
|
uiBuildRegionLine,
|
||||||
|
uiBuildScopeMetaText,
|
||||||
|
uiDescribeStoryTimeSpanDisplay,
|
||||||
|
uiMemoryNodeTypeClass,
|
||||||
|
uiOwnerTypeLabel,
|
||||||
|
uiScopeBadgeText,
|
||||||
|
uiTypeLabel,
|
||||||
|
} from "../ui/ui-label-formatter.js";
|
||||||
|
|
||||||
|
setLocale("zh-CN");
|
||||||
|
assert.equal(uiTypeLabel("event"), "事件");
|
||||||
|
assert.equal(uiTypeLabel("pov_memory"), "主观记忆");
|
||||||
|
assert.equal(uiTypeLabel("custom_type"), "custom_type");
|
||||||
|
assert.equal(uiTypeLabel(""), "—");
|
||||||
|
assert.equal(uiMemoryNodeTypeClass("character"), "type-character");
|
||||||
|
assert.equal(uiMemoryNodeTypeClass("pov_memory"), "type-character");
|
||||||
|
assert.equal(uiMemoryNodeTypeClass("event"), "type-event");
|
||||||
|
assert.equal(uiOwnerTypeLabel("user"), "用户");
|
||||||
|
assert.equal(uiOwnerTypeLabel("character"), "角色");
|
||||||
|
assert.equal(
|
||||||
|
uiScopeBadgeText({ layer: "pov", ownerType: "character", ownerName: "艾琳" }),
|
||||||
|
"角色 POV · 艾琳",
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
uiScopeBadgeText({ layer: "objective", regionPrimary: "钟楼" }),
|
||||||
|
"客观 · 钟楼",
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
uiBuildRegionLine({
|
||||||
|
regionPrimary: "钟楼",
|
||||||
|
regionPath: ["王城", "钟楼"],
|
||||||
|
regionSecondary: ["地下室"],
|
||||||
|
}),
|
||||||
|
"主地区: 钟楼 | 地区路径: 王城 / 钟楼 | 次级地区: 地下室",
|
||||||
|
);
|
||||||
|
assert.equal(uiDescribeStoryTimeSpanDisplay({ mixed: true }), "混合时间");
|
||||||
|
assert.equal(
|
||||||
|
uiDescribeStoryTimeSpanDisplay({ startLabel: "第一章", endLabel: "第二章", mixed: true }),
|
||||||
|
"第一章 → 第二章 · 混合",
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
uiBuildScopeMetaText({
|
||||||
|
scope: { layer: "pov", ownerType: "user", ownerName: "玩家" },
|
||||||
|
storyTimeSpan: { startLabel: "第一章", mixed: true },
|
||||||
|
}),
|
||||||
|
"用户 POV: 玩家 · 剧情时间: 第一章 · 混合",
|
||||||
|
);
|
||||||
|
|
||||||
|
setLocale("en-US");
|
||||||
|
assert.equal(uiTypeLabel("event"), "Event");
|
||||||
|
assert.equal(uiTypeLabel("pov_memory"), "POV Memory");
|
||||||
|
assert.equal(uiOwnerTypeLabel("user"), "User");
|
||||||
|
assert.equal(uiOwnerTypeLabel("character"), "Character");
|
||||||
|
assert.equal(
|
||||||
|
uiScopeBadgeText({ layer: "pov", ownerType: "character", ownerName: "Eileen" }),
|
||||||
|
"Character POV · Eileen",
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
uiScopeBadgeText({ layer: "objective", regionPrimary: "Clocktower" }),
|
||||||
|
"Objective · Clocktower",
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
uiBuildRegionLine({
|
||||||
|
regionPrimary: "Clocktower",
|
||||||
|
regionPath: ["Capital", "Clocktower"],
|
||||||
|
regionSecondary: ["Basement"],
|
||||||
|
}),
|
||||||
|
"Primary region: Clocktower | Region path: Capital / Clocktower | Secondary regions: Basement",
|
||||||
|
);
|
||||||
|
assert.equal(uiDescribeStoryTimeSpanDisplay({ mixed: true }), "Mixed time");
|
||||||
|
assert.equal(
|
||||||
|
uiDescribeStoryTimeSpanDisplay({ startLabel: "Chapter 1", endLabel: "Chapter 2", mixed: true }),
|
||||||
|
"Chapter 1 → Chapter 2 · mixed",
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
uiBuildScopeMetaText({
|
||||||
|
scope: { layer: "pov", ownerType: "user", ownerName: "Player" },
|
||||||
|
storyTimeSpan: { startLabel: "Chapter 1", mixed: true },
|
||||||
|
}),
|
||||||
|
"User POV: Player · Story time: Chapter 1 · mixed",
|
||||||
|
);
|
||||||
|
|
||||||
|
setLocale("zh-CN");
|
||||||
|
console.log("ui label formatter tests passed");
|
||||||
104
ui/panel.js
104
ui/panel.js
@@ -13,8 +13,6 @@ import {
|
|||||||
} from "./panel-ena-sections.js";
|
} from "./panel-ena-sections.js";
|
||||||
import { getNodeDisplayName } from "../graph/node-labels.js";
|
import { getNodeDisplayName } from "../graph/node-labels.js";
|
||||||
import {
|
import {
|
||||||
buildRegionLine,
|
|
||||||
buildScopeBadgeText,
|
|
||||||
normalizeMemoryScope,
|
normalizeMemoryScope,
|
||||||
} from "../graph/memory-scope.js";
|
} from "../graph/memory-scope.js";
|
||||||
import { listKnowledgeOwners } from "../graph/knowledge-state.js";
|
import { listKnowledgeOwners } from "../graph/knowledge-state.js";
|
||||||
@@ -72,6 +70,15 @@ import {
|
|||||||
setLocale,
|
setLocale,
|
||||||
t,
|
t,
|
||||||
} from "../i18n/index.js";
|
} from "../i18n/index.js";
|
||||||
|
import {
|
||||||
|
normalizeOwnerUiType,
|
||||||
|
uiBuildScopeMetaText,
|
||||||
|
uiBuildRegionLine,
|
||||||
|
uiMemoryNodeTypeClass,
|
||||||
|
uiOwnerTypeLabel,
|
||||||
|
uiScopeBadgeText,
|
||||||
|
uiTypeLabel,
|
||||||
|
} from "./ui-label-formatter.js";
|
||||||
|
|
||||||
let defaultPromptCache = null;
|
let defaultPromptCache = null;
|
||||||
|
|
||||||
@@ -2726,24 +2733,6 @@ function _refreshTaskTimeline() {
|
|||||||
|
|
||||||
// ---------- Memory Browser (Master-Detail) ----------
|
// ---------- Memory Browser (Master-Detail) ----------
|
||||||
|
|
||||||
function _getMemoryNodeTypeClass(type) {
|
|
||||||
switch (type) {
|
|
||||||
case "pov_memory":
|
|
||||||
case "character":
|
|
||||||
return "type-character";
|
|
||||||
case "event":
|
|
||||||
return "type-event";
|
|
||||||
case "location":
|
|
||||||
return "type-location";
|
|
||||||
case "rule":
|
|
||||||
return "type-rule";
|
|
||||||
case "thread":
|
|
||||||
return "type-thread";
|
|
||||||
default:
|
|
||||||
return "type-default";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function _parseFloorFilter(raw) {
|
function _parseFloorFilter(raw) {
|
||||||
const text = String(raw || "").trim();
|
const text = String(raw || "").trim();
|
||||||
if (!text) return null;
|
if (!text) return null;
|
||||||
@@ -2917,13 +2906,13 @@ function _refreshTaskMemoryBrowser() {
|
|||||||
const listItems = sorted.map((node) => {
|
const listItems = sorted.map((node) => {
|
||||||
const sel = node.id === currentSelectedMemoryNodeId ? "selected" : "";
|
const sel = node.id === currentSelectedMemoryNodeId ? "selected" : "";
|
||||||
const preview = _getNodeSnippet(node);
|
const preview = _getNodeSnippet(node);
|
||||||
const scopeBadge = buildScopeBadgeText(node.scope);
|
const scopeBadge = uiScopeBadgeText(node.scope);
|
||||||
const metaText = _buildScopeMetaText(node);
|
const metaText = _buildScopeMetaText(node);
|
||||||
const displayName = getNodeDisplayName(node);
|
const displayName = getNodeDisplayName(node);
|
||||||
return `
|
return `
|
||||||
<div class="bme-memory-node-item ${sel}" data-node-id="${_escHtml(node.id)}">
|
<div class="bme-memory-node-item ${sel}" data-node-id="${_escHtml(node.id)}">
|
||||||
<div class="bme-memory-node-item__header">
|
<div class="bme-memory-node-item__header">
|
||||||
<span class="bme-memory-node-item__type ${_getMemoryNodeTypeClass(node.type)}">${_escHtml(_typeLabel(node.type))}</span>
|
<span class="bme-memory-node-item__type ${uiMemoryNodeTypeClass(node.type)}">${_escHtml(uiTypeLabel(node.type))}</span>
|
||||||
<span class="bme-memory-node-item__imp">IMP: ${typeof node.importance === "number" ? node.importance.toFixed(1) : "—"}</span>
|
<span class="bme-memory-node-item__imp">IMP: ${typeof node.importance === "number" ? node.importance.toFixed(1) : "—"}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="bme-memory-node-item__title">${_escHtml(displayName)}</div>
|
<div class="bme-memory-node-item__title">${_escHtml(displayName)}</div>
|
||||||
@@ -3016,12 +3005,12 @@ function _renderTaskMemoryDetailPanel(detailEl, node, graph) {
|
|||||||
(e?.fromId === node.id || e?.toId === node.id),
|
(e?.fromId === node.id || e?.toId === node.id),
|
||||||
);
|
);
|
||||||
const detailSummary = _getNodeSnippet(node);
|
const detailSummary = _getNodeSnippet(node);
|
||||||
const scopeBadge = buildScopeBadgeText(node.scope);
|
const scopeBadge = uiScopeBadgeText(node.scope);
|
||||||
const displayName = getNodeDisplayName(node);
|
const displayName = getNodeDisplayName(node);
|
||||||
const writeBlocked = _isGraphWriteBlocked();
|
const writeBlocked = _isGraphWriteBlocked();
|
||||||
const disabledAttr = writeBlocked ? " disabled" : "";
|
const disabledAttr = writeBlocked ? " disabled" : "";
|
||||||
const badges = [
|
const badges = [
|
||||||
node.type ? `<span class="bme-memory-node-item__type ${_getMemoryNodeTypeClass(node.type)}">${_escHtml(_typeLabel(node.type))}</span>` : "",
|
node.type ? `<span class="bme-memory-node-item__type ${uiMemoryNodeTypeClass(node.type)}">${_escHtml(uiTypeLabel(node.type))}</span>` : "",
|
||||||
scopeBadge ? `<span class="bme-memory-node-item__type type-default">${_escHtml(scopeBadge)}</span>` : "",
|
scopeBadge ? `<span class="bme-memory-node-item__type type-default">${_escHtml(scopeBadge)}</span>` : "",
|
||||||
node.archived ? '<span class="bme-memory-node-item__type type-default">ARCHIVED</span>' : "",
|
node.archived ? '<span class="bme-memory-node-item__type type-default">ARCHIVED</span>' : "",
|
||||||
].filter(Boolean).join("");
|
].filter(Boolean).join("");
|
||||||
@@ -3111,9 +3100,9 @@ function _openMemoryPopup(node, graph) {
|
|||||||
if (!popup || !bodyEl) return;
|
if (!popup || !bodyEl) return;
|
||||||
|
|
||||||
const displayName = getNodeDisplayName(node);
|
const displayName = getNodeDisplayName(node);
|
||||||
const scopeBadge = buildScopeBadgeText(node.scope);
|
const scopeBadge = uiScopeBadgeText(node.scope);
|
||||||
const badges = [
|
const badges = [
|
||||||
node.type ? `<span class="bme-memory-node-item__type ${_getMemoryNodeTypeClass(node.type)}">${_escHtml(_typeLabel(node.type))}</span>` : "",
|
node.type ? `<span class="bme-memory-node-item__type ${uiMemoryNodeTypeClass(node.type)}">${_escHtml(uiTypeLabel(node.type))}</span>` : "",
|
||||||
scopeBadge ? `<span class="bme-memory-node-item__type type-default">${_escHtml(scopeBadge)}</span>` : "",
|
scopeBadge ? `<span class="bme-memory-node-item__type type-default">${_escHtml(scopeBadge)}</span>` : "",
|
||||||
node.archived ? '<span class="bme-memory-node-item__type type-default">ARCHIVED</span>' : "",
|
node.archived ? '<span class="bme-memory-node-item__type type-default">ARCHIVED</span>' : "",
|
||||||
].filter(Boolean).join("");
|
].filter(Boolean).join("");
|
||||||
@@ -4006,13 +3995,6 @@ function _ownerAvatarHsl(name) {
|
|||||||
return `hsl(${hue}, 55%, 42%)`;
|
return `hsl(${hue}, 55%, 42%)`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function _normalizeOwnerUiType(ownerType = "") {
|
|
||||||
const normalized = String(ownerType || "").trim();
|
|
||||||
if (normalized === "user") return "user";
|
|
||||||
if (normalized === "character") return "character";
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
function _inferOwnerTypeFromKey(ownerKey = "") {
|
function _inferOwnerTypeFromKey(ownerKey = "") {
|
||||||
const normalizedOwnerKey = String(ownerKey || "").trim().toLowerCase();
|
const normalizedOwnerKey = String(ownerKey || "").trim().toLowerCase();
|
||||||
if (normalizedOwnerKey.startsWith("user:")) return "user";
|
if (normalizedOwnerKey.startsWith("user:")) return "user";
|
||||||
@@ -4020,13 +4002,6 @@ function _inferOwnerTypeFromKey(ownerKey = "") {
|
|||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
function _getOwnerTypeDisplayLabel(ownerType = "") {
|
|
||||||
const normalizedType = _normalizeOwnerUiType(ownerType);
|
|
||||||
if (normalizedType === "user") return "用户";
|
|
||||||
if (normalizedType === "character") return "角色";
|
|
||||||
return "Owner";
|
|
||||||
}
|
|
||||||
|
|
||||||
function _buildOwnerCollisionIndex(owners = []) {
|
function _buildOwnerCollisionIndex(owners = []) {
|
||||||
const collisionIndex = new Map();
|
const collisionIndex = new Map();
|
||||||
for (const owner of Array.isArray(owners) ? owners : []) {
|
for (const owner of Array.isArray(owners) ? owners : []) {
|
||||||
@@ -4034,7 +4009,7 @@ function _buildOwnerCollisionIndex(owners = []) {
|
|||||||
String(owner?.ownerName || owner?.ownerKey || "未命名角色").trim() ||
|
String(owner?.ownerName || owner?.ownerKey || "未命名角色").trim() ||
|
||||||
"未命名角色";
|
"未命名角色";
|
||||||
const nameKey = baseName.toLocaleLowerCase("zh-Hans-CN");
|
const nameKey = baseName.toLocaleLowerCase("zh-Hans-CN");
|
||||||
const ownerType = _normalizeOwnerUiType(owner?.ownerType) || "unknown";
|
const ownerType = normalizeOwnerUiType(owner?.ownerType) || "unknown";
|
||||||
const entry = collisionIndex.get(nameKey) || {
|
const entry = collisionIndex.get(nameKey) || {
|
||||||
count: 0,
|
count: 0,
|
||||||
typeCounts: new Map(),
|
typeCounts: new Map(),
|
||||||
@@ -4058,8 +4033,8 @@ function _getOwnerDisplayInfo(owner = {}, collisionIndex = null) {
|
|||||||
"未命名角色";
|
"未命名角色";
|
||||||
const ownerKey = String(owner?.ownerKey || "").trim();
|
const ownerKey = String(owner?.ownerKey || "").trim();
|
||||||
const ownerType =
|
const ownerType =
|
||||||
_normalizeOwnerUiType(owner?.ownerType) || _inferOwnerTypeFromKey(ownerKey);
|
normalizeOwnerUiType(owner?.ownerType) || _inferOwnerTypeFromKey(ownerKey);
|
||||||
const typeLabel = _getOwnerTypeDisplayLabel(ownerType);
|
const typeLabel = uiOwnerTypeLabel(ownerType);
|
||||||
const collisionInfo =
|
const collisionInfo =
|
||||||
collisionIndex instanceof Map
|
collisionIndex instanceof Map
|
||||||
? collisionIndex.get(baseName.toLocaleLowerCase("zh-Hans-CN")) || null
|
? collisionIndex.get(baseName.toLocaleLowerCase("zh-Hans-CN")) || null
|
||||||
@@ -5282,7 +5257,7 @@ function _renderRecentList(elementId, items) {
|
|||||||
|
|
||||||
const badge = document.createElement("span");
|
const badge = document.createElement("span");
|
||||||
badge.className = `bme-type-badge ${_safeCssToken(item.type)}`;
|
badge.className = `bme-type-badge ${_safeCssToken(item.type)}`;
|
||||||
badge.textContent = _typeLabel(item.type);
|
badge.textContent = uiTypeLabel(item.type);
|
||||||
li.appendChild(badge);
|
li.appendChild(badge);
|
||||||
|
|
||||||
const content = document.createElement("div");
|
const content = document.createElement("div");
|
||||||
@@ -5393,11 +5368,11 @@ function _refreshMemoryBrowser() {
|
|||||||
|
|
||||||
const badge = document.createElement("span");
|
const badge = document.createElement("span");
|
||||||
badge.className = `bme-type-badge ${_safeCssToken(node.type)}`;
|
badge.className = `bme-type-badge ${_safeCssToken(node.type)}`;
|
||||||
badge.textContent = _typeLabel(node.type);
|
badge.textContent = uiTypeLabel(node.type);
|
||||||
|
|
||||||
const scopeChip = document.createElement("span");
|
const scopeChip = document.createElement("span");
|
||||||
scopeChip.className = "bme-memory-scope-chip";
|
scopeChip.className = "bme-memory-scope-chip";
|
||||||
scopeChip.textContent = buildScopeBadgeText(node.scope);
|
scopeChip.textContent = uiScopeBadgeText(node.scope);
|
||||||
|
|
||||||
head.append(badge, scopeChip);
|
head.append(badge, scopeChip);
|
||||||
|
|
||||||
@@ -6076,9 +6051,9 @@ function _describeStoryTimeSpanDisplay(storyTimeSpan = {}) {
|
|||||||
: normalized.startLabel || normalized.endLabel || "";
|
: normalized.startLabel || normalized.endLabel || "";
|
||||||
|
|
||||||
if (!label) {
|
if (!label) {
|
||||||
return normalized.mixed ? "混合时间" : "";
|
return normalized.mixed ? t("storyTime.mixedTime") : "";
|
||||||
}
|
}
|
||||||
return normalized.mixed ? `${label} · 混合` : label;
|
return normalized.mixed ? `${label} · ${t("storyTime.mixed")}` : label;
|
||||||
}
|
}
|
||||||
|
|
||||||
function _describeNodeStoryTimeDisplay(node = {}) {
|
function _describeNodeStoryTimeDisplay(node = {}) {
|
||||||
@@ -6208,11 +6183,11 @@ function _buildNodeDetailEditorFragment(raw, { idPrefix = "bme-detail" } = {}) {
|
|||||||
const fragment = document.createDocumentFragment();
|
const fragment = document.createDocumentFragment();
|
||||||
const inputId = (suffix) => `${idPrefix}-${suffix}`;
|
const inputId = (suffix) => `${idPrefix}-${suffix}`;
|
||||||
|
|
||||||
_appendNodeDetailReadOnly(fragment, "类型", _typeLabel(raw.type));
|
_appendNodeDetailReadOnly(fragment, "类型", uiTypeLabel(raw.type));
|
||||||
_appendNodeDetailReadOnly(
|
_appendNodeDetailReadOnly(
|
||||||
fragment,
|
fragment,
|
||||||
"作用域",
|
"作用域",
|
||||||
buildScopeBadgeText(raw.scope),
|
uiScopeBadgeText(raw.scope),
|
||||||
);
|
);
|
||||||
_appendNodeDetailReadOnly(fragment, "ID", raw.id || "—");
|
_appendNodeDetailReadOnly(fragment, "ID", raw.id || "—");
|
||||||
_appendNodeDetailReadOnly(
|
_appendNodeDetailReadOnly(
|
||||||
@@ -6228,7 +6203,7 @@ function _buildNodeDetailEditorFragment(raw, { idPrefix = "bme-detail" } = {}) {
|
|||||||
`${scope.ownerType || "unknown"} / ${scope.ownerName || scope.ownerId || "—"}`,
|
`${scope.ownerType || "unknown"} / ${scope.ownerName || scope.ownerId || "—"}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const regionLine = buildRegionLine(scope);
|
const regionLine = uiBuildRegionLine(scope);
|
||||||
if (regionLine) {
|
if (regionLine) {
|
||||||
_appendNodeDetailReadOnly(fragment, "地区", regionLine);
|
_appendNodeDetailReadOnly(fragment, "地区", regionLine);
|
||||||
}
|
}
|
||||||
@@ -14843,18 +14818,7 @@ function _matchesMemoryFilter(node, filter = "all") {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function _buildScopeMetaText(node) {
|
function _buildScopeMetaText(node) {
|
||||||
const scope = normalizeMemoryScope(node?.scope);
|
return uiBuildScopeMetaText(node);
|
||||||
const parts = [];
|
|
||||||
if (scope.layer === "pov") {
|
|
||||||
parts.push(
|
|
||||||
`${scope.ownerType === "user" ? "用户 POV" : "角色 POV"}: ${scope.ownerName || scope.ownerId || "未命名"}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const regionLine = buildRegionLine(scope);
|
|
||||||
if (regionLine) parts.push(regionLine);
|
|
||||||
const storyTime = _describeNodeStoryTimeDisplay(node);
|
|
||||||
if (storyTime) parts.push(`剧情时间: ${storyTime}`);
|
|
||||||
return parts.join(" · ");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 记忆列表等指标:避免浮点误差打出 9.499999999999998 */
|
/** 记忆列表等指标:避免浮点误差打出 9.499999999999998 */
|
||||||
@@ -14878,20 +14842,6 @@ function _formatMemoryInt(value, fallback = 0) {
|
|||||||
return String(Math.trunc(x));
|
return String(Math.trunc(x));
|
||||||
}
|
}
|
||||||
|
|
||||||
function _typeLabel(type) {
|
|
||||||
const map = {
|
|
||||||
character: "角色",
|
|
||||||
event: "事件",
|
|
||||||
location: "地点",
|
|
||||||
thread: "主线",
|
|
||||||
rule: "规则",
|
|
||||||
synopsis: "全局概要(旧)",
|
|
||||||
reflection: "反思",
|
|
||||||
pov_memory: "主观记忆",
|
|
||||||
};
|
|
||||||
return map[type] || type || "—";
|
|
||||||
}
|
|
||||||
|
|
||||||
function _getNodeSnippet(node) {
|
function _getNodeSnippet(node) {
|
||||||
const fields = node.fields || {};
|
const fields = node.fields || {};
|
||||||
const storyTime = _describeNodeStoryTimeDisplay(node);
|
const storyTime = _describeNodeStoryTimeDisplay(node);
|
||||||
|
|||||||
145
ui/ui-label-formatter.js
Normal file
145
ui/ui-label-formatter.js
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
// UI-only label formatting for ST-BME.
|
||||||
|
//
|
||||||
|
// Keep this module on the frontend boundary: these labels are translated for
|
||||||
|
// human-facing UI only. Do not import it from prompt/model/persistence paths.
|
||||||
|
|
||||||
|
import { normalizeMemoryScope } from "../graph/memory-scope.js";
|
||||||
|
import {
|
||||||
|
normalizeStoryTime,
|
||||||
|
normalizeStoryTimeSpan,
|
||||||
|
} from "../graph/story-timeline.js";
|
||||||
|
import { t } from "../i18n/index.js";
|
||||||
|
|
||||||
|
const UI_MEMORY_SCOPE_LAYER = Object.freeze({
|
||||||
|
OBJECTIVE: "objective",
|
||||||
|
POV: "pov",
|
||||||
|
});
|
||||||
|
|
||||||
|
const UI_MEMORY_SCOPE_OWNER_TYPE = Object.freeze({
|
||||||
|
CHARACTER: "character",
|
||||||
|
USER: "user",
|
||||||
|
});
|
||||||
|
|
||||||
|
export function uiMemoryNodeTypeClass(type) {
|
||||||
|
switch (type) {
|
||||||
|
case "pov_memory":
|
||||||
|
case "character":
|
||||||
|
return "type-character";
|
||||||
|
case "event":
|
||||||
|
return "type-event";
|
||||||
|
case "location":
|
||||||
|
return "type-location";
|
||||||
|
case "rule":
|
||||||
|
return "type-rule";
|
||||||
|
case "thread":
|
||||||
|
return "type-thread";
|
||||||
|
default:
|
||||||
|
return "type-default";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function uiTypeLabel(type) {
|
||||||
|
const normalized = String(type || "").trim();
|
||||||
|
if (!normalized) return "—";
|
||||||
|
const key = `memory.type.${normalized}`;
|
||||||
|
const translated = t(key);
|
||||||
|
return translated === key ? normalized : translated;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeOwnerUiType(ownerType = "") {
|
||||||
|
const normalized = String(ownerType || "").trim();
|
||||||
|
if (normalized === "user") return "user";
|
||||||
|
if (normalized === "character") return "character";
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function uiOwnerTypeLabel(ownerType = "") {
|
||||||
|
const normalizedType = normalizeOwnerUiType(ownerType);
|
||||||
|
if (normalizedType === "user") return t("scope.owner.user");
|
||||||
|
if (normalizedType === "character") return t("scope.owner.character");
|
||||||
|
return "Owner";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function uiScopeBadgeText(scope) {
|
||||||
|
const normalized = normalizeMemoryScope(scope);
|
||||||
|
if (normalized.layer === UI_MEMORY_SCOPE_LAYER.POV) {
|
||||||
|
const ownerLabel = normalized.ownerName || normalized.ownerId || "POV";
|
||||||
|
return normalized.ownerType === UI_MEMORY_SCOPE_OWNER_TYPE.USER
|
||||||
|
? t("scope.badge.userPov", { owner: ownerLabel })
|
||||||
|
: t("scope.badge.characterPov", { owner: ownerLabel });
|
||||||
|
}
|
||||||
|
return normalized.regionPrimary
|
||||||
|
? t("scope.badge.objectiveRegion", { region: normalized.regionPrimary })
|
||||||
|
: t("scope.badge.objectiveGlobal");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function uiBuildRegionLine(scope) {
|
||||||
|
const normalized = normalizeMemoryScope(scope);
|
||||||
|
const regionPath = Array.isArray(normalized.regionPath)
|
||||||
|
? normalized.regionPath.filter(Boolean)
|
||||||
|
: [];
|
||||||
|
const regionSecondary = Array.isArray(normalized.regionSecondary)
|
||||||
|
? normalized.regionSecondary.filter(Boolean)
|
||||||
|
: [];
|
||||||
|
const parts = [];
|
||||||
|
if (normalized.regionPrimary) {
|
||||||
|
parts.push(t("scope.region.primary", { region: normalized.regionPrimary }));
|
||||||
|
}
|
||||||
|
if (regionPath.length > 0) {
|
||||||
|
parts.push(t("scope.region.path", { path: regionPath.join(" / ") }));
|
||||||
|
}
|
||||||
|
if (regionSecondary.length > 0) {
|
||||||
|
parts.push(t("scope.region.secondary", { regions: regionSecondary.join(", ") }));
|
||||||
|
}
|
||||||
|
return parts.join(" | ");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function uiDescribeStoryTimeDisplay(storyTime = {}) {
|
||||||
|
const normalized = normalizeStoryTime(storyTime);
|
||||||
|
const parts = [];
|
||||||
|
if (normalized.arc) parts.push(normalized.arc);
|
||||||
|
if (normalized.chapter) parts.push(normalized.chapter);
|
||||||
|
if (normalized.scene) parts.push(normalized.scene);
|
||||||
|
return parts.join(" / ");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function uiDescribeStoryTimeSpanDisplay(storyTimeSpan = {}) {
|
||||||
|
const normalized = normalizeStoryTimeSpan(storyTimeSpan);
|
||||||
|
const label =
|
||||||
|
normalized.startLabel &&
|
||||||
|
normalized.endLabel &&
|
||||||
|
normalized.startLabel !== normalized.endLabel
|
||||||
|
? `${normalized.startLabel} → ${normalized.endLabel}`
|
||||||
|
: normalized.startLabel || normalized.endLabel || "";
|
||||||
|
|
||||||
|
if (!label) {
|
||||||
|
return normalized.mixed ? t("storyTime.mixedTime") : "";
|
||||||
|
}
|
||||||
|
return normalized.mixed ? `${label} · ${t("storyTime.mixed")}` : label;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function uiDescribeNodeStoryTimeDisplay(node = {}) {
|
||||||
|
return (
|
||||||
|
uiDescribeStoryTimeDisplay(node.storyTime) ||
|
||||||
|
uiDescribeStoryTimeSpanDisplay(node.storyTimeSpan) ||
|
||||||
|
""
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function uiBuildScopeMetaText(node = {}) {
|
||||||
|
const scope = normalizeMemoryScope(node?.scope);
|
||||||
|
const parts = [];
|
||||||
|
if (scope.layer === UI_MEMORY_SCOPE_LAYER.POV) {
|
||||||
|
const ownerLabel = scope.ownerName || scope.ownerId || t("scope.owner.unnamed");
|
||||||
|
parts.push(
|
||||||
|
scope.ownerType === UI_MEMORY_SCOPE_OWNER_TYPE.USER
|
||||||
|
? t("scope.meta.userPov", { owner: ownerLabel })
|
||||||
|
: t("scope.meta.characterPov", { owner: ownerLabel }),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const regionLine = uiBuildRegionLine(scope);
|
||||||
|
if (regionLine) parts.push(regionLine);
|
||||||
|
const storyTime = uiDescribeNodeStoryTimeDisplay(node);
|
||||||
|
if (storyTime) parts.push(t("storyTime.meta", { time: storyTime }));
|
||||||
|
return parts.join(" · ");
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user