Compare commits

...
7 Commits
Author SHA1 Message Date
梦圣智杰 d6c51f9de2 chore: 发布 v2.0.6 / Release v2.0.6 2026-10-10 23:05:14 +08:00
梦圣智杰 b8218f7e10 feat: 明细表可按关键词查找当前提示词 / Filter the detail table by keyword
- 明细表上方新增查找框,输入即筛选出当前节点里命中的标签,英文原文和中文解释都参与匹配
- 命中片段会高亮,右侧显示命中数量,按 Esc 或点 × 一键清空
- 查找与「全部 / 未收录 / 机器翻译 / 个人词库 / 收藏」叠加生效。
2026-10-10 23:00:01 +08:00
梦圣智杰 01123cef5c chore: 去掉跳词库的「管理面板」按钮 / Remove the redundant manager panel button
- 侧边栏「标签管理」里的「管理面板」按钮和顶部「词库」页签是一回事,去掉后工具栏只留「刷新词库」
- 画布上节点面板里的同一个按钮也一并去掉,两边共用同一份面板
2026-10-10 19:09:04 +08:00
梦圣智杰 a8b7aca8f8 feat: 词库可按自然语言词库来源筛选 / Filter the dictionary by the natural language source
- 来源下拉新增「自然语言词库」,只显示保存时选了该词库的条目;「个人」不受影响,仍包含它们
- 分类下拉里「自然语言」固定排在「全部」下面,其余分类仍按拼音排序
2026-10-10 18:33:18 +08:00
梦圣智杰 c72422e648 feat: 侧边栏节点列表显示画布上改过的节点名 / Show custom node titles in the sidebar node list
- 标签管理的节点下拉、以及保存收藏时选节点的弹窗,会显示在画布上双击改过的节点名,后面保留节点号以便区分同名节点
- 没改过名的节点仍显示节点号加提示词摘要
2026-10-10 18:17:19 +08:00
梦圣智杰 ff8395302a feat: Anima 排序可一键取消 / Cancel the Anima sort with one click
- 排序写入后按钮变成「取消 Anima 顺序排序」,点击把提示词恢复到排序前的样子,同时退出分类表视图。
- 提示词本来就是 Anima 顺序时,取消只退出分类视图,不动提示词内容。
- 排序后又手动改过提示词,取消入口会自动失效,避免回退把后来的改动一起吞掉。
- 排序与优化的预览弹窗里,「确认写入英文」改为「确认写入新提示词」。
2026-10-10 18:03:11 +08:00
梦圣智杰 cab56b2f60 feat: 词库搜索添加的标签插到选中标签之后 / Insert searched tags after the selected tag
- 在节点和侧边栏里搜索词库添加标签时,新标签会插在当前选中标签的后面,不再总是追加到提示词末尾;没有选中标签时才加到末尾。
- 连续添加多个标签会依次往后排,添加完新标签自动处于选中状态。
2026-10-10 17:43:17 +08:00
8 changed files with 234 additions and 43 deletions
+28 -5
View File
@@ -1,5 +1,31 @@
# 更新记录
## v2.0.6(2026-10-10)
### 明细表可以按关键词查找
- 明细表上方新增查找框,输入即筛选出当前节点里命中的标签,英文原文和中文解释都参与匹配。
- 命中的片段会高亮,右侧显示命中数量,按 `Esc` 或点 × 一键清空。
- 查找与「全部 / 未收录 / 机器翻译 / 个人词库 / 收藏」**叠加生效**,可以先筛来源再筛关键词。
### 添加标签会插到选中标签之后
- 在节点里、以及侧边栏搜索词库添加标签时,新标签插在当前选中标签的后面,不再一律追加到提示词末尾;没选中标签时才加到末尾。
- 连续添加多个标签会依次往后排,添加完新标签自动处于选中状态。
### Anima 排序可以一键取消
- 排序写入后按钮变成「取消 Anima 顺序排序」,点击把提示词恢复到排序前的样子,同时退出分类表视图。
- 提示词本来就是 Anima 顺序时,取消只退出分类视图,不动提示词内容。
- 排序后又手动改过提示词的,取消入口会自动失效,避免回退把后来的改动一起吞掉。
- 排序与优化的预览弹窗里,「确认写入英文」改成「确认写入新提示词」。
### 侧边栏与词库
- 词库来源下拉新增「自然语言词库」,只显示保存时选了该词库的条目;分类下拉里「自然语言」固定排在「全部」下面,其余仍按拼音排序。
- 标签管理的节点下拉、以及保存收藏时选节点的弹窗,会显示在画布上双击改过的节点名(后面保留节点号,方便区分同名节点)。
- 去掉侧边栏「标签管理」里的「管理面板」按钮——它和顶部「词库」页签是同一回事,工具栏只留「刷新词库」。
## v2.0.0(2026-10-10)
### 新增「重复检查」节点
@@ -67,6 +93,7 @@
- **修复插件前端扩展被加载两遍**:`pyproject.toml` 的 `[tool.comfy] web = "js"` 与 `__init__.py` 的 `WEB_DIRECTORY = "./js"` 会被 ComfyUI 分别登记(key 一个是 `project.name`、一个是模块名),`/extensions` 接口因此把同一批 js 列两次,前端各加载一次并报 `Extension named ... already registered.`。已移除 `web` 字段,前端资源统一由 `WEB_DIRECTORY` 提供。
- 上游连接判定改用 `node.isInputConnected(slot)`(老版本无此方法时退回读 `input.link`),消除新版前端里 `input.link is deprecated` 的控制台刷屏。
## v1.2.0(2026-09-23)
- 翻译服务与 AI 后端解耦(schema v3):翻译/解释可选纯词库 / 百度翻译 / AI;"翻译并优化"与"优化为 Anima"恒走 AI。AI 与百度密钥独立留存,切换翻译服务互不清空——只有 AI 后端/地址变更或换机器时才清 AI Key,百度密钥只在显式清除时才清。
@@ -75,7 +102,6 @@
- 修复 `clear_ai_api_key` / `clear_baidu_secret_key` 在 payload 未同时包含对应密钥字段时不生效的嵌套条件 bug。
- 修复节点高度棘轮上涨:ComfyUI `text` 控件的 `computedHeight` 是单调累加器,每次写值都会让节点变大一点。改用原生 `text` 控件的 `scrollHeight@auto` 作为稳定基线,不再读 `widget.y`,节点高度在多次写值后锁定不变。
- `inspectorNodeTargetHeight` 签名简化为 `{baseHeight, targetWidgetHeight, fallbackBaseHeight, minimumHeight}`,移除旧 `widgetY`/`nodeHeight`/`currentWidgetHeight` 等参数。
- 新增标签拖拽排序:英文标签区可按住标签直接拖动,明细表每行左侧提供 `⠿` 手柄(含插入指示线与边缘自动滚动),选中标签后支持 `Alt+↑` / `Alt+↓` 键盘微调;三处入口共用同一套 `Ctrl+Z` 撤销历史。
- 排序按“提取—插入”建模:保留原有逗号、空格与换行排版,多行结构不被合并;自然语言块整体移动,内部不拆分;重复标签按出现位置独立移动。
- 明细表筛选视图(未知/机器译/个人词库/收藏)与自然语言、指令模式下不启用拖拽,避免局部顺序被误解为完整顺序。
@@ -96,16 +122,13 @@
- 收藏数据存在 ComfyUI 用户目录 `user/bilingual-prompt-inspector/saved_prompts/`(`index.json` + 独立图片文件),不写入插件目录,重装插件不丢数据;单图上限 5 MB,最多 2000 条。
- 收藏不记录工作流信息;「载入节点」先弹窗确认再覆盖节点文本,不进 `Ctrl+Z` 撤销栈。侧边栏「新建」在没有选中节点时提供节点选择器兜底。
- 新增 `tests/test_saved_prompt_store.py`:覆盖图片魔数校验、超限拒绝、删除连带删图、导出导入往返(保留原顺序)、重复与非法条目跳过、非法时间戳回退等 14 个用例。
- 新增外观模式配置:助手设置里可切换「跟随 ComfyUI / 暗色 / 亮色」,默认跟随——ComfyUI 用什么主题插件就用什么主题。此前插件把配色硬编码成暗色值,在 ComfyUI 亮色模式下文字与背景几乎无法分辨。
- 实现上新增一组 `--bpi-*` CSS 变量(面板、文字、边框、按钮、输入框、表格、弹窗),按 `:root`(亮色默认)、`.dark-theme`(跟随 ComfyUI 暗色)、`[data-bpi-theme]`(强制暗/亮)四套定义;配置存在 `assistant_store` 的 `appearance` 字段,切换后即时生效无需刷新。
- 节点中文同步编辑区的「标签管理」「助手设置」入口已移除(与侧边栏管理面板重复),代码里保留注释说明如何恢复;助手设置里的翻译服务说明改为整行显示,不再挤在表单左列。
- 用户界面英文化:为符合 ComfyUI-Manager 注册要求,节点显示名改为 `Prompt Translator & Manager`,`@title` / `@description` / `RETURN_NAMES` / `CATEGORY` 与约 540 处 JS 界面文字(按钮、提示、状态消息、输入框占位符、语法诊断)全部改为英文。
- 中文通过 locale 保留:新增 `locales/zh/main.json`(`nodeDefs` 结构),在 ComfyUI 里把界面语言切到中文后,节点名、描述与输入输出名仍显示中文「提示词翻译与管理(英文输出)」——由 ComfyUI 的 `build_translations()` 与前端 `mergeCustomNodesI18n()` 自动合并,无需额外配置。
- 英文化时有四类内容刻意保留中文:代码注释;匹配用户中文输入的正则模式(如「翻译要求/正文」结构化指令识别);词库中文分类名匹配表(改掉会导致分类排序失效);以及「未收录」「自然语言片段(待翻译或确认)」等在中文列显示的翻译数据。
- 更新 5 个测试文件的断言以匹配新的英文字符串(`test_anima_sorter.mjs`、`test_clear_controls.mjs`、`test_dictionary_tools.mjs`、`test_parser.mjs`、`test_project_link.mjs`)。
- 新增界面语言设置:助手设置页新增「Language」下拉,可选「跟随 ComfyUI 语言设置」(默认)/「中文」/「英文」,随其它设置一起存在 `assistant_store` 的 `language` 字段(auto / zh / en),并在 `localStorage` 留一份供首屏同步取值。
- ComfyUI 自带的 locale 只翻译 `nodeDefs`(节点名、描述、输入输出名),面板里的按钮、状态栏、tooltip 管不到,因此新增插件自建的 i18n 层 `js/i18n.js`:源码继续保留英文原文(上架要求),运行时按原文查中文表,查不到就原样显示英文,不会出现半中半英或空文案。
- 文案统一走 `t()`:`element()` / `button()` / `field()` 三个 DOM 工厂与全部 `title` / `placeholder` / `textContent` 赋值点都接入翻译(共 200+ 条文案)。带变量的文案(如 `deleted “xxx”; press Ctrl+Z to undo`)把「“…”」片段和数字折叠成 `{}` 占位符再回填,模板字符串也能整句翻译。
@@ -115,7 +138,7 @@
- 补齐第二批翻译(约 200 条):词包卡片信息(个人词库/大词库/社区词包的条数、版本、来源、授权)、标签管理的统计行「共 N 个标签|已识别|未收录|待确认」、模式提示、各类操作回执(移动/隐藏/恢复/翻译/导入)、语法诊断全部文案(括号、权重、重复标签、冲突组、输入模式判定理由)、Anima 分类名、以及所有请求失败提示。
- 变量的递归翻译:嵌在文案里的变量本身也可能是界面词(如冲突组名 `Solo vs multiple subjects`、模式名 `Tag`),回填时会再翻译一层,避免出现「中文句子里夹一个英文词」。
- 为让变量能被稳定折叠,`parser.js` 的诊断模板与词包信息模板给动态片段加上「“...”」包裹(版本号 `1.0.0`、来源、授权等),否则带点号的版本会被拆成多个变量而匹配不到译表。
- 统计行改为分段翻译再拼接:词库页的 `Showing … | Selected … | Enabled packs … | Built-in … | Large … | Personal … | Pending …` 与节点里的 `Common dictionary … items | Large …` 原来是整句做键,大词库有「未安装 / 按需(N 条)/ 已停用」三种形态,整句键必然漏掉其中几种。现在每段各自翻译后再用 ` | ` 拼起来,新增形态不会再漏。
- 统计行改为分段翻译再拼接:词库页的 `Showing … | Selected … | Enabled packs … | Built-in … | Large … | Personal … | Pending …` 与节点里的 `Common dictionary … items | Large …` 原来是整句做键,大词库有「未安装 / 按需(N 条)/ 已停用」三种形态,整句键必然漏掉其中几种。现在每段各自翻译后再用 `|` 拼起来,新增形态不会再漏。
- 修掉一类「译表里有、界面上却还是英文」的隐蔽 bug:模板里的自由文本变量(标签名、词包名、错误信息、文件名、操作名)没有用「“...”」包裹时不会被折叠成 `{}`,整句变成带具体内容的字面量,永远匹配不到译表。已统一给 27 处这类变量加上包裹,并把 4 处误用 ASCII 直引号的地方(`"Re-translated \"...\""` 等)换成全角引号;译表里 6 条把直引号烤进键名的条目(`"Processed \"{}\""` 等)同步改成纯 `{}`。
- 补齐第三批翻译(22 条):置信度徽章(`Low · pending` / `Medium · pending` / `Medium · session-only`)、收藏星标按钮(★/☆)、`⚠ Not indexed:` 标签提示、Anima 格式说明、`Node #N · N tags` 收藏备注、`Running …` / `Translating unknown tags N/M` / `Translation complete …` 三条运行状态、`Querying dictionary…` / `Searching for more…` / `Paused, waiting for confirmation…`、权重越界诊断、以及词包导入预览与删除确认。
- 两处组合句改分段翻译:词包导入预览 `Tags N | Sample: …`(示例内容是数据,不该参与翻译)与标签管理节点标题 `Node #N: …`(提示词原文不翻译)。
+1 -1
View File
@@ -1,4 +1,4 @@
# ComfyUI 双语提示词管理器 v2.0.0
# ComfyUI 双语提示词管理器 v2.0.6
> ComfyUI 双语提示词节点:把英文提示词拆成可点击的标签,边写边看中文解释、调顺序、试效果。
+161 -26
View File
@@ -57,7 +57,7 @@ import {
} from "./bpi_shared.js";
const NODE_NAME = "BilingualPromptInspector";
const EXTENSION_VERSION = "v2.0.0";
const EXTENSION_VERSION = "v2.0.6";
const PROJECT_URL = "https://github.com/mengshengzhijie/ComfyUI-Bilingual-Prompt-Inspector";
const COLLAPSED_WIDGET_FALLBACK_HEIGHT = 390;
const COLLAPSED_NODE_MIN_HEIGHT = 360;
@@ -201,6 +201,21 @@ function createPanel(node, textWidget) {
// 机器翻译批量入库操作栏:只在表格里出现机器翻译词条时才显示(详见 renderMachineBatchBar)
const machineBatchBar = element("div", "bpi-filters bpi-batch-bar bpi-hidden");
const table = element("div", "bpi-table");
// 明细表查找:只筛视图,不动提示词。视觉上跟表格扣成一块(上圆角、无下边框),
// 好跟上面那个「搜词库插标签」的输入框区分开——那个改提示词,这个不改。
const tableSearch = element("div", "bpi-table-search");
const tableSearchIcon = element("span", "bpi-table-search-icon");
tableSearchIcon.innerHTML = '<svg viewBox="0 0 12 12" width="11" height="11" fill="none" stroke="currentColor" stroke-width="1.4"><circle cx="5" cy="5" r="3.3"/><path d="M7.5 7.5 10.6 10.6"/></svg>';
const tableSearchInput = element("input", "bpi-search bpi-table-search-input");
// 不用 type=search:Chrome 会再画一个原生清除按钮,跟我们自己的 × 重复
tableSearchInput.type = "text";
setPlaceholder(tableSearchInput, "Find in the current prompt");
setTitle(tableSearchInput, "Filter the detail list by keyword; matches the English original or the Chinese explanation; the prompt itself never changes");
const tableSearchCount = element("span", "bpi-table-search-count");
const tableSearchClear = element("button", "bpi-table-search-clear", "×");
tableSearchClear.type = "button";
setTitle(tableSearchClear, "Clear search");
tableSearch.append(tableSearchIcon, tableSearchInput, tableSearchCount, tableSearchClear);
const head = element("div", "bpi-head");
head.append(element("div", "", "English original (actual output)"), element("div", "", "Chinese explanation (read-only)"));
// 表头分界处的抓手:拖动改左右两栏比例。比例写在表格的 --bpi-split 变量上,
@@ -278,6 +293,8 @@ function createPanel(node, textWidget) {
editing: null,
preferences: loadPreferences(),
tableFilter: "all",
// 明细表查找词:只筛视图,不进任何持久化数据
tableQuery: "",
filterButtons: new Map(),
// 表格里勾选待入库的机器翻译词条,只存 token.key
batchSelected: new Set(),
@@ -305,6 +322,9 @@ function createPanel(node, textWidget) {
searchVisibleLimit: 40,
lastRenderedSearchQuery: "",
categoryView: false,
// 「按 Anima 顺序排序」写入后留一份快照 { before, after },按钮据此变成「取消排序」;
// 用户又手动改了文本就作废(见 render),避免回退把他后来的改动一起吞掉
animaSortSnapshot: null,
// 节点标签卡颜色模式:default(不着色)| status(按收录:未收录淡橙、机器翻译淡紫、已收录不变)
// | random(从 token_colors.json 摇色)| by_pack(按标签所在词库取 pack_colors)
colorMode: localStorage.getItem("bpi.colorMode") || "default",
@@ -606,6 +626,7 @@ function createPanel(node, textWidget) {
}
if (before === after) {
if (label === "Anima official sort") {
state.animaSortSnapshot = { before, after };
state.categoryView = true;
render();
setText(englishHint, "Category table view | click tags to link; category names are not written to the prompt");
@@ -639,6 +660,8 @@ function createPanel(node, textWidget) {
setText(editEnglishButton, "Edit text");
setText(englishHint, "");
updateText(after);
// 排序落盘后记住排序前那一版,按钮才好变成「取消 Anima 顺序排序」
if (label === "Anima official sort") state.animaSortSnapshot = { before, after };
setStatus(`applied “${label}”; press Ctrl+Z to undo`, "ok");
return true;
};
@@ -706,7 +729,7 @@ function createPanel(node, textWidget) {
if (detailNode) modal.appendChild(detailNode);
const actions = element("div", "bpi-modal-actions");
const close = () => shade.remove();
actions.append(button("Cancel", close), button("Confirm write English", () => {
actions.append(button("Cancel", close), button("Confirm write new prompt", () => {
if (applyFullText(after.value, label)) close();
}, "bpi-primary"));
modal.appendChild(actions);
@@ -1729,9 +1752,32 @@ function createPanel(node, textWidget) {
state.textareaListeners = [];
};
// 把当前文本解析成标签列表。render 用它,插入标签后也立刻刷一次:
// 连着插入(双击、回车连按)时下一次要拿新坐标,否则会拿着上一次的
// 位置去切已经变长的文本,落点就偏了。
const parseTokens = (text) => parsePrompt(text, state.index, state.machine, { mode: state.modePreference }).map((token) => {
const override = state.localOverrides.get(token.key);
if (!override) return token;
return {
...token,
chinese: override.text,
source: override.source,
status: "session",
confidence: "medium",
confidenceLabel: "Medium · session-only",
};
});
const insertEnglish = (english, keepSearch = false) => {
const current = String(textWidget.value ?? "");
const start = state.englishEditing ? (englishEditor.selectionStart ?? current.length) : current.length;
// 标签视图下没有光标可依,落点跟底部「快捷添加」一致:插到当前选中标签后面,
// 没选中标签才落到末尾。侧边栏「标签管理」复用的就是这份明细表,所以那边
// 搜词库点结果也是走这里。
const anchor = state.englishEditing ? null : state.tokens.find((item) => item.id === state.pinned);
const at = anchor?.end;
const start = state.englishEditing
? (englishEditor.selectionStart ?? current.length)
: (Number.isInteger(at) && at > 0 && at <= current.length ? at : current.length);
const end = state.englishEditing ? (englishEditor.selectionEnd ?? start) : start;
const before = current.slice(0, start);
const after = current.slice(end);
@@ -1739,7 +1785,16 @@ function createPanel(node, textWidget) {
const needsSuffix = after.trim() && !/^[\s,\n]/.test(after);
const insertion = `${needsPrefix ? ", " : ""}${english}${needsSuffix ? ", " : ""}`;
const nextText = before + insertion + after;
// 新标签落在选中标签之后,也就是原来排在它前面的那些标签之后:加完把它设为选中,
// 连着加几个才会依次往后排,不用每次重新点一遍落点。
const nextId = state.englishEditing ? null : state.tokens.filter((token) => token.end <= start).length;
updateText(nextText, state.englishEditing ? start + insertion.length : null);
if (nextId !== null) {
state.tokens = parseTokens(nextText);
state.pinned = nextId;
state.selected.clear();
state.selected.add(nextId);
}
rememberRecent(english);
if (!keepSearch) search.value = "";
renderSearch();
@@ -2067,6 +2122,33 @@ function createPanel(node, textWidget) {
rebuildDictionaryIndex();
};
// 排序写入之后按钮就变成「取消」:点一下把提示词和分类视图一起还原到排序前。
const updateSortButton = () => {
const sorted = Boolean(state.animaSortSnapshot);
setText(sortPromptButton, sorted ? "Cancel Anima order sort" : "Sort by Anima order");
setTitle(sortPromptButton, sorted
? "Restore the prompt as it was before the Anima sort"
: "press Anima stable sort by recommended categories, one row per non-empty category; natural language and BREAK/AND kept intact");
};
const cancelAnimaSort = () => {
const snapshot = state.animaSortSnapshot;
if (!snapshot) return;
state.animaSortSnapshot = null;
if (snapshot.before !== snapshot.after) {
if (applyFullText(snapshot.before, "Cancel Anima sort")) {
setStatus("Restored the prompt from before the Anima sort", "ok");
}
} else {
// 排序没改动文本(本来就是 Anima 顺序),只切了分类视图:退出视图就回到原样
state.categoryView = false;
setText(englishHint, "");
render();
setStatus("Exited the Anima category view", "ok");
}
updateSortButton();
};
const sortByAnimaOrder = async () => {
if (state.assistantBusy) return;
const source = String(textWidget.value ?? "").trim();
@@ -2082,6 +2164,8 @@ function createPanel(node, textWidget) {
tokens = parsePrompt(source, state.index, state.machine, { mode: "auto" });
const result = sortAnimaPrompt(tokens, { groupLines: true });
if (result.text === source) {
// 本来就是 Anima 顺序,只是切成分类视图:前后文本相同,取消时退出视图即可
state.animaSortSnapshot = { before: source, after: source };
state.categoryView = true;
render();
setText(englishHint, "Category table view | click tags to link; category names are not written to the prompt");
@@ -2688,6 +2772,28 @@ function createPanel(node, textWidget) {
// 快捷添加区要等面板搭完才存在,这里先占位,render 里用可选链调用就不会撞 TDZ
let updateQuickTarget = null;
// 命中的片段包一层 mark。全程用文本节点拼,不碰 innerHTML —— 提示词是用户输入,
// 走字符串拼接等于给自己开个注入口子。
const highlightFragment = (text, needle) => {
const source = String(text ?? "");
const fragment = document.createDocumentFragment();
if (!needle) {
fragment.appendChild(document.createTextNode(source));
return fragment;
}
const haystack = source.toLowerCase();
let cursor = 0;
for (let at = haystack.indexOf(needle); at !== -1; at = haystack.indexOf(needle, cursor)) {
if (at > cursor) fragment.appendChild(document.createTextNode(source.slice(cursor, at)));
const hit = document.createElement("mark");
hit.className = "bpi-hit";
hit.textContent = source.slice(at, at + needle.length);
fragment.appendChild(hit);
cursor = at + needle.length;
}
fragment.appendChild(document.createTextNode(source.slice(cursor)));
return fragment;
};
const render = () => {
renderSourceBar();
// 输出管道跟着视图一起刷:任何文本/隐藏列表变化最后都会走到 render,
@@ -2695,19 +2801,11 @@ function createPanel(node, textWidget) {
syncEffectiveOutput();
const text = String(textWidget.value ?? "");
state.lastText = text;
// 排序完又手动改了文本,「回到排序前」会把这些改动一起吞掉,取消入口随之失效
if (state.animaSortSnapshot && text.trim() !== state.animaSortSnapshot.after) state.animaSortSnapshot = null;
updateSortButton();
state.modeInfo = detectInputMode(text, state.modePreference);
state.tokens = parsePrompt(text, state.index, state.machine, { mode: state.modePreference }).map((token) => {
const override = state.localOverrides.get(token.key);
if (!override) return token;
return {
...token,
chinese: override.text,
source: override.source,
status: "session",
confidence: "medium",
confidenceLabel: "Medium · session-only",
};
});
state.tokens = parseTokens(text);
queueLargeLookup(state.tokens);
// 重复分组要在渲染两个视图之前算好,标签区和明细表都吃这份数据
state.duplicateGroups = duplicateGroups(state.tokens);
@@ -2749,7 +2847,14 @@ function createPanel(node, textWidget) {
if (state.tableFilter === "favorites") return isFavorite(token.term);
return true;
});
renderMachineBatchBar(visibleTokens.filter((token) => token.status === "machine"));
// 查找和上面的筛选是叠加关系:先按来源筛,再按关键词筛,清空关键词回到筛选结果
const query = state.tableQuery.trim().toLowerCase();
const searchedTokens = query
? visibleTokens.filter((token) => token.raw.toLowerCase().includes(query) || token.chinese.toLowerCase().includes(query))
: visibleTokens;
tableSearchCount.textContent = query ? `${searchedTokens.length} / ${visibleTokens.length}` : "";
tableSearchClear.classList.toggle("bpi-visible", Boolean(query));
renderMachineBatchBar(searchedTokens.filter((token) => token.status === "machine"));
const errorsByKey = new Map();
for (const item of state.issues) {
for (const key of item.tokenKeys ?? []) {
@@ -2776,8 +2881,10 @@ function createPanel(node, textWidget) {
table.appendChild(element("div", "bpi-empty", "Enter a prompt in the English text box above; per-tag bilingual mapping will appear here."));
} else if (!visibleTokens.length) {
table.appendChild(element("div", "bpi-empty", "No matching items under the current filter."));
} else if (!searchedTokens.length) {
table.appendChild(element("div", "bpi-empty", `No tag matches “${state.tableQuery.trim()}”`));
} else {
visibleTokens.forEach((token, visibleIndex) => {
searchedTokens.forEach((token, visibleIndex) => {
const row = element("div", `bpi-row bpi-${token.status}`);
if (token.suspectedNatural) row.classList.add("bpi-suspected-natural");
const tokenHidden = isTokenHidden(token);
@@ -2793,14 +2900,16 @@ function createPanel(node, textWidget) {
if (state.selected.size > 1 && state.selected.has(token.id)) row.classList.add("bpi-row-multi");
const englishCell = element("div", "bpi-cell bpi-en");
if (token.status === "machine") englishCell.appendChild(buildRowCheckButton(token));
if (state.tableFilter === "all" && canReorderTokens()) {
// 查找状态下不给拖拽手柄:落点是按可见行算的,藏着行会把标签挪到错误的位置
if (state.tableFilter === "all" && !query && canReorderTokens()) {
englishCell.appendChild(buildRowDragHandle(row, token));
}
if (canHideTokens()) {
englishCell.appendChild(buildRowHideButton(token));
}
const chineseCell = element("div", "bpi-cell bpi-zh");
const englishText = element("span", tokenHidden || tokenSuppressed ? "bpi-hidden-term" : "", token.raw);
const englishText = element("span", tokenHidden || tokenSuppressed ? "bpi-hidden-term" : "");
englishText.appendChild(highlightFragment(token.raw, query));
setTitle(englishText, tokenSuppressed
? `Query: “${token.term}” | “Unique” is on: only the first occurrence reaches the actual output`
: `Query: “${token.term}”${token.weight === null ? "" : ` | weight: ${token.weight}`}`);
@@ -2843,7 +2952,8 @@ function createPanel(node, textWidget) {
setTimeout(() => { editor.focus(); editor.select(); }, 0);
return; // forEach 里不能用 continue;这里跳过本条剩余的渲染逻辑
}
const chineseText = element("span", "", token.chinese);
const chineseText = element("span", "");
chineseText.appendChild(highlightFragment(token.chinese, query));
setTitle(chineseText, "Double-click to edit Chinese explanation");
chineseText.addEventListener("dblclick", (event) => {
event.stopPropagation();
@@ -3228,14 +3338,15 @@ function createPanel(node, textWidget) {
render();
}),
);
// 侧边栏「标签管理」复用的就是这份 detailsBody,而它自己就在侧边栏里,
// 再放一个跳去词库的按钮和点顶部「词库」页签是同一件事,故不保留。
rightTools.append(
button("Manager panel", () => openManagerPanel("tags"), "bpi-primary"),
button("Refresh dictionary", refreshDictionary),
);
toolbar.append(leftTools, rightTools);
searchLine.append(modeSelect, searchLabel, search);
summary.append(counts, modeInfo, status);
detailsBody.append(toolbar, searchLine, results, summary, filtersBar, colorPanel, machineBatchBar, issuesPanel, table, detailsHiddenBar);
detailsBody.append(toolbar, searchLine, results, summary, filtersBar, colorPanel, machineBatchBar, issuesPanel, tableSearch, table, detailsHiddenBar);
// 底部快捷输入:中文先翻译再追加,英文直接追加。只挂在节点面板上,
// 明细表(detailsBody)才会被侧边栏「标签管理」复用,这个输入框不会跟着过去。
const quickSection = element("section", "bpi-quick-section");
@@ -3367,11 +3478,12 @@ function createPanel(node, textWidget) {
action();
});
setTitle(optimizeChineseButton, "Only optimizes existing English; does not translate");
setTitle(sortPromptButton, "press Anima stable sort by recommended categories, one row per non-empty category; natural language and BREAK/AND kept intact");
bindMirrorAction(optimizeChineseButton, runAnimaOptimize);
bindMirrorAction(sortPromptButton, sortByAnimaOrder);
// bindMirrorAction(tagManagerButton, () => openManagerPanel("tag-manager", node.id));
// setTitle(tagManagerButton, "在侧边栏管理面板中打开本节点的标签翻译与词库搜索");
bindMirrorAction(sortPromptButton, () => {
if (state.animaSortSnapshot) cancelAnimaSort();
else sortByAnimaOrder();
});
updateSortButton();
setTitle(editEnglishButton, "Switch between the English text editor and the linked tag view");
setTitle(clearEnglishButton, "Requires a second click to clear the actual English output; after clearing, click the same button or press Ctrl+Z to undo");
editEnglishButton.addEventListener("mousedown", (event) => {
@@ -3424,6 +3536,26 @@ function createPanel(node, textWidget) {
});
search.addEventListener("input", renderSearch);
search.addEventListener("focus", renderSearch);
const clearTableSearch = () => {
tableSearchInput.value = "";
state.tableQuery = "";
render();
};
tableSearchInput.addEventListener("input", () => {
state.tableQuery = tableSearchInput.value;
render();
});
tableSearchInput.addEventListener("keydown", (event) => {
if (event.key !== "Escape" || !tableSearchInput.value) return;
// 节点里按 Esc 会被画布当成取消,这里自己吃掉
event.preventDefault();
event.stopPropagation();
clearTableSearch();
});
tableSearchClear.addEventListener("click", () => {
clearTableSearch();
tableSearchInput.focus({ preventScroll: true });
});
modeSelect.addEventListener("change", () => {
state.modePreference = modeSelect.value;
state.editing = null;
@@ -3617,6 +3749,9 @@ app.registerExtension({
bindUpstreamListener();
const inspector = createPanel(this, textWidget);
this._bilingualPromptInspector = inspector;
// 记下出厂标题。用户在画布上双击节点标题改名后 node.title 会跟着变,
// 侧边栏拿这个当基准,才能判断「这是用户自己起的名字」。
inspector.defaultNodeTitle = this.constructor?.title ?? this.title;
inspectorRegistry.set(String(this.id ?? ""), inspector);
try {
this.addDOMWidget("bilingual_inspector", "bilingual-inspector", inspector.panel, {
+6 -5
View File
@@ -1214,10 +1214,10 @@ function injectBpiStyles() {
const style = document.createElement("style");
style.id = "bpi-styles";
style.textContent = `
:root{--bpm-accent:#45926f;--bpm-accent-hover:#3a7d5e;--bpm-accent-border:#35755a;--bpi-custom-bg:#e6f6f1;--bpi-custom-border:#7fc9b0;--bpi-custom-text:#0f6e56;--bpi-surface:rgba(255,255,255,.92);--bpi-surface-2:#f5f5f5;--bpi-surface-3:#eaeaea;--bpi-surface-table:#f0f0f0;--bpi-surface-modal:#fff;--bpi-text:#333;--bpi-text-muted:#666;--bpi-text-faint:#999;--bpi-border:#ccc;--bpi-border-2:#bbb;--bpi-border-3:#e0e0e0;--bpi-button-bg:#e8e8e8;--bpi-button-text:#333;--bpi-input-bg:#fff;--bpi-input-text:#333;--bpi-head-bg:#e8e8e8;--bpi-danger-bg:#c0392b;--bpi-danger-bg-hover:#a53023;--bpi-danger-border:#a53023;--bpi-danger-text:#fff;--bpi-break-bg:#e6f6f1;--bpi-break-bg-hover:#0f6e56;--bpi-break-border:#7fc9b0;--bpi-break-text:#0f6e56;--bpi-primary-bg:#2b6cb0;--bpi-primary-bg-hover:#245a94;--bpi-primary-border:#1e4f85;--bpi-primary-text:#fff;--bpi-en-head-text:#17557f;--bpi-en-hint-text:#5b7f9e;--bpi-zh-head-text:#1f5c22;--bpi-zh-hint-text:#5b7d5e;--bpi-cat-name-text:#1e5a28;--bpi-info-text:#1f5f96;--bpi-link-text:#1a5f96;--bpi-zh-text:#18508a;--bpi-status-error:#b3261e;--bpi-status-ok:#1a7f45;--bpi-status-busy:#8a5a00;--bpi-badge-warn:#8a5a00;--bpi-badge-source:#0f6e56;--bpi-badge-suspected:#1c5f80;--bpi-badge-machine:#5b3fa8;--bpi-star-on:#a8750a;--bpi-star-off:#6b7280;--bpi-quick-head-text:#4a3a7a;--bpi-quick-hint-text:#6b5a8f;--bpi-handle-text:#4a6a85;--bpi-handle-hover:#1f5f96;--bpi-mini-hover-bg:#d8dee7;--bpi-handle-hover-bg:#e2e8ef;--bpi-chip-hide-bg:#eef1f5;--bpi-waiting-bg:#fdf1dc;--bpi-waiting-border:#d9a95a;--bpi-hidden-chip-hover-bg:#e8eef5;--bpi-issues-bg:#fdf6e8;--bpi-issues-border:#d9b98a;--bpi-issue-text:#7a5410;--bpi-row-hover-overlay:rgba(47,108,176,.06);--bpi-row-pinned-overlay:#cfe3f7;--bpi-row-hover-ring:rgba(47,108,176,.55);--bpi-row-pinned-ring:#1a5f96;--bpi-row-hover-text:#1f1f1f;--bpi-row-hover-shadow:none;--bpi-row-selected-bg:#dcebf8;--bpi-en-section-bg:rgba(43,108,176,.06);--bpi-quick-section-bg:rgba(107,85,148,.06);--bpi-dup-overlay:rgba(179,38,30,.12);--bpi-editor-bg:#fff}
.dark-theme{--bpi-custom-bg:#17342c;--bpi-custom-border:#3f8f77;--bpi-custom-text:#7fdcbd;--bpi-surface:rgba(10,12,18,.72);--bpi-surface-2:#171b22;--bpi-surface-3:#14181f;--bpi-surface-table:#11151b;--bpi-surface-modal:#20252d;--bpi-text:#e6edf7;--bpi-text-muted:#aab5c5;--bpi-text-faint:#7f8a9a;--bpi-border:#3a4250;--bpi-border-2:#4c5668;--bpi-border-3:#29313d;--bpi-button-bg:#2c3340;--bpi-button-text:#e8edf5;--bpi-input-bg:#171b22;--bpi-input-text:#eef3fb;--bpi-head-bg:#252c36;--bpi-danger-bg:#65313b;--bpi-danger-bg-hover:#7d3d49;--bpi-danger-border:#8d4653;--bpi-danger-text:#ffd9de;--bpi-break-bg:#17342c;--bpi-break-bg-hover:#0f6e56;--bpi-break-border:#3f8f77;--bpi-break-text:#7fdcbd;--bpi-primary-bg:#285f8e;--bpi-primary-bg-hover:#346f9f;--bpi-primary-border:#3f83ba;--bpi-primary-text:#eaf3fb;--bpi-en-head-text:#cbe8ff;--bpi-en-hint-text:#91b4cf;--bpi-zh-head-text:#cbe9c8;--bpi-zh-hint-text:#91ad93;--bpi-cat-name-text:#9ee8a7;--bpi-info-text:#87bfea;--bpi-link-text:#76c9ff;--bpi-zh-text:#9ed0ff;--bpi-status-error:#ff8b93;--bpi-status-ok:#7fdda2;--bpi-status-busy:#ffd27a;--bpi-badge-warn:#e2ca78;--bpi-badge-source:#86d5c4;--bpi-badge-suspected:#7fc4e0;--bpi-badge-machine:#cbb1ff;--bpi-star-on:#ffd45f;--bpi-star-off:#8f9aaa;--bpi-quick-head-text:#d9ccff;--bpi-quick-hint-text:#a795c6;--bpi-handle-text:#7194ad;--bpi-handle-hover:#a9c8e4;--bpi-mini-hover-bg:#3c4b60;--bpi-handle-hover-bg:#2b3a4d;--bpi-chip-hide-bg:#1d2937;--bpi-waiting-bg:rgba(70,52,25,.5);--bpi-waiting-border:#8a5f16;--bpi-hidden-chip-hover-bg:#31404f;--bpi-issues-bg:rgba(55,39,25,.55);--bpi-issues-border:#5b4a36;--bpi-issue-text:#e6c792;--bpi-row-hover-overlay:rgba(76,167,232,.34);--bpi-row-pinned-overlay:#314b68;--bpi-row-hover-ring:rgba(76,167,232,.7);--bpi-row-pinned-ring:#76c9ff;--bpi-row-hover-text:#fff;--bpi-row-hover-shadow:0 0 7px rgba(109,190,255,.6);--bpi-row-selected-bg:#314b68;--bpi-en-section-bg:rgba(76,167,232,.08);--bpi-quick-section-bg:rgba(167,149,198,.08);--bpi-dup-overlay:rgba(255,139,147,.22);--bpi-editor-bg:#111923}
[data-bpi-theme="dark"]{--bpi-custom-bg:#17342c;--bpi-custom-border:#3f8f77;--bpi-custom-text:#7fdcbd;--bpi-surface:rgba(10,12,18,.72);--bpi-surface-2:#171b22;--bpi-surface-3:#14181f;--bpi-surface-table:#11151b;--bpi-surface-modal:#20252d;--bpi-text:#e6edf7;--bpi-text-muted:#aab5c5;--bpi-text-faint:#7f8a9a;--bpi-border:#3a4250;--bpi-border-2:#4c5668;--bpi-border-3:#29313d;--bpi-button-bg:#2c3340;--bpi-button-text:#e8edf5;--bpi-input-bg:#171b22;--bpi-input-text:#eef3fb;--bpi-head-bg:#252c36;--bpi-danger-bg:#65313b;--bpi-danger-bg-hover:#7d3d49;--bpi-danger-border:#8d4653;--bpi-danger-text:#ffd9de;--bpi-break-bg:#17342c;--bpi-break-bg-hover:#0f6e56;--bpi-break-border:#3f8f77;--bpi-break-text:#7fdcbd;--bpi-primary-bg:#285f8e;--bpi-primary-bg-hover:#346f9f;--bpi-primary-border:#3f83ba;--bpi-primary-text:#eaf3fb;--bpi-en-head-text:#cbe8ff;--bpi-en-hint-text:#91b4cf;--bpi-zh-head-text:#cbe9c8;--bpi-zh-hint-text:#91ad93;--bpi-cat-name-text:#9ee8a7;--bpi-info-text:#87bfea;--bpi-link-text:#76c9ff;--bpi-zh-text:#9ed0ff;--bpi-status-error:#ff8b93;--bpi-status-ok:#7fdda2;--bpi-status-busy:#ffd27a;--bpi-badge-warn:#e2ca78;--bpi-badge-source:#86d5c4;--bpi-badge-suspected:#7fc4e0;--bpi-badge-machine:#cbb1ff;--bpi-star-on:#ffd45f;--bpi-star-off:#8f9aaa;--bpi-quick-head-text:#d9ccff;--bpi-quick-hint-text:#a795c6;--bpi-handle-text:#7194ad;--bpi-handle-hover:#a9c8e4;--bpi-mini-hover-bg:#3c4b60;--bpi-handle-hover-bg:#2b3a4d;--bpi-chip-hide-bg:#1d2937;--bpi-waiting-bg:rgba(70,52,25,.5);--bpi-waiting-border:#8a5f16;--bpi-hidden-chip-hover-bg:#31404f;--bpi-issues-bg:rgba(55,39,25,.55);--bpi-issues-border:#5b4a36;--bpi-issue-text:#e6c792;--bpi-row-hover-overlay:rgba(76,167,232,.34);--bpi-row-pinned-overlay:#314b68;--bpi-row-hover-ring:rgba(76,167,232,.7);--bpi-row-pinned-ring:#76c9ff;--bpi-row-hover-text:#fff;--bpi-row-hover-shadow:0 0 7px rgba(109,190,255,.6);--bpi-row-selected-bg:#314b68;--bpi-en-section-bg:rgba(76,167,232,.08);--bpi-quick-section-bg:rgba(167,149,198,.08);--bpi-dup-overlay:rgba(255,139,147,.22);--bpi-editor-bg:#111923}
[data-bpi-theme="light"]{--bpi-custom-bg:#e6f6f1;--bpi-custom-border:#7fc9b0;--bpi-custom-text:#0f6e56;--bpi-surface:rgba(255,255,255,.92);--bpi-surface-2:#f5f5f5;--bpi-surface-3:#eaeaea;--bpi-surface-table:#f0f0f0;--bpi-surface-modal:#fff;--bpi-text:#333;--bpi-text-muted:#666;--bpi-text-faint:#999;--bpi-border:#ccc;--bpi-border-2:#bbb;--bpi-border-3:#e0e0e0;--bpi-button-bg:#e8e8e8;--bpi-button-text:#333;--bpi-input-bg:#fff;--bpi-input-text:#333;--bpi-head-bg:#e8e8e8;--bpi-danger-bg:#c0392b;--bpi-danger-bg-hover:#a53023;--bpi-danger-border:#a53023;--bpi-danger-text:#fff;--bpi-break-bg:#e6f6f1;--bpi-break-bg-hover:#0f6e56;--bpi-break-border:#7fc9b0;--bpi-break-text:#0f6e56;--bpi-primary-bg:#2b6cb0;--bpi-primary-bg-hover:#245a94;--bpi-primary-border:#1e4f85;--bpi-primary-text:#fff;--bpi-en-head-text:#17557f;--bpi-en-hint-text:#5b7f9e;--bpi-zh-head-text:#1f5c22;--bpi-zh-hint-text:#5b7d5e;--bpi-cat-name-text:#1e5a28;--bpi-info-text:#1f5f96;--bpi-link-text:#1a5f96;--bpi-zh-text:#18508a;--bpi-status-error:#b3261e;--bpi-status-ok:#1a7f45;--bpi-status-busy:#8a5a00;--bpi-badge-warn:#8a5a00;--bpi-badge-source:#0f6e56;--bpi-badge-suspected:#1c5f80;--bpi-badge-machine:#5b3fa8;--bpi-star-on:#a8750a;--bpi-star-off:#6b7280;--bpi-quick-head-text:#4a3a7a;--bpi-quick-hint-text:#6b5a8f;--bpi-handle-text:#4a6a85;--bpi-handle-hover:#1f5f96;--bpi-mini-hover-bg:#d8dee7;--bpi-handle-hover-bg:#e2e8ef;--bpi-chip-hide-bg:#eef1f5;--bpi-waiting-bg:#fdf1dc;--bpi-waiting-border:#d9a95a;--bpi-hidden-chip-hover-bg:#e8eef5;--bpi-issues-bg:#fdf6e8;--bpi-issues-border:#d9b98a;--bpi-issue-text:#7a5410;--bpi-row-hover-overlay:rgba(47,108,176,.06);--bpi-row-pinned-overlay:#cfe3f7;--bpi-row-hover-ring:rgba(47,108,176,.55);--bpi-row-pinned-ring:#1a5f96;--bpi-row-hover-text:#1f1f1f;--bpi-row-hover-shadow:none;--bpi-row-selected-bg:#dcebf8;--bpi-en-section-bg:rgba(43,108,176,.06);--bpi-quick-section-bg:rgba(107,85,148,.06);--bpi-dup-overlay:rgba(179,38,30,.12);--bpi-editor-bg:#fff}
:root{--bpm-accent:#45926f;--bpm-accent-hover:#3a7d5e;--bpm-accent-border:#35755a;--bpi-custom-bg:#e6f6f1;--bpi-custom-border:#7fc9b0;--bpi-custom-text:#0f6e56;--bpi-surface:rgba(255,255,255,.92);--bpi-surface-2:#f5f5f5;--bpi-surface-3:#eaeaea;--bpi-surface-table:#f0f0f0;--bpi-surface-modal:#fff;--bpi-text:#333;--bpi-text-muted:#666;--bpi-text-faint:#999;--bpi-border:#ccc;--bpi-border-2:#bbb;--bpi-border-3:#e0e0e0;--bpi-button-bg:#e8e8e8;--bpi-button-text:#333;--bpi-input-bg:#fff;--bpi-input-text:#333;--bpi-head-bg:#e8e8e8;--bpi-danger-bg:#c0392b;--bpi-danger-bg-hover:#a53023;--bpi-danger-border:#a53023;--bpi-danger-text:#fff;--bpi-break-bg:#e6f6f1;--bpi-break-bg-hover:#0f6e56;--bpi-break-border:#7fc9b0;--bpi-break-text:#0f6e56;--bpi-primary-bg:#2b6cb0;--bpi-primary-bg-hover:#245a94;--bpi-primary-border:#1e4f85;--bpi-primary-text:#fff;--bpi-en-head-text:#17557f;--bpi-en-hint-text:#5b7f9e;--bpi-zh-head-text:#1f5c22;--bpi-zh-hint-text:#5b7d5e;--bpi-cat-name-text:#1e5a28;--bpi-info-text:#1f5f96;--bpi-link-text:#1a5f96;--bpi-zh-text:#18508a;--bpi-status-error:#b3261e;--bpi-status-ok:#1a7f45;--bpi-status-busy:#8a5a00;--bpi-badge-warn:#8a5a00;--bpi-badge-source:#0f6e56;--bpi-badge-suspected:#1c5f80;--bpi-badge-machine:#5b3fa8;--bpi-star-on:#a8750a;--bpi-star-off:#6b7280;--bpi-quick-head-text:#4a3a7a;--bpi-quick-hint-text:#6b5a8f;--bpi-handle-text:#4a6a85;--bpi-handle-hover:#1f5f96;--bpi-mini-hover-bg:#d8dee7;--bpi-handle-hover-bg:#e2e8ef;--bpi-chip-hide-bg:#eef1f5;--bpi-waiting-bg:#fdf1dc;--bpi-waiting-border:#d9a95a;--bpi-hidden-chip-hover-bg:#e8eef5;--bpi-issues-bg:#fdf6e8;--bpi-issues-border:#d9b98a;--bpi-issue-text:#7a5410;--bpi-row-hover-overlay:rgba(47,108,176,.06);--bpi-row-pinned-overlay:#cfe3f7;--bpi-row-hover-ring:rgba(47,108,176,.55);--bpi-row-pinned-ring:#1a5f96;--bpi-row-hover-text:#1f1f1f;--bpi-row-hover-shadow:none;--bpi-row-selected-bg:#dcebf8;--bpi-en-section-bg:rgba(43,108,176,.06);--bpi-quick-section-bg:rgba(107,85,148,.06);--bpi-dup-overlay:rgba(179,38,30,.12);--bpi-editor-bg:#fff;--bpi-hit-bg:#fbe49c;--bpi-hit-text:#7a5410}
.dark-theme{--bpi-custom-bg:#17342c;--bpi-custom-border:#3f8f77;--bpi-custom-text:#7fdcbd;--bpi-surface:rgba(10,12,18,.72);--bpi-surface-2:#171b22;--bpi-surface-3:#14181f;--bpi-surface-table:#11151b;--bpi-surface-modal:#20252d;--bpi-text:#e6edf7;--bpi-text-muted:#aab5c5;--bpi-text-faint:#7f8a9a;--bpi-border:#3a4250;--bpi-border-2:#4c5668;--bpi-border-3:#29313d;--bpi-button-bg:#2c3340;--bpi-button-text:#e8edf5;--bpi-input-bg:#171b22;--bpi-input-text:#eef3fb;--bpi-head-bg:#252c36;--bpi-danger-bg:#65313b;--bpi-danger-bg-hover:#7d3d49;--bpi-danger-border:#8d4653;--bpi-danger-text:#ffd9de;--bpi-break-bg:#17342c;--bpi-break-bg-hover:#0f6e56;--bpi-break-border:#3f8f77;--bpi-break-text:#7fdcbd;--bpi-primary-bg:#285f8e;--bpi-primary-bg-hover:#346f9f;--bpi-primary-border:#3f83ba;--bpi-primary-text:#eaf3fb;--bpi-en-head-text:#cbe8ff;--bpi-en-hint-text:#91b4cf;--bpi-zh-head-text:#cbe9c8;--bpi-zh-hint-text:#91ad93;--bpi-cat-name-text:#9ee8a7;--bpi-info-text:#87bfea;--bpi-link-text:#76c9ff;--bpi-zh-text:#9ed0ff;--bpi-status-error:#ff8b93;--bpi-status-ok:#7fdda2;--bpi-status-busy:#ffd27a;--bpi-badge-warn:#e2ca78;--bpi-badge-source:#86d5c4;--bpi-badge-suspected:#7fc4e0;--bpi-badge-machine:#cbb1ff;--bpi-star-on:#ffd45f;--bpi-star-off:#8f9aaa;--bpi-quick-head-text:#d9ccff;--bpi-quick-hint-text:#a795c6;--bpi-handle-text:#7194ad;--bpi-handle-hover:#a9c8e4;--bpi-mini-hover-bg:#3c4b60;--bpi-handle-hover-bg:#2b3a4d;--bpi-chip-hide-bg:#1d2937;--bpi-waiting-bg:rgba(70,52,25,.5);--bpi-waiting-border:#8a5f16;--bpi-hidden-chip-hover-bg:#31404f;--bpi-issues-bg:rgba(55,39,25,.55);--bpi-issues-border:#5b4a36;--bpi-issue-text:#e6c792;--bpi-row-hover-overlay:rgba(76,167,232,.34);--bpi-row-pinned-overlay:#314b68;--bpi-row-hover-ring:rgba(76,167,232,.7);--bpi-row-pinned-ring:#76c9ff;--bpi-row-hover-text:#fff;--bpi-row-hover-shadow:0 0 7px rgba(109,190,255,.6);--bpi-row-selected-bg:#314b68;--bpi-en-section-bg:rgba(76,167,232,.08);--bpi-quick-section-bg:rgba(167,149,198,.08);--bpi-dup-overlay:rgba(255,139,147,.22);--bpi-editor-bg:#111923;--bpi-hit-bg:#5a4413;--bpi-hit-text:#ffe6a8}
[data-bpi-theme="dark"]{--bpi-custom-bg:#17342c;--bpi-custom-border:#3f8f77;--bpi-custom-text:#7fdcbd;--bpi-surface:rgba(10,12,18,.72);--bpi-surface-2:#171b22;--bpi-surface-3:#14181f;--bpi-surface-table:#11151b;--bpi-surface-modal:#20252d;--bpi-text:#e6edf7;--bpi-text-muted:#aab5c5;--bpi-text-faint:#7f8a9a;--bpi-border:#3a4250;--bpi-border-2:#4c5668;--bpi-border-3:#29313d;--bpi-button-bg:#2c3340;--bpi-button-text:#e8edf5;--bpi-input-bg:#171b22;--bpi-input-text:#eef3fb;--bpi-head-bg:#252c36;--bpi-danger-bg:#65313b;--bpi-danger-bg-hover:#7d3d49;--bpi-danger-border:#8d4653;--bpi-danger-text:#ffd9de;--bpi-break-bg:#17342c;--bpi-break-bg-hover:#0f6e56;--bpi-break-border:#3f8f77;--bpi-break-text:#7fdcbd;--bpi-primary-bg:#285f8e;--bpi-primary-bg-hover:#346f9f;--bpi-primary-border:#3f83ba;--bpi-primary-text:#eaf3fb;--bpi-en-head-text:#cbe8ff;--bpi-en-hint-text:#91b4cf;--bpi-zh-head-text:#cbe9c8;--bpi-zh-hint-text:#91ad93;--bpi-cat-name-text:#9ee8a7;--bpi-info-text:#87bfea;--bpi-link-text:#76c9ff;--bpi-zh-text:#9ed0ff;--bpi-status-error:#ff8b93;--bpi-status-ok:#7fdda2;--bpi-status-busy:#ffd27a;--bpi-badge-warn:#e2ca78;--bpi-badge-source:#86d5c4;--bpi-badge-suspected:#7fc4e0;--bpi-badge-machine:#cbb1ff;--bpi-star-on:#ffd45f;--bpi-star-off:#8f9aaa;--bpi-quick-head-text:#d9ccff;--bpi-quick-hint-text:#a795c6;--bpi-handle-text:#7194ad;--bpi-handle-hover:#a9c8e4;--bpi-mini-hover-bg:#3c4b60;--bpi-handle-hover-bg:#2b3a4d;--bpi-chip-hide-bg:#1d2937;--bpi-waiting-bg:rgba(70,52,25,.5);--bpi-waiting-border:#8a5f16;--bpi-hidden-chip-hover-bg:#31404f;--bpi-issues-bg:rgba(55,39,25,.55);--bpi-issues-border:#5b4a36;--bpi-issue-text:#e6c792;--bpi-row-hover-overlay:rgba(76,167,232,.34);--bpi-row-pinned-overlay:#314b68;--bpi-row-hover-ring:rgba(76,167,232,.7);--bpi-row-pinned-ring:#76c9ff;--bpi-row-hover-text:#fff;--bpi-row-hover-shadow:0 0 7px rgba(109,190,255,.6);--bpi-row-selected-bg:#314b68;--bpi-en-section-bg:rgba(76,167,232,.08);--bpi-quick-section-bg:rgba(167,149,198,.08);--bpi-dup-overlay:rgba(255,139,147,.22);--bpi-editor-bg:#111923;--bpi-hit-bg:#5a4413;--bpi-hit-text:#ffe6a8}
[data-bpi-theme="light"]{--bpi-custom-bg:#e6f6f1;--bpi-custom-border:#7fc9b0;--bpi-custom-text:#0f6e56;--bpi-surface:rgba(255,255,255,.92);--bpi-surface-2:#f5f5f5;--bpi-surface-3:#eaeaea;--bpi-surface-table:#f0f0f0;--bpi-surface-modal:#fff;--bpi-text:#333;--bpi-text-muted:#666;--bpi-text-faint:#999;--bpi-border:#ccc;--bpi-border-2:#bbb;--bpi-border-3:#e0e0e0;--bpi-button-bg:#e8e8e8;--bpi-button-text:#333;--bpi-input-bg:#fff;--bpi-input-text:#333;--bpi-head-bg:#e8e8e8;--bpi-danger-bg:#c0392b;--bpi-danger-bg-hover:#a53023;--bpi-danger-border:#a53023;--bpi-danger-text:#fff;--bpi-break-bg:#e6f6f1;--bpi-break-bg-hover:#0f6e56;--bpi-break-border:#7fc9b0;--bpi-break-text:#0f6e56;--bpi-primary-bg:#2b6cb0;--bpi-primary-bg-hover:#245a94;--bpi-primary-border:#1e4f85;--bpi-primary-text:#fff;--bpi-en-head-text:#17557f;--bpi-en-hint-text:#5b7f9e;--bpi-zh-head-text:#1f5c22;--bpi-zh-hint-text:#5b7d5e;--bpi-cat-name-text:#1e5a28;--bpi-info-text:#1f5f96;--bpi-link-text:#1a5f96;--bpi-zh-text:#18508a;--bpi-status-error:#b3261e;--bpi-status-ok:#1a7f45;--bpi-status-busy:#8a5a00;--bpi-badge-warn:#8a5a00;--bpi-badge-source:#0f6e56;--bpi-badge-suspected:#1c5f80;--bpi-badge-machine:#5b3fa8;--bpi-star-on:#a8750a;--bpi-star-off:#6b7280;--bpi-quick-head-text:#4a3a7a;--bpi-quick-hint-text:#6b5a8f;--bpi-handle-text:#4a6a85;--bpi-handle-hover:#1f5f96;--bpi-mini-hover-bg:#d8dee7;--bpi-handle-hover-bg:#e2e8ef;--bpi-chip-hide-bg:#eef1f5;--bpi-waiting-bg:#fdf1dc;--bpi-waiting-border:#d9a95a;--bpi-hidden-chip-hover-bg:#e8eef5;--bpi-issues-bg:#fdf6e8;--bpi-issues-border:#d9b98a;--bpi-issue-text:#7a5410;--bpi-row-hover-overlay:rgba(47,108,176,.06);--bpi-row-pinned-overlay:#cfe3f7;--bpi-row-hover-ring:rgba(47,108,176,.55);--bpi-row-pinned-ring:#1a5f96;--bpi-row-hover-text:#1f1f1f;--bpi-row-hover-shadow:none;--bpi-row-selected-bg:#dcebf8;--bpi-en-section-bg:rgba(43,108,176,.06);--bpi-quick-section-bg:rgba(107,85,148,.06);--bpi-dup-overlay:rgba(179,38,30,.12);--bpi-editor-bg:#fff;--bpi-hit-bg:#fbe49c;--bpi-hit-text:#7a5410}
.bpi-panel{box-sizing:border-box;width:100%;height:100%;min-height:var(--bpi-collapsed-height,390px);max-height:var(--bpi-collapsed-height,390px);padding:8px;display:flex;flex-direction:column;gap:7px;color:var(--bpi-text);font:12px/1.4 Arial,sans-serif;background:var(--bpi-surface);border:1px solid var(--bpi-border);border-radius:8px;overflow-y:auto;scrollbar-gutter:stable}
.bpi-english-section{flex:none;border:1px solid #3e7197;border-radius:7px;background-color:var(--bpi-en-section-bg);overflow:hidden}.bpi-english-head{display:flex;align-items:center;gap:7px;padding:5px 8px;background:var(--bpi-head-bg);color:var(--bpi-en-head-text);font-weight:700;flex-wrap:wrap}.bpi-english-hint{margin-left:auto;color:var(--bpi-en-hint-text);font-size:10px;font-weight:400}.bpi-english-token-view{box-sizing:border-box;width:100%;height:auto;min-height:92px;max-height:360px;overflow:auto;scrollbar-gutter:stable;padding:8px 9px;outline:none;color:var(--bpi-text);font:12px/1.85 Consolas,monospace;white-space:normal}.bpi-english-token-view:focus{box-shadow:inset 0 0 0 1px #5aa6d8}.bpi-english-editor{box-sizing:border-box;width:100%;height:120px;min-height:92px;max-height:420px;overflow-y:auto;resize:vertical;border:0;border-top:1px solid #3e7197;background-color:transparent;color:var(--bpi-text);padding:9px;outline:none;font:12px/1.55 Consolas,monospace}.bpi-english-editor:focus{box-shadow:inset 0 0 0 1px #5aa6d8}.bpi-english-token{font-family:Consolas,monospace;color:var(--bpi-text)}.bpi-english-token.bpi-linked{background:#147fc3;border-color:#76c9ff;color:#fff;box-shadow:0 0 0 1px rgba(118,201,255,.32)}
.bpi-mirror-section{flex:none;border:1px solid #488739;border-radius:7px;background:var(--bpi-surface);overflow:visible}.bpi-section-head{display:flex;align-items:center;gap:7px;padding:5px 8px;background:var(--bpi-head-bg);color:var(--bpi-zh-head-text);font-weight:700;flex-wrap:wrap}.bpi-section-hint{margin-left:auto;color:var(--bpi-zh-hint-text);font-size:10px;font-weight:400}.bpi-mirror-actions{display:flex;gap:4px;align-items:center;flex-wrap:wrap}.bpi-chinese-mirror{box-sizing:border-box;width:100%;height:auto;min-height:92px;max-height:360px;overflow:auto;resize:none;scrollbar-gutter:stable;padding:8px 9px;outline:none;color:var(--bpi-text);line-height:1.85;cursor:text;white-space:normal}.bpi-chinese-mirror:focus{box-shadow:inset 0 0 0 1px #70b35f}.bpi-category-table{display:grid;grid-template-columns:minmax(110px,150px) minmax(0,1fr);border:1px solid #315f3a;border-radius:6px;overflow:hidden;background:var(--bpi-surface-3)}.bpi-category-row{display:contents}.bpi-category-name,.bpi-category-content{padding:6px 8px;border-top:1px solid #31513a}.bpi-category-row:first-child .bpi-category-name,.bpi-category-row:first-child .bpi-category-content{border-top:0}.bpi-category-name{background:var(--bpi-head-bg);border-right:1px solid #31513a;color:var(--bpi-cat-name-text);font-weight:700}.bpi-category-content{min-width:0}.bpi-chinese-editor{box-sizing:border-box;width:100%;height:auto;min-height:110px;max-height:600px;overflow-y:auto;resize:vertical;border:0;border-top:1px solid #3b693c;background:var(--bpi-editor-bg);color:var(--bpi-text);padding:9px;outline:none;font:12px/1.65 Arial,sans-serif}.bpi-chinese-editor:focus{box-shadow:inset 0 0 0 1px #70b35f}.bpi-hidden{display:none!important}.bpi-mirror-empty{color:var(--bpi-text-muted)}.bpi-mirror-token{display:inline-flex;align-items:center;border:1px solid transparent;border-radius:5px;padding:0 3px;margin:1px 0;cursor:pointer;transition:background .12s,border-color .12s,color .12s}.bpi-mirror-token:hover{background:#345a3b;border-color:#5c8d63;color:#fff}.bpi-mirror-token.bpi-linked{background:#315f83;border-color:#76a9ff;color:#fff}
@@ -1231,11 +1231,12 @@ function injectBpiStyles() {
.bpi-summary{color:var(--bpi-text-muted);font-size:11px;min-height:17px}.bpi-status{margin-left:auto}.bpi-status[data-kind="error"]{color:var(--bpi-status-error)}.bpi-status[data-kind="ok"]{color:var(--bpi-status-ok)}.bpi-status[data-kind="busy"]{color:var(--bpi-status-busy)}
.bpi-issues{display:none;max-height:105px;overflow:auto;border:1px solid var(--bpi-issues-border);border-radius:6px;background:var(--bpi-issues-bg);padding:4px 6px}.bpi-issues.bpi-visible{display:block}.bpi-issue{padding:2px 4px;color:var(--bpi-issue-text)}.bpi-issue[data-severity="error"]{color:var(--bpi-status-error)}.bpi-issue[data-severity="info"]{color:var(--bpi-info-text)}.bpi-issue::before{content:"⚠ ";}.bpi-issue[data-severity="error"]::before{content:"⛔ ";}.bpi-issue[data-severity="info"]::before{content:"ℹ ";}
.bpi-search-label{color:var(--bpi-text-muted);font-size:10px;white-space:nowrap}.bpi-results{display:none;max-height:220px;overflow:auto;border:1px solid var(--bpi-border-2);border-radius:6px;background:var(--bpi-surface-2)}.bpi-results.bpi-visible{display:block}.bpi-result{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr) minmax(150px,auto);gap:7px;padding:5px 7px;border-bottom:1px solid var(--bpi-border-3);cursor:pointer}.bpi-result:last-child{border-bottom:0}.bpi-result:hover{background:var(--bpi-surface-3)}.bpi-result-en{color:var(--bpi-text)}.bpi-result-zh{color:var(--bpi-zh-text)}.bpi-category{color:var(--bpi-text-muted);font-size:10px;white-space:normal}.bpi-search-reason{display:block;color:var(--bpi-info-text);margin-top:2px}.bpi-search-more{display:block;margin:7px auto}.bpi-results>.bpi-searching{text-align:center}
.bpi-table{position:relative;height:300px;min-height:180px;max-height:360px;flex:1 1 300px;overflow-x:hidden;overflow-y:scroll;scrollbar-gutter:stable;border:1px solid var(--bpi-border-2);border-radius:6px;background:var(--bpi-surface-table)}.bpi-split-handle{position:absolute;top:0;bottom:0;left:var(--bpi-split,40%);width:9px;margin-left:-4px;cursor:col-resize;z-index:4}.bpi-split-handle:hover{background:rgba(76,167,232,.35)}.bpi-head,.bpi-row{display:grid;grid-template-columns:var(--bpi-split,minmax(0,2fr)) minmax(0,1fr);gap:1px}.bpi-head{position:sticky;top:0;z-index:2;background:var(--bpi-head-bg);color:var(--bpi-text-muted);font-weight:700}.bpi-head>div,.bpi-cell{padding:6px 8px}.bpi-head>div+div,.bpi-cell+.bpi-cell{border-left:1px solid var(--bpi-border-2)}
.bpi-table{position:relative;height:300px;min-height:180px;max-height:360px;flex:1 1 300px;overflow-x:hidden;overflow-y:scroll;scrollbar-gutter:stable;border:1px solid var(--bpi-border-2);border-radius:0 0 6px 6px;background:var(--bpi-surface-table)}.bpi-split-handle{position:absolute;top:0;bottom:0;left:var(--bpi-split,40%);width:9px;margin-left:-4px;cursor:col-resize;z-index:4}.bpi-split-handle:hover{background:rgba(76,167,232,.35)}.bpi-head,.bpi-row{display:grid;grid-template-columns:var(--bpi-split,minmax(0,2fr)) minmax(0,1fr);gap:1px}.bpi-head{position:sticky;top:0;z-index:2;background:var(--bpi-head-bg);color:var(--bpi-text-muted);font-weight:700}.bpi-head>div,.bpi-cell{padding:6px 8px}.bpi-head>div+div,.bpi-cell+.bpi-cell{border-left:1px solid var(--bpi-border-2)}
.bpi-row{border-top:1px solid var(--bpi-border-3);cursor:pointer}.bpi-row:hover{background-image:linear-gradient(var(--bpi-row-hover-overlay),var(--bpi-row-hover-overlay));outline:1px dashed var(--bpi-row-hover-ring);outline-offset:-1px}.bpi-row.bpi-pinned{background-image:linear-gradient(var(--bpi-row-pinned-overlay),var(--bpi-row-pinned-overlay));outline:1px solid var(--bpi-row-pinned-ring);outline-offset:-1px;font-weight:500}.bpi-row:hover .bpi-cell,.bpi-row.bpi-pinned .bpi-cell{color:var(--bpi-row-hover-text)}.bpi-row.bpi-unknown{background-color:rgba(112,70,28,.18)}.bpi-row.bpi-machine{background-color:rgba(85,62,131,.22)}.bpi-row.bpi-suspected-natural{background-color:rgba(58,132,160,.20)}.bpi-row.bpi-has-warning{box-shadow:inset 3px 0 #d39845}.bpi-row.bpi-has-error{box-shadow:inset 3px 0 #e35b66}
.bpi-cell{min-width:0;word-break:break-word;display:flex;align-items:flex-start;gap:5px}.bpi-en{color:var(--bpi-text);font-family:Consolas,monospace}.bpi-zh{color:var(--bpi-zh-text)}.bpi-row:hover .bpi-en,.bpi-row:hover .bpi-zh,.bpi-row.bpi-pinned .bpi-en,.bpi-row.bpi-pinned .bpi-zh{color:var(--bpi-row-hover-text);text-shadow:var(--bpi-row-hover-shadow)}
.bpi-badge{flex:none;border:1px solid var(--bpi-border-2);border-radius:8px;padding:0 5px;color:var(--bpi-text-muted);font-size:9px;line-height:15px}.bpi-source{border-color:#3e6f67;color:var(--bpi-badge-source)}.bpi-suspected{border-color:#3a84a0;color:var(--bpi-badge-suspected)}.bpi-confidence-high{border-color:#3f785c;color:var(--bpi-status-ok)}.bpi-confidence-medium{border-color:#8a7538;color:var(--bpi-badge-warn)}.bpi-confidence-low,.bpi-confidence-none{border-color:#7a4c55;color:var(--bpi-status-error)}.bpi-unknown .bpi-badge{color:var(--bpi-badge-warn);border-color:#85602f}.bpi-machine .bpi-badge{color:var(--bpi-badge-machine);border-color:#6b5594}.bpi-inline-actions{display:flex;gap:3px;margin-left:auto;flex-wrap:wrap}.bpi-mini{border:1px solid var(--bpi-border-2);border-radius:4px;background:var(--bpi-button-bg);color:var(--bpi-button-text);padding:1px 5px;cursor:pointer;font-size:9px}.bpi-mini:hover{background:var(--bpi-mini-hover-bg)}.bpi-inline-editor{min-width:90px;flex:1;border:1px solid #4ca7e8;border-radius:4px;background:var(--bpi-input-bg);color:var(--bpi-input-text);padding:3px 5px}.bpi-empty{padding:24px;text-align:center;color:var(--bpi-text-faint)}
.bpi-filters{display:flex;gap:4px;align-items:center;flex-wrap:wrap}
.bpi-table-search{flex:none;display:flex;align-items:center;gap:6px;box-sizing:border-box;padding:4px 6px;margin-bottom:-7px;border:1px solid var(--bpi-border-2);border-bottom:0;border-radius:6px 6px 0 0;background-color:var(--bpi-head-bg)}.bpi-table-search-icon{flex:none;display:inline-flex;color:var(--bpi-text-faint)}.bpi-search.bpi-table-search-input{flex:1;min-width:0;padding:3px 6px}.bpi-table-search-count{flex:none;color:var(--bpi-text-muted);font-size:10px;white-space:nowrap}.bpi-table-search-clear{flex:none;display:none;border:0;background-color:transparent;color:var(--bpi-text-muted);cursor:pointer;font-size:13px;line-height:1;padding:1px 3px;border-radius:3px}.bpi-table-search-clear.bpi-visible{display:block}.bpi-table-search-clear:hover{color:var(--bpi-text);background-color:var(--bpi-border-3)}.bpi-hit{background-color:var(--bpi-hit-bg);color:var(--bpi-hit-text);border-radius:2px}
.bpi-batch-bar{flex:none;gap:8px;padding:6px 8px;margin-bottom:6px;border:1px solid var(--bpi-border-2);border-radius:6px;background:var(--bpi-surface-3)}.bpi-filter-active{background:#365f84;border-color:#5792c4;color:#fff}.bpi-star{color:var(--bpi-star-off)}.bpi-star.bpi-starred{color:var(--bpi-star-on);border-color:#8f772f}.bpi-result.bpi-result-selected{background:var(--bpi-row-selected-bg);outline:1px solid var(--bpi-link-text)}.bpi-result-star{font-size:14px;color:var(--bpi-star-off);align-self:center}.bpi-result-star.bpi-starred{color:var(--bpi-star-on)}
.bpi-quick-section{flex:none;border:1px solid #6b5594;border-radius:7px;background-color:var(--bpi-quick-section-bg);overflow:hidden}.bpi-quick-head{display:flex;align-items:center;gap:7px;padding:5px 8px;background:var(--bpi-head-bg);color:var(--bpi-quick-head-text);font-weight:700;flex-wrap:wrap}.bpi-quick-hint{margin-left:auto;color:var(--bpi-quick-hint-text);font-size:10px;font-weight:400}.bpi-quick-target{padding:0 8px 6px;font-size:11px;color:var(--bpi-text-muted)}.bpi-quick-target-linked{color:var(--bpi-link-text)}.bpi-quick-row{display:flex;gap:6px;align-items:flex-start;padding:6px 8px}.bpi-quick-input{flex:1;min-width:0;box-sizing:border-box;height:44px;min-height:44px;max-height:140px;overflow-y:auto;resize:vertical;border:1px solid var(--bpi-border-2);border-radius:5px;background:var(--bpi-input-bg);color:var(--bpi-input-text);padding:5px 7px;outline:none;font:12px/1.55 Arial,sans-serif}.bpi-quick-input:focus{box-shadow:inset 0 0 0 1px #a98be0}.bpi-quick-row .bpi-button{flex:none}
.bpi-drag-handle{flex:none;cursor:grab;color:var(--bpi-handle-text);font-size:11px;line-height:1;user-select:none;padding:1px 2px;border-radius:3px}.bpi-drag-handle:hover{color:var(--bpi-handle-hover);background:var(--bpi-handle-hover-bg)}.bpi-drag-handle:active{cursor:grabbing}
+10 -2
View File
@@ -107,7 +107,9 @@ const ZH = {
"Re-translate": "重新翻译",
"Translate all unknown": "翻译全部未收录",
"Sort by Anima order": "按 Anima 顺序排序",
"Confirm write English": "确认写入英文",
"Cancel Anima order sort": "取消 Anima 顺序排序",
"Cancel Anima sort": "取消 Anima 排序",
"Confirm write new prompt": "确认写入新提示词",
"Confirm & save": "确认并保存",
"Save to dict": "存入词库",
"Save to Personal Dictionary": "保存到个人词库",
@@ -144,6 +146,10 @@ const ZH = {
"Personal dictionary": "个人词库",
"No inspector node on canvas": "画布上没有检查器节点",
"No matching items under the current filter.": "当前筛选条件下没有匹配项。",
"Find in the current prompt": "在当前提示词里查找",
"Clear search": "清除查找",
"No tag matches {}": "没有匹配“…”的标签",
"Filter the detail list by keyword; matches the English original or the Chinese explanation; the prompt itself never changes": "按关键词筛明细表;英文原文和中文解释都参与匹配,提示词本身不会变",
"No unknown tags currently": "当前没有未收录标签",
"Edit directly": "直接编辑",
@@ -346,7 +352,6 @@ const ZH = {
Packs: "词包",
Favorites: "收藏",
"Assistant Settings": "助手设置",
"Manager panel": "管理面板",
"Add or select a BilingualPromptInspector node to see its tag manager here.":
"添加或选中画布上的 BilingualPromptInspector 节点,即可在此看到它的标签管理。",
"Tag translation, dictionary search and per-tag operations edit the selected node below.":
@@ -481,6 +486,9 @@ const ZH = {
"English prompt tag view": "英文提示词标签视图",
"Anima official sort": "Anima 官方顺序",
"Anima official order preview": "Anima 官方顺序预览",
"Restore the prompt as it was before the Anima sort": "恢复到 Anima 排序前的提示词(分类视图一起退出)",
"Restored the prompt from before the Anima sort": "已恢复到 Anima 排序前的提示词",
"Exited the Anima category view": "已退出 Anima 分类视图",
"Empty input": "输入为空",
"Copied": "已复制",
+26 -2
View File
@@ -136,6 +136,9 @@ function rebuildSourceOptions() {
["all", "All Packs"],
["builtin", "Built-in pack enabled"],
["personal", "Personal"],
// 保存时选了「自然语言词库」的条目:和个人词库共用文件,只靠 natural 标记区分,
// 与分类叫「自然语言」的标签是两回事(后者是 Anima 分类名)。
["natural", "Natural Language Dictionary"],
...(manager.data.large_dictionary?.available ? [["large", "Danbooru Large Dict"]] : []),
["machine", "Pending Machine"],
["favorites", "Favorites"],
@@ -149,6 +152,13 @@ function rebuildSourceOptions() {
refs.source.value = options.some(([value]) => value === current) ? current : "all";
}
// 自然语言跟普通标签分类不是一个层级,按拼音排会掉到很后面;
// 词库里这个分类名中英两种写法都有(Anima 分类是英文,手填的多是中文),都认。
function isNaturalCategory(name) {
const value = String(name ?? "").trim().toLowerCase();
return value === "自然语言" || value === "natural language";
}
function rebuildCategoryOptions() {
const current = refs.category.value;
refs.category.replaceChildren();
@@ -156,7 +166,7 @@ function rebuildCategoryOptions() {
all.value = "all";
refs.category.appendChild(all);
const categories = [...new Set(managerRows().map((row) => row.tag.category).filter(Boolean))].sort((a, b) => a.localeCompare(b, "zh-CN"));
for (const category of categories) {
for (const category of [...categories.filter(isNaturalCategory), ...categories.filter((name) => !isNaturalCategory(name))]) {
const option = element("option", "", category);
option.value = category;
refs.category.appendChild(option);
@@ -218,9 +228,10 @@ function renderRows() {
const category = refs.category.value;
const source = refs.source.value;
const rows = managerRows().filter((row) => {
if (source === "natural" && !row.tag.natural) return false;
if (source === "favorites" && !isFavorite(row.tag.english)) return false;
if (source.startsWith("pack:") && row.tag.pack_id !== source.slice(5)) return false;
if (!["all", "favorites"].includes(source) && !source.startsWith("pack:") && row.kind !== source) return false;
if (!["all", "favorites", "natural"].includes(source) && !source.startsWith("pack:") && row.kind !== source) return false;
if (category !== "all" && row.tag.category !== category) return false;
if (query) {
const haystack = [row.tag.english, row.tag.chinese, row.tag.category, ...(row.tag.aliases ?? [])].join("\n").toLowerCase();
@@ -1109,8 +1120,21 @@ function inspectorNodes() {
return (app.graph?._nodes ?? []).filter((node) => node._bilingualPromptInspector?.detailsBody);
}
// 用户在画布上双击节点顶部改名后,列表里显示他自己起的名字;
// 名字后面保留节点号,免得两个节点改成同一个名字时认不出来。
function customNodeTitle(node) {
const current = String(node.title ?? "").trim();
// defaultNodeTitle 是节点创建时记下的出厂标题;拿不到就退回节点类型的默认标题,
// 两者都拿不到时不做判断,免得把出厂标题当成用户改过的名字。
const factory = String(node._bilingualPromptInspector?.defaultNodeTitle ?? node.constructor?.title ?? "").trim();
if (!current || !factory || current === factory) return null;
return current;
}
function tagNodeLabel(node) {
const text = String(node.widgets?.find((widget) => widget.name === "text")?.value ?? "").trim();
const custom = customNodeTitle(node);
if (custom) return `${custom} · ${t(`Node #${node.id}`)}`;
return `${t(`Node #${node.id}`)}: ${text ? text.slice(0, 30) : t("(empty prompt)")}`;
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "comfyui-bilingual-prompt-inspector",
"version": "2.0.0",
"version": "2.0.6",
"description": "ComfyUI bilingual prompt inspection, translation, Anima sorting, and on-demand Danbooru dictionary extension.",
"private": true,
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
[project]
name = "prompt-translator-manager"
version = "2.0.0"
version = "2.0.6"
# 与 Comfy Registry 页面上显示的描述保持一致: publish 会用这里的值覆盖网页上的手改
description = "ComfyUI prompt node with translation and management: bilingual tag view, drag-to-reorder, temporary hiding, multiple translation services and a sidebar dictionary manager.\nComfyUI 提示节点,支持翻译和管理:双语标签视图、拖拽排序、临时隐藏、多翻译服务和侧边栏词典管理器。"
license = { file = "LICENSE" }