diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e17095..114c51e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## v1.63.0 — unreleased +- 发版前把每个页面按中英文、深浅色和 480 窄宽走了一遍,修了一批小问题。浅色主题下,模型整理页的标题和文件名、任务上“接着做”的标签原来几乎看不见。窄窗口下,任务的“接着哪个任务”标签伸出窗口、× 点不到,设置页的主题按钮把页面撑出横向滚动,模型页的搜索框压住标题,工作流详情的“新预设”被切掉。 +- 英文界面的列表和冒号不再是中文标点,“1 saves”这类单复数对了,左边栏的 AI 工作台写作 “AI”,一条漏翻的提示补上了。深色主题下浅紫色按钮和角标上的字改成深色,看得更清楚。 +- A round through every page in both languages, all themes and at 480 fixed small things. In the light theme, the Tidy page's headings and file names and a task's "goes on in" chip could hardly be read. In a narrow window, a task's "goes on from" chip ran out of the window with its × out of reach, the theme buttons in Settings pushed the page sideways, the Models search sat over the title, and "New preset" was cut off. +- The English window no longer shows Chinese punctuation in lists and after labels, "1 saves" and the like have the right plural, the rail says "AI" for the AI workbench, and one line that was left in Chinese is translated. In the dark themes, text on pale accent buttons and badges is dark now, so it reads better. - 侧栏下面的工具按常用程度排了:当前节点在最上面,然后是模型检查,模型扫描在最下面。 - The rail's tools go by how often they are used: Current node first, then Model Check, with the scan last. - 当前节点的参数卡清爽了:来源不再一条一行,改成按工作流、搭配、图片、这里存的分开的小标签,同一渠道几条合成一个,点标签直接过去,几条的话先列出来。 diff --git a/tests/words_in_language.mjs b/tests/words_in_language.mjs new file mode 100644 index 0000000..6beceb1 --- /dev/null +++ b/tests/words_in_language.mjs @@ -0,0 +1,20 @@ +// What the code puts together itself goes through the locale: `.join('、')` or a ":" in a template shows +// Chinese punctuation in the English window. tts_setup_api.js runs without the window's words. +import assert from 'node:assert/strict'; +import { readFileSync, readdirSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const folder = join(dirname(fileURLToPath(import.meta.url)), '..', 'web', 'modules'); +const LEFT_OUT = new Set(['locales.js', 'tts_setup_api.js']); +const CHINESE_JOIN = [/\.join\('[、:]'\)/, /:\$\{/, /\}:[`$]/, /(\$\{/]; + +const found = []; +for (const name of readdirSync(folder).filter(file => file.endsWith('.js') && !LEFT_OUT.has(file))) { + readFileSync(join(folder, name), 'utf8').split(/\r?\n/).forEach((line, index) => { + if (/^\s*(\/\/|\*|\/\*)/.test(line)) return; + if (CHINESE_JOIN.some(pattern => pattern.test(line))) found.push(`${name}:${index + 1}: ${line.trim()}`); + }); +} +assert.deepEqual(found, [], 'use t(\'listSeparator\') and t(\'wordColon\')'); +console.log('words in the shown language: ok'); diff --git a/web/modules/locales.js b/web/modules/locales.js index aac2b1d..f9ddfdb 100644 --- a/web/modules/locales.js +++ b/web/modules/locales.js @@ -1220,6 +1220,9 @@ export const i18n = { homeStartNews: "看更新说明", shellActivity: "记录", shellAi: "AI 工作台", + shellAiRail: "AI 工作台", + listSeparator: "、", + wordColon: ":", aiTasks: "AI 的任务", aiTaskCount: "{count} 个", aiUntitled: "未命名任务", @@ -4310,7 +4313,7 @@ export const i18n = { paramSetDiffers: "{count} different from the default", paramSetSame: "Same as the default", paramSetCompare: "Compare with the default", - paramSetCount: "{count} presets", + paramSetCount: "{count} {count|preset|presets}", workflowsSteps: "{count} {count|step|steps}", recipePromptShowAll: "Show all", compareWithDefault: "Compare with the default", @@ -4318,7 +4321,7 @@ export const i18n = { workflowOwnValuesHint: "The values saved with the workflow", workflowOwnValuesShort: "Saved with it", workflowPresetDiffers: "{count} different", - workflowPresetMarked: "{count} values unlike the default, orange in the flow above", + workflowPresetMarked: "{count} {count|value|values} unlike the default, orange in the flow above", workflowPresetAdd: "New preset", workflowPresetFromShown: "New from the one shown", workflowSetFromCanvas: "From canvas", @@ -4784,7 +4787,7 @@ export const i18n = { mainFloatingTriggerStyleIcon: 'Circular icon', mainFloatingTriggerStylePill: 'Labeled pill', mainResetFloatingTriggerPosition: 'Reset floating entry position', - mainRetryInit: '插件界面初始化失败,点击可重试', + mainRetryInit: 'The plugin window failed to start. Click to try again', mainRetryInitEn: 'The plugin UI failed to initialize. Click to retry.', mainShareFingerprints: "{hashed} of {total} {total|model|models} carry a fingerprint, so others can find the same file even under another name.", mainShareNoFingerprint: "{count} have none (not scanned, or not on this computer); others can only go by name or size, so scan first if you can.", @@ -4872,6 +4875,9 @@ export const i18n = { homeStartNews: "What's new", shellActivity: "Activity", shellAi: "AI workbench", + shellAiRail: "AI", + listSeparator: ", ", + wordColon: ": ", aiTasks: "The AI's tasks", aiTaskCount: "{count}", aiUntitled: "Untitled task", @@ -4905,7 +4911,7 @@ export const i18n = { aiMapDenoise: "denoise {value}", aiMapBase: "base {name}", aiMapLoras: "{count} LoRA(s)", - aiMapNodes: "{count} nodes", + aiMapNodes: "{count} {count|node|nodes}", aiMapAdded: "added \"{title}\"", aiMapRemoved: "removed \"{title}\"", aiMapChanged: "{title} · {key}: {from} → {to}", @@ -5024,7 +5030,7 @@ export const i18n = { aiExecution_untracked: "No execution information yet", aiExecutionRefresh: "Refresh status", aiExecutionTime: "Elapsed {seconds}s", - aiExecutionBatch: "{count} runs submitted", + aiExecutionBatch: "{count} {count|run|runs} submitted", aiExecutionContact: "Last AI call started {seconds}s ago", aiExecutionReview: "ComfyUI finished this generation. Waiting for the AI to review and report the results.", aiExecutionUntracked: "The AI took the task, but its generation is not being tracked. Ask it to run through AMB or attach the exact IDs returned by its external submission.", @@ -5190,8 +5196,8 @@ export const i18n = { aiDecisionCanvas: "Where the AI works on the canvas", aiDecisionStop: "Stopped a run", aiDecisionGap: "Since the last recorded decision the AI {what}, with no new decision recorded.", - aiDecisionGapRuns: "ran {count} times", - aiDecisionGapCanvas: "changed the canvas {count} times", + aiDecisionGapRuns: "ran {count} {count|time|times}", + aiDecisionGapCanvas: "changed the canvas {count} {count|time|times}", aiDecisionsNone: "No decisions yet. Your choices on panels, what you allow or refuse and the materials you hand over are kept here by themselves; what you settle with the AI in its own chat, it records here with log_decision.", aiKind_act: "Action", aiLooks: "Looked things up {count} {count|time|times}", @@ -5576,27 +5582,27 @@ export const i18n = { snapshotNoTab: "Could not reach that tab: go to it first, then try again", savesRunOne: "Run", savesRunTimes: "times", - savesRunning: "{count} runs to go", - savesPicturesAll: "See all {count} pictures", + savesRunning: "{count} {count|run|runs} to go", + savesPicturesAll: "See all {count} {count|picture|pictures}", savesPicturesBack: "← All saves", - savesPicturesTitle: "{name} · save {number} · {count} pictures", - savesRunOneHint: "Run only this save, {count} times, a new seed each", - savesRunsLeft: "{count} runs to go; their pictures come here", + savesPicturesTitle: "{name} · save {number} · {count} {count|picture|pictures}", + savesRunOneHint: "Run only this save, {count} {count|time|times}, a new seed each", + savesRunsLeft: "{count} {count|run|runs} to go; their pictures come here", savesListToggle: "Saves", savesListBack: "← Workflows", savesListTitle: "{canvases} canvases, {saves} saves", savesListEmpty: "No saves yet. Right-click the canvas and choose \"Save this state\".", savesListFailed: "The saves could not be read: reload the page and try again.", - savesListCount: "{count} saves", + savesListCount: "{count} {count|save|saves}", savesListCanvasOpen: "Canvas open", savesListCanvasClosed: "Canvas closed", savesListRemove: "Remove these", - savesListRemoveConfirm: "Move the {count} saves of \"{name}\" to the Recycle Bin? Their pictures stay.", + savesListRemoveConfirm: "Move the {count} {count|save|saves} of \"{name}\" to the Recycle Bin? Their pictures stay.", snapshotOpenTab: "Open on a new tab", snapshotRemoveConfirm: "Move save {number} to the Recycle Bin? Its pictures stay.", snapshotNoPictures: "Not run yet", snapshotSaveFailed: "Not saved: {why}", - snapshotLogged: "{marks} saves, {count} runs", + snapshotLogged: "{marks} {marks|save|saves}, {count} {count|run|runs}", aiTool_pick_base: "Asked you to pick a base", aiRouteTitle: "How should the two models you picked be used?", aiRouteCard: "Your saved card: {name}", @@ -5695,13 +5701,13 @@ export const i18n = { aiTbPromptFrom_combo: "From a combo", aiTbPromptFrom_workflow: "From a workflow", aiTbPageTool: "Page-only helper: the AI can place it, not look it up", - aiTbSettings: "{count} settings", + aiTbSettings: "{count} {count|setting|settings}", aiTbPaid: "Paid", aiTbPageShort: "Page-only", aiTbDeprecated: "Deprecated", aiTbLoras: "{count} LoRAs", aiTbPartial: "Fragment", - aiTbChoices: "{count} choices", + aiTbChoices: "{count} {count|choice|choices}", aiTbPick: "Pick for the AI", aiTbPicked: "Picked", aiTbPickPrompt: "Pick just its prompt", @@ -5716,7 +5722,7 @@ export const i18n = { aiTbLoraList: "LoRAs", aiTbPositive: "Positive prompt", aiTbNegative: "Negative prompt", - aiTbNodeCount: "{count} nodes", + aiTbNodeCount: "{count} {count|node|nodes}", aiTbWords: "Trigger words", aiHandWorkflowWish: "With the workflow “{name}”: ", workflowHandHint: "Hand it to the AI: a task with this preset; you only tell the AI what to draw", @@ -5746,7 +5752,7 @@ export const i18n = { workflowFlowRunsIn: "Used in these presets:", workflowFlowRunsNowhere: "No preset uses it.", workflowFlowChangedHere: "Changed in this stage:", - workflowFlowNodes: "{count} nodes: {ids}", + workflowFlowNodes: "{count} {count|node|nodes}: {ids}", workflowFlowDiffNote: "Orange boxes are what this preset changed; struck out is the default's value.", aiToolboxTitle: "Materials for the AI", aiToolboxKind_nodes: "Nodes", @@ -5815,11 +5821,11 @@ export const i18n = { mcpDidLora: "The AI added the LoRA {name} (Ctrl+Z undoes it)", mcpDidCombo: "The AI placed the combo “{name}” (Ctrl+Z undoes it)", mcpDidNode: "The AI added the node “{name}” (Ctrl+Z undoes it)", - mcpDidNodes: "The AI added {count} nodes (Ctrl+Z undoes it)", + mcpDidNodes: "The AI added {count} {count|node|nodes} (Ctrl+Z undoes it)", mcpDidWire: "The AI wired “{from}” to “{to}” (Ctrl+Z undoes it)", - mcpDidValues: "The AI changed {count} settings (Ctrl+Z undoes it)", + mcpDidValues: "The AI changed {count} {count|setting|settings} (Ctrl+Z undoes it)", mcpDidFill: "The AI filled in this run on {count} {count|node|nodes} (Ctrl+Z undoes it)", - mcpDidRemove: "The AI removed {count} nodes (Ctrl+Z undoes it)", + mcpDidRemove: "The AI removed {count} {count|node|nodes} (Ctrl+Z undoes it)", mcpDidWorkflow: "The AI started the workflow “{name}”", mcpDidFix: "The AI put back {count} missing models (Ctrl+Z undoes it)", mcpDidRun: "The AI started the workflow", @@ -6198,12 +6204,12 @@ export const i18n = { tidyUsageRecycle: 'Move to the Recycle Bin', tidyUsageConfirm: 'Move the {count} picked models ({size}) with their covers and notes to the Recycle Bin? They can be restored from there.', tidyUsageRecycleFailed: 'Some models could not be moved to the Recycle Bin; the browser console says why.', - tidyUsageRecycled: '✓ Sent {count} models to the Recycle Bin, {size} freed.', + tidyUsageRecycled: '✓ Sent {count} {count|model|models} to the Recycle Bin, {size} freed.', tidyUsageNone: 'No models in this group.', tidyUsageMore: '{count} more ({size}) not listed; deal with the ones above first.', tidyUsagePacksLead: 'These packs work only through their nodes, and none of their nodes was run, opened or saved in this period (on the canvas or in an open tab counts as use). "Installed" counts from the day its folder was made. The plugin leaves node packs alone: "Find it in the Manager" opens ComfyUI Manager searched for the pack (its name is copied too, to paste in the search box if nothing shows); there, Disable it (it can be enabled again) or Uninstall it, then restart ComfyUI.', tidyUsageNoManager: 'ComfyUI Manager was not found. Once it is installed, node packs can be disabled or removed there.', - tidyUsageNodes: '{count} nodes', + tidyUsageNodes: '{count} {count|node|nodes}', tidyUsageNoNodes: 'No nodes', tidyUsageInstalled: 'installed {days} days ago', tidyUsageFeeds: 'The nodes of {packs} take what it makes: check those are not needed before disabling it', @@ -7248,7 +7254,7 @@ export const i18n = { scriptTuneResetLines: "{count} {count|line|lines} set for {count|itself|themselves}: let them follow this character", scriptTuneResetAll: "Let these lines follow the layer above in everything", scriptTuneLine: "Line {n}", - scriptTuneLines: "{count} lines picked", + scriptTuneLines: "{count} {count|line|lines} picked", scriptTuneWho: "Read by", scriptTuneFromLine: "This line", scriptTuneFromUsual: "{name}'s usual", diff --git a/web/modules/model_source_links.js b/web/modules/model_source_links.js index 292893a..d121e28 100644 --- a/web/modules/model_source_links.js +++ b/web/modules/model_source_links.js @@ -29,7 +29,7 @@ export function detectPlatform(url) { if (!clean) return null; const [, name, rgb] = PLATFORMS.find(([pattern]) => pattern.test(clean)) || [null, 'Web Link', '']; return rgb - ? { name, color: `rgb(${rgb})`, bg: `rgba(${rgb}, 0.15)`, border: `rgba(${rgb}, 0.4)` } + ? { name, color: `color-mix(in srgb, rgb(${rgb}) 60%, var(--amb-text-main))`, bg: `rgba(${rgb}, 0.15)`, border: `rgba(${rgb}, 0.4)` } : { name, color: 'var(--amb-text-muted)', bg: 'transparent', border: 'var(--amb-border-strong)' }; } diff --git a/web/modules/notebook_canvas.js b/web/modules/notebook_canvas.js index b0f9e41..01dc5bc 100644 --- a/web/modules/notebook_canvas.js +++ b/web/modules/notebook_canvas.js @@ -114,7 +114,7 @@ function placeStructure(owner, data, position) { } catch (error) { if (error.code !== 'missing_nodes') throw error; void anomalousAlert(t('comboPlaceMissingNodes', { - nodes: error.missing.map(item => (item.pack ? `${item.type}(${item.pack})` : item.type)).join('、'), + nodes: error.missing.map(item => (item.pack ? `${item.type} (${item.pack})` : item.type)).join(t('listSeparator')), })); return; } diff --git a/web/modules/ui_ai_chain.js b/web/modules/ui_ai_chain.js index b69473f..87dc417 100644 --- a/web/modules/ui_ai_chain.js +++ b/web/modules/ui_ai_chain.js @@ -33,7 +33,7 @@ export function looksRow(steps, number) { row.append(el('span', 'anomalous-ai-step-dot', number)); const body = el('div', 'anomalous-ai-step-body'); const tools = lookTools(steps).map(({ tool, count }) => counted(tool, count)); - const head = el('button', 'anomalous-ai-looks', `${t('aiLooks', { count: steps.length })}:${tools.join('、')}`); + const head = el('button', 'anomalous-ai-looks', `${t('aiLooks', { count: steps.length })}${t('wordColon')}${tools.join(t('listSeparator'))}`); head.type = 'button'; head.onclick = () => { details.hidden = !details.hidden; diff --git a/web/modules/ui_ai_decisions.js b/web/modules/ui_ai_decisions.js index a48dc0f..e7889bd 100644 --- a/web/modules/ui_ai_decisions.js +++ b/web/modules/ui_ai_decisions.js @@ -50,7 +50,7 @@ export function decisionCard(record, numberOf = null) { if (BY[record.by]) head.append(el('span', `anomalous-ai-decision-by is-${record.by}`, t(BY[record.by]))); card.append(head, el('div', 'anomalous-ai-decision-text', record.decision || '')); if (record.reason) card.append(el('div', 'anomalous-ai-decision-why', t('aiDecisionWhy', { reason: record.reason }))); - if (record.rejected?.length) card.append(el('div', 'anomalous-ai-decision-not', t('aiDecisionRejected', { options: record.rejected.join('、') }))); + if (record.rejected?.length) card.append(el('div', 'anomalous-ai-decision-not', t('aiDecisionRejected', { options: record.rejected.join(t('listSeparator')) }))); if (record.related?.length) { const links = el('div', 'anomalous-ai-decision-links'); links.append(el('span', '', t('aiDecisionAbout')), ...record.related.map(id => stepLink(id, numberOf))); @@ -64,7 +64,7 @@ function described(item) { switch (item.type) { case 'logged': return [item.title, item.decision]; case 'answer': return [item.question || t('aiDecisionAnswer'), item.skipped ? t('aiPanelSkipped') : item.timed_out ? t('aiPanelTookRecommended', { answer: item.answer }) : item.answer]; - case 'gate': return [t(toolKey(item.tool)), [t(item.allowed ? 'aiDecisionAllowed' : 'aiDecisionRefused'), item.text].filter(Boolean).join(':')]; + case 'gate': return [t(toolKey(item.tool)), [t(item.allowed ? 'aiDecisionAllowed' : 'aiDecisionRefused'), item.text].filter(Boolean).join(t('wordColon'))]; case 'toolbox': return [t('aiDecisionToolbox'), item.count ? t('aiDecisionToolboxText', { count: item.count, mode: t(`aiTbMode_${item.mode}`) }) : t('aiToolboxCleared')]; case 'canvas': return [t('aiDecisionCanvas'), t(item.mode === 'new' ? 'aiCanvasChoseNew' : 'aiCanvasChoseCurrent')]; case 'privacy': return [t('aiPrivacy'), t(item.on ? 'aiPrivacyChoseOn' : 'aiPrivacyChoseOff')]; @@ -85,7 +85,7 @@ export function renderDecisionsView(pane, task, ctx) { const gap = task.decisions_gap; if (gap && items.length) { const parts = [gap.runs ? t('aiDecisionGapRuns', { count: gap.runs }) : '', gap.canvas ? t('aiDecisionGapCanvas', { count: gap.canvas }) : ''].filter(Boolean); - body.append(el('div', 'anomalous-ai-decisions-gap', t('aiDecisionGap', { what: parts.join('、') }))); + body.append(el('div', 'anomalous-ai-decisions-gap', t('aiDecisionGap', { what: parts.join(t('listSeparator')) }))); } if (!items.length) body.append(el('div', 'anomalous-ai-stage-empty anomalous-ai-decisions-none', t('aiDecisionsNone'))); for (const item of items) { @@ -100,7 +100,7 @@ export function renderDecisionsView(pane, task, ctx) { if (BY[by]) top.append(el('span', `anomalous-ai-decision-by is-${by}`, t(BY[by]))); text.append(top, el('div', 'anomalous-ai-decisions-words', words || '')); if (item.type === 'logged' && item.reason) text.append(el('div', 'anomalous-ai-decision-why', t('aiDecisionWhy', { reason: item.reason }))); - if (item.type === 'logged' && item.rejected?.length) text.append(el('div', 'anomalous-ai-decision-not', t('aiDecisionRejected', { options: item.rejected.join('、') }))); + if (item.type === 'logged' && item.rejected?.length) text.append(el('div', 'anomalous-ai-decision-not', t('aiDecisionRejected', { options: item.rejected.join(t('listSeparator')) }))); const foot = el('div', 'anomalous-ai-decision-links'); foot.append(el('span', '', timeLabel(item.at)), stepLink(item.step, ctx.numberOf)); text.append(foot); diff --git a/web/modules/ui_ai_gate.js b/web/modules/ui_ai_gate.js index 265b5b4..d52c1b7 100644 --- a/web/modules/ui_ai_gate.js +++ b/web/modules/ui_ai_gate.js @@ -52,7 +52,7 @@ export function gateCard(step, taskId, changed) { if (!asked.decision || asked.earlier) return null; const words = asked.decision === 'allow' ? t(step.state === 'allowed' ? 'aiGateAllowedLater' : 'aiGateAllowed') - : t('aiGateDenied') + (asked.text ? `:${asked.text}` : ''); + : t('aiGateDenied') + (asked.text ? `${t('wordColon')}${asked.text}` : ''); return el('div', `anomalous-ai-gate-answer is-${asked.decision}`, words); } const card = el('div', 'anomalous-ai-gate'); diff --git a/web/modules/ui_ai_run_map.js b/web/modules/ui_ai_run_map.js index 5b92534..f6c6e65 100644 --- a/web/modules/ui_ai_run_map.js +++ b/web/modules/ui_ai_run_map.js @@ -39,7 +39,7 @@ export function boxDetail(box) { const facts = box.facts || {}; switch (box.role) { case 'lora': return [facts.file, Number.isFinite(facts.strength) ? `(${facts.strength})` : ''].filter(Boolean).join(' '); - case 'prompt': return `${t(facts.polarity === 'negative' ? 'aiMapNegative' : 'aiMapPositive')}:${facts.text || '—'}`; + case 'prompt': return `${t(facts.polarity === 'negative' ? 'aiMapNegative' : 'aiMapPositive')}${t('wordColon')}${facts.text || '—'}`; case 'sampler': return [size(facts), facts.steps ? t('aiRunSteps', { steps: facts.steps }) : '', facts.cfg !== undefined ? `CFG ${facts.cfg}` : '', facts.sampler_name || '', facts.denoise !== undefined && facts.denoise < 1 ? t('aiMapDenoise', { value: facts.denoise }) : ''].filter(Boolean).join(' · '); case 'size': return size(facts); diff --git a/web/modules/ui_ai_toolbox.js b/web/modules/ui_ai_toolbox.js index 55aa601..8aa9ff4 100644 --- a/web/modules/ui_ai_toolbox.js +++ b/web/modules/ui_ai_toolbox.js @@ -354,7 +354,7 @@ export async function openToolbox(task, changed = () => {}) { const current = list.find(facet => facet.key === state.facet); const open = button(`anomalous-tb-facet-open${current && state.picks.packs.includes(current.key) ? ' has-pack' : ''}`, [ current?.source ? sourceBadge(current.source, 20) : icon('list', 15), - el('span', 'anomalous-tb-facet-open-label', `${title}:${current?.label || t('aiTbEverything')}`), + el('span', 'anomalous-tb-facet-open-label', `${title}${t('wordColon')}${current?.label || t('aiTbEverything')}`), el('span', 'anomalous-tb-facet-count', String(current?.count ?? beforeFacet.length)), icon('chevronDown', 14)], () => toggleFacetMenu({ host: main, top: side.offsetTop + side.offsetHeight, title, rows: rows(), button: open }), t('aiTbPickSource')); open.setAttribute('aria-haspopup', 'listbox'); diff --git a/web/modules/ui_audio_tts_editor.js b/web/modules/ui_audio_tts_editor.js index 8384e0a..4f33e75 100644 --- a/web/modules/ui_audio_tts_editor.js +++ b/web/modules/ui_audio_tts_editor.js @@ -334,7 +334,7 @@ export function openGptSovitsEditor(group, { onSaved } = {}) { const uses = usesOf(state.voices, path).map(use => t(`ttsRefsUse_${use.role}`, { name: use.name === MAIN ? t('ttsRefsMainVoice') : use.name })); const message = [ t('ttsRefsRemoveConfirm', { file: baseName(path) }), - uses.length ? t('ttsRefsRemoveUses', { uses: uses.join('、') }) : '', + uses.length ? t('ttsRefsRemoveUses', { uses: uses.join(t('listSeparator')) }) : '', ].filter(Boolean).join('\n'); if (!await anomalousConfirm(message, t('ttsRefsRemove'))) return; if (state.playing?.path === path) stopPlaying(); diff --git a/web/modules/ui_backup.js b/web/modules/ui_backup.js index f8fc7df..001a9a9 100644 --- a/web/modules/ui_backup.js +++ b/web/modules/ui_backup.js @@ -177,7 +177,7 @@ function showChoices(owner, name, summary, { dialog, footer, close }) { if (summary.models?.total) { const { matched, total, missing } = summary.models; const hint = missing.length - ? t('backupModelsMissing', { names: missing.slice(0, 5).map(rel => rel.split('/').pop()).join('、'), count: missing.length }) + ? t('backupModelsMissing', { names: missing.slice(0, 5).map(rel => rel.split('/').pop()).join(t('listSeparator')), count: missing.length }) : ''; models = checkbox(t('backupModels', { matched, total }), matched > 0, hint); dialog.append(models.line); diff --git a/web/modules/ui_combo_structure.js b/web/modules/ui_combo_structure.js index 6a47cc7..420c85f 100644 --- a/web/modules/ui_combo_structure.js +++ b/web/modules/ui_combo_structure.js @@ -207,11 +207,11 @@ export async function saveSelectionAsCombo(owner, nodes = null) { el('p', 'anomalous-structure-note', t('comboFromSelectionKept', { count: structure.nodes.length, nodes: structureSummary(structure) }))); if (dropped.length) { dialog.append(el('p', 'anomalous-structure-note is-muted', t('comboFromSelectionDropped', { - count: dropped.length, nodes: dropped.map(node => node.title || node.type).join('、'), + count: dropped.length, nodes: dropped.map(node => node.title || node.type).join(t('listSeparator')), }))); } const open = openInputs(structure); - if (open.length) dialog.append(el('p', 'anomalous-structure-note is-muted', t('comboFromSelectionOpen', { inputs: open.join('、') }))); + if (open.length) dialog.append(el('p', 'anomalous-structure-note is-muted', t('comboFromSelectionOpen', { inputs: open.join(t('listSeparator')) }))); const rows = el('div', 'anomalous-structure-slots'); const choices = structure.slots.map((slot) => { const row = el('label', 'anomalous-structure-slot'); @@ -364,7 +364,7 @@ export function renderStructuredCombo(owner, host, note, toolbar) { const nodeName = (slot) => { const node = nodeOf.get(slot.node); if (!node) return null; - return { text: `${node.title || node.name || node.type}${node.pack ? `(${node.pack})` : ''}`, + return { text: `${node.title || node.name || node.type}${node.pack ? ` (${node.pack})` : ''}`, title: node.pack ? `${node.type} · ${node.pack}` : node.type }; }; const labelLine = (slot) => { @@ -506,15 +506,15 @@ export function renderStructuredCombo(owner, host, note, toolbar) { const about = el('div', 'anomalous-structure-about'); const open = openInputs(structure); - if (open.length) about.append(el('p', 'anomalous-structure-note', t('comboOpenInputs', { inputs: open.join('、') }))); + if (open.length) about.append(el('p', 'anomalous-structure-note', t('comboOpenInputs', { inputs: open.join(t('listSeparator')) }))); about.append(el('p', 'anomalous-structure-note is-muted', t('comboStructureNodes', { nodes: structureSummary(structure) })), el('p', 'anomalous-structure-note is-muted', t('comboFixedHint'))); const packs = [...new Set(structure.nodes.map(item => item.pack).filter(Boolean))]; - if (packs.length) about.append(el('p', 'anomalous-structure-note is-muted', t('comboStructurePacks', { packs: packs.join('、') }))); + if (packs.length) about.append(el('p', 'anomalous-structure-note is-muted', t('comboStructurePacks', { packs: packs.join(t('listSeparator')) }))); const missing = missingTypes(structure); if (missing.length) { about.append(el('p', 'anomalous-structure-note is-bad', t('comboStructureMissing', { - nodes: missing.map(item => (item.pack ? `${item.type}(${item.pack})` : item.type)).join('、'), + nodes: missing.map(item => (item.pack ? `${item.type} (${item.pack})` : item.type)).join(t('listSeparator')), }))); } card.append(about); diff --git a/web/modules/ui_node_model_picker.js b/web/modules/ui_node_model_picker.js index 77a23cb..2f48907 100644 --- a/web/modules/ui_node_model_picker.js +++ b/web/modules/ui_node_model_picker.js @@ -260,7 +260,7 @@ export function _openGalleryReplacer(node, w, options = {}) { if (mode === 'replace') chosen = chosen.filter(pathMatchesBaseFilter); }; const updateSelection = () => { - const names = chosen.map(getName).join('、'); + const names = chosen.map(getName).join(t('listSeparator')); if (mode === 'insert') { selectionText.textContent = chosen.length ? t('pickerChosenLoras', { count: chosen.length, names }) : t('pickerChooseLoras'); selectionText.title = names; diff --git a/web/modules/ui_param_compare.js b/web/modules/ui_param_compare.js index 594a5eb..c7f1604 100644 --- a/web/modules/ui_param_compare.js +++ b/web/modules/ui_param_compare.js @@ -183,7 +183,7 @@ function keyBlock(keys, ticks, starred, focus) { function nodeList(title, nodes) { const block = el('div', 'anomalous-compare-node is-structure'); block.append(el('div', 'anomalous-compare-node-title', title)); - block.append(el('div', 'anomalous-compare-structure-list', nodes.map(node => `${node.title} #${node.id}`).join('、'))); + block.append(el('div', 'anomalous-compare-structure-list', nodes.map(node => `${node.title} #${node.id}`).join(t('listSeparator')))); return block; } diff --git a/web/modules/ui_recipe_save_dialog.js b/web/modules/ui_recipe_save_dialog.js index 11693b6..d5c9775 100644 --- a/web/modules/ui_recipe_save_dialog.js +++ b/web/modules/ui_recipe_save_dialog.js @@ -43,7 +43,7 @@ function verificationBox(parent, missing) { if (!missing.length) return null; const box = appendText(parent, 'div', '', 'anomalous-wfs-verify'); appendText(box, 'strong', formatRecipeText('recipeVerificationMissing', { count: missing.length })); - appendText(box, 'small', missing.map((reference) => reference.saved_value).join('、'), 'anomalous-wfs-hint'); + appendText(box, 'small', missing.map((reference) => reference.saved_value).join(t('listSeparator')), 'anomalous-wfs-hint'); const choice = appendText(box, 'label', '', 'anomalous-wfs-check'); const input = document.createElement('input'); input.type = 'checkbox'; diff --git a/web/modules/ui_script_director.js b/web/modules/ui_script_director.js index d734bb6..35a03ae 100644 --- a/web/modules/ui_script_director.js +++ b/web/modules/ui_script_director.js @@ -482,7 +482,7 @@ function renderPackage() { const ok = !pkg.error; refs.summary.classList.toggle('is-error', !ok); if (ok && pkg.speakers.length > 1) { - refs.summary.textContent = t('scriptDirectorPackageSummaryCast', { count: pkg.lineCount, speakers: pkg.speakers.join('、') }); + refs.summary.textContent = t('scriptDirectorPackageSummaryCast', { count: pkg.lineCount, speakers: pkg.speakers.join(t('listSeparator')) }); } else if (ok) { const used = [...new Set(state.lines.filter(line => line.text.trim()).map(line => emotionLabel(line.emotion)))]; refs.summary.textContent = t('scriptDirectorPackageSummary', { count: pkg.lineCount, emotions: used.join(' · ') }); diff --git a/web/modules/ui_shell_rail.js b/web/modules/ui_shell_rail.js index 5466955..2a267d5 100644 --- a/web/modules/ui_shell_rail.js +++ b/web/modules/ui_shell_rail.js @@ -30,7 +30,7 @@ export const PAGE_ICONS = { export const RAIL_PAGES = Object.freeze([ { page: 'home', id: 'anomalous-home-btn', labelKey: 'shellHome' }, { page: 'activity', id: 'anomalous-activity-btn', labelKey: 'shellActivity' }, - { page: 'ai', id: 'anomalous-ai-btn', labelKey: 'shellAi' }, // the AI workbench (ui_ai_workbench.js) + { page: 'ai', id: 'anomalous-ai-btn', labelKey: 'shellAi', railKey: 'shellAiRail' }, // the AI workbench (ui_ai_workbench.js) { page: 'models', id: 'anomalous-models-btn', labelKey: 'models' }, { page: 'gallery', id: 'anomalous-gallery-btn', labelKey: 'gallery' }, { page: 'recipes', id: 'anomalous-notebook-btn', labelKey: 'recipeTitle' }, @@ -84,7 +84,7 @@ export function createShellRail(owner) { RAIL_PAGES.forEach((entry, index) => { const button = items.get(entry.page); const text = t(entry.labelKey); - button.querySelector('.anomalous-rail-label').textContent = text; + button.querySelector('.anomalous-rail-label').textContent = t(entry.railKey || entry.labelKey); button.setAttribute('aria-label', text); // Only the first nine have a number key. button.dataset.tooltip = index < 9 ? t('shellRailKey', { name: text, key: index + 1 }) : text; diff --git a/web/modules/ui_tts_import_draft.js b/web/modules/ui_tts_import_draft.js index c334f86..2253cb9 100644 --- a/web/modules/ui_tts_import_draft.js +++ b/web/modules/ui_tts_import_draft.js @@ -354,7 +354,7 @@ export function renderDraftCard(draft, view, ctx, { solo = false } = {}) { .filter(item => !draft.target || needs.includes(item.id)); if (missing.length) { body.append(el('div', 'anomalous-tts-card-note', t('ttsImportPretrainedNote', { - files: missing.map(item => item.label).join('、'), + files: missing.map(item => item.label).join(t('listSeparator')), size: formatSize(missing.reduce((sum, item) => sum + (item.size || 0), 0)), }))); } diff --git a/web/modules/ui_tts_import_sources.js b/web/modules/ui_tts_import_sources.js index a26f645..ae9edc9 100644 --- a/web/modules/ui_tts_import_sources.js +++ b/web/modules/ui_tts_import_sources.js @@ -21,7 +21,7 @@ export const UPLOAD_WARN_FILES = 1500; /** `a、b、c…`: the first few names of a list, in name order. */ function few(names) { const sorted = [...names].sort((a, b) => a.localeCompare(b)); - return sorted.slice(0, 3).join('、') + (sorted.length > 3 ? '…' : ''); + return sorted.slice(0, 3).join(t('listSeparator')) + (sorted.length > 3 ? '…' : ''); } /** diff --git a/web/modules/ui_tts_presets.js b/web/modules/ui_tts_presets.js index 9a78345..1ade1a0 100644 --- a/web/modules/ui_tts_presets.js +++ b/web/modules/ui_tts_presets.js @@ -120,7 +120,7 @@ export function renderTtsPresets(status, groups, local) { : t('ttsPresetsPair', { ref: t(`scriptRunLang_${use.split('>')[0]}`), lang: t(`scriptRunLang_${use.split('>')[1]}`) }))), ...uses.characters, ]; - const message = t('ttsPresetDeleteAsk', { name: label(name) }) + (where.length ? `\n${t('ttsPresetDeleteUses', { list: where.join('、') })}` : ''); + const message = t('ttsPresetDeleteAsk', { name: label(name) }) + (where.length ? `\n${t('ttsPresetDeleteUses', { list: where.join(t('listSeparator')) })}` : ''); if (!await anomalousConfirm(message, t('ttsPresetsTitle'), { okLabel: t('ttsPresetDelete') })) return; change(() => { removePreset(state, name); diff --git a/web/styles/00-foundation-models.css b/web/styles/00-foundation-models.css index 9de0f7a..13c9dad 100644 --- a/web/styles/00-foundation-models.css +++ b/web/styles/00-foundation-models.css @@ -33,6 +33,7 @@ --amb-border-strong: rgba(255, 255, 255, 0.16); /* controls and dividers that must show at rest */ --amb-border-hover: rgba(255, 255, 255, 0.16); --amb-link: #a5b4fc; /* the one accent for links, selection and focus; tints via color-mix */ + --amb-on-link: #15171c; --amb-btn-primary-bg: #e5e7eb; --amb-btn-primary-hover: #ffffff; --amb-btn-primary-text: #111827; @@ -87,6 +88,7 @@ html.amb-theme-light { --amb-border-strong: rgba(0, 0, 0, 0.16); --amb-border-hover: rgba(0, 0, 0, 0.22); --amb-link: #4f46e5; + --amb-on-link: #ffffff; --amb-btn-primary-bg: #1f2937; --amb-btn-primary-hover: #111827; --amb-btn-primary-text: #ffffff; @@ -154,6 +156,7 @@ html.amb-theme-light :is(.anomalous-workbench-overlay, .anomalous-gallery-viewer --amb-border-strong: rgba(255, 255, 255, 0.16); --amb-border-hover: rgba(255, 255, 255, 0.16); --amb-link: #a5b4fc; + --amb-on-link: #15171c; --amb-btn-primary-bg: #e5e7eb; --amb-btn-primary-hover: #ffffff; --amb-btn-primary-text: #111827; @@ -239,6 +242,7 @@ html.theme-abyssal-scarlet, --amb-border-strong: rgba(255, 235, 240, 0.16); --amb-border-hover: rgba(220, 20, 60, 0.35); --amb-link: #F28CA0; + --amb-on-link: #2a0c14; --amb-btn-primary-bg: #87384E; --amb-btn-primary-hover: #9c425b; --amb-btn-primary-text: #FFF5F7; @@ -478,6 +482,7 @@ html:not(.anomalous-floating-entry-enabled) [data-setting-id="Anomalous.ModelBro #anomalous-container { width: 90%; height: 90%; + color: var(--amb-text-main); background-color: var(--amb-bg-page, var(--amb-bg-page)); border: 1px solid var(--amb-border); border-radius: 8px; diff --git a/web/styles/17-model-types.css b/web/styles/17-model-types.css index d1a4e96..e333c97 100644 --- a/web/styles/17-model-types.css +++ b/web/styles/17-model-types.css @@ -134,7 +134,7 @@ align-items: center; gap: 6px; width: min(360px, 100%); - min-width: 120px; + min-width: 0; padding: 0 10px; border: 1px solid var(--amb-border-strong); border-radius: 999px; diff --git a/web/styles/18-scan-page.css b/web/styles/18-scan-page.css index 8783f44..f20b36f 100644 --- a/web/styles/18-scan-page.css +++ b/web/styles/18-scan-page.css @@ -606,8 +606,10 @@ button.anomalous-scan-stat:hover { .anomalous-scan-segment { display: inline-flex; flex-wrap: wrap; - flex-shrink: 0; + flex-shrink: 1; align-self: flex-start; + box-sizing: border-box; + max-width: 100%; gap: 2px; padding: 3px; border: 1px solid var(--amb-border); diff --git a/web/styles/20-current-node.css b/web/styles/20-current-node.css index 7c1aea6..96e4b04 100644 --- a/web/styles/20-current-node.css +++ b/web/styles/20-current-node.css @@ -26,7 +26,7 @@ .anomalous-node-section-head h3 { margin: 0; - color: #8b91a3; + color: var(--amb-text-muted); font-size: 10px; font-weight: 750; letter-spacing: 0.1em; @@ -392,7 +392,7 @@ button.anomalous-node-param-source:hover { background: color-mix(in srgb, var(-- .anomalous-node-muted { margin: 0; - color: #8b91a3; + color: var(--amb-text-muted); font-size: 11px; line-height: 1.5; } diff --git a/web/styles/29-workflow-detail.css b/web/styles/29-workflow-detail.css index 8898649..c43dd32 100644 --- a/web/styles/29-workflow-detail.css +++ b/web/styles/29-workflow-detail.css @@ -127,6 +127,7 @@ html.amb-theme-light .anomalous-recipe-detail-view { --wfd-orange: #ea580c; } top: -16px; z-index: 2; display: flex; + flex-wrap: wrap; align-items: flex-end; gap: 10px; margin-top: 4px; @@ -137,7 +138,7 @@ html.amb-theme-light .anomalous-recipe-detail-view { --wfd-orange: #ea580c; } .anomalous-wfd-tabs { display: flex; - flex: 1; + flex: 1 1 240px; align-items: flex-end; gap: 2px; min-width: 0; @@ -806,5 +807,5 @@ html.amb-theme-light .anomalous-recipe-detail-view { --wfd-orange: #ea580c; } .anomalous-example-title { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; color: var(--amb-text-main); font-size: 13px; font-weight: 650; } .anomalous-example-badge { padding: 0 6px; border-radius: 6px; background: color-mix(in srgb, var(--amb-link) 18%, transparent); color: var(--amb-link); font-size: 11px; font-weight: 500; } .anomalous-example-about { margin: 3px 0 4px; color: var(--amb-text-soft); font-size: 12.5px; line-height: 1.6; } -.anomalous-example-open { flex: none; height: 32px; padding: 0 12px; border: 0; border-radius: 8px; background: var(--amb-link); color: #fff; font: inherit; font-size: 12.5px; font-weight: 600; cursor: pointer; } +.anomalous-example-open { flex: none; height: 32px; padding: 0 12px; border: 0; border-radius: 8px; background: var(--amb-link); color: var(--amb-on-link); font: inherit; font-size: 12.5px; font-weight: 600; cursor: pointer; } .anomalous-example-open:hover { filter: brightness(1.15); } diff --git a/web/styles/38-ai-panels.css b/web/styles/38-ai-panels.css index 9fb0c4f..5b4230a 100644 --- a/web/styles/38-ai-panels.css +++ b/web/styles/38-ai-panels.css @@ -389,7 +389,7 @@ padding: 0 7px; border-radius: 999px; background: var(--amb-link); - color: #fff; + color: var(--amb-on-link); font-size: 10.5px; font-weight: 700; line-height: 17px; diff --git a/web/styles/40-ai-side.css b/web/styles/40-ai-side.css index 133c759..c80a737 100644 --- a/web/styles/40-ai-side.css +++ b/web/styles/40-ai-side.css @@ -555,7 +555,7 @@ border: 0; border-radius: 9px; background: var(--amb-link); - color: #fff; + color: var(--amb-on-link); font: inherit; font-size: 12.5px; font-weight: 600; diff --git a/web/styles/41-ai-results.css b/web/styles/41-ai-results.css index bdef55d..ebc7a53 100644 --- a/web/styles/41-ai-results.css +++ b/web/styles/41-ai-results.css @@ -122,7 +122,7 @@ .anomalous-ai-tick.is-on { border-color: var(--amb-link); background: var(--amb-link); - color: #fff; + color: var(--amb-on-link); opacity: 1; } @@ -182,7 +182,7 @@ border: 0; border-radius: 999px; background: var(--amb-link); - color: #fff; + color: var(--amb-on-link); font: inherit; cursor: pointer; } diff --git a/web/styles/42-ai-flow.css b/web/styles/42-ai-flow.css index cd04c15..5d5b59d 100644 --- a/web/styles/42-ai-flow.css +++ b/web/styles/42-ai-flow.css @@ -343,7 +343,7 @@ border: 0; border-radius: 8px; background: var(--amb-link); - color: #fff; + color: var(--amb-on-link); font: inherit; font-weight: 600; cursor: pointer; diff --git a/web/styles/43-ai-toolbox.css b/web/styles/43-ai-toolbox.css index 5eaf152..ad1d59b 100644 --- a/web/styles/43-ai-toolbox.css +++ b/web/styles/43-ai-toolbox.css @@ -167,7 +167,7 @@ transition: opacity 0.15s, transform 0.15s, background 0.15s; } .anomalous-tb-card:hover .anomalous-tb-check { opacity: 1; } -.anomalous-tb-card.is-picked .anomalous-tb-check { border-color: var(--amb-link); background: var(--amb-link); color: #fff; opacity: 1; transform: scale(1); } +.anomalous-tb-card.is-picked .anomalous-tb-check { border-color: var(--amb-link); background: var(--amb-link); color: var(--amb-on-link); opacity: 1; transform: scale(1); } .anomalous-tb-star { position: absolute; right: 8px; @@ -292,7 +292,7 @@ font-size: 12.5px; cursor: pointer; } -.anomalous-tb-take { border-color: transparent; background: var(--amb-link); color: #fff; } +.anomalous-tb-take { border-color: transparent; background: var(--amb-link); color: var(--amb-on-link); } .anomalous-tb-take.is-on { border-color: var(--amb-link); background: transparent; color: var(--amb-link); } .anomalous-tb-like:hover { background: rgba(var(--amb-ink-rgb), 0.06); color: var(--amb-text-main); } .anomalous-tb-like.is-on { color: var(--amb-text-main); } diff --git a/web/styles/45-ai-side-views.css b/web/styles/45-ai-side-views.css index a853bf5..7932e0a 100644 --- a/web/styles/45-ai-side-views.css +++ b/web/styles/45-ai-side-views.css @@ -44,7 +44,7 @@ padding: 0 5px; border-radius: 999px; background: var(--amb-link); - color: var(--amb-btn-primary-text, #fff); + color: var(--amb-on-link); font-size: 10.5px; font-weight: 700; line-height: 16px; diff --git a/web/styles/46-ai-mention.css b/web/styles/46-ai-mention.css index ea6cd82..fd7728d 100644 --- a/web/styles/46-ai-mention.css +++ b/web/styles/46-ai-mention.css @@ -141,7 +141,7 @@ box-sizing: border-box; border-radius: 9px; background: var(--amb-link); - color: #fff; + color: var(--amb-on-link); font-size: 11px; font-weight: 700; line-height: 18px; @@ -215,7 +215,7 @@ border-radius: 50%; background: var(--amb-link); box-shadow: 0 2px 6px rgba(0, 0, 0, 0.45); - color: #fff; + color: var(--amb-on-link); } .anomalous-ai-pics-tile.is-picked .anomalous-ai-pics-mark { display: flex; } .anomalous-ai-pics-none { grid-column: 1 / -1; padding: 18px 6px; color: var(--amb-text-soft); font-size: 12.5px; text-align: center; } @@ -260,7 +260,7 @@ box-sizing: border-box; border-radius: 9px; background: var(--amb-link); - color: #fff; + color: var(--amb-on-link); font-size: 11px; font-weight: 700; font-variant-numeric: tabular-nums; diff --git a/web/styles/47-ai-decisions.css b/web/styles/47-ai-decisions.css index 494d3f4..be53533 100644 --- a/web/styles/47-ai-decisions.css +++ b/web/styles/47-ai-decisions.css @@ -70,6 +70,7 @@ .anomalous-ai-task-link { display: inline-flex; align-items: center; + min-width: 0; max-width: 320px; border: 1px solid color-mix(in srgb, var(--amb-link) 40%, transparent); border-radius: 999px; @@ -90,10 +91,10 @@ font: inherit; cursor: pointer; } -.anomalous-ai-task-link.is-next { border: 1px solid color-mix(in srgb, var(--amb-link) 40%, transparent); font-size: 12px; } /* they are the chips: not the header's size */ +.anomalous-ai-task-link.is-next { border: 1px solid color-mix(in srgb, var(--amb-link) 40%, transparent); color: var(--amb-link); font-size: 12px; } /* they are the chips: not the header's size */ .anomalous-ai-task-link span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .anomalous-ai-task-link-go:hover span, .anomalous-ai-task-link.is-next:hover span { text-decoration: underline; } -.anomalous-ai-task-link-x { display: inline-flex; margin-right: 4px; padding: 3px; border: 0; border-radius: 50%; background: none; color: inherit; cursor: pointer; } +.anomalous-ai-task-link-x { display: inline-flex; flex: none; margin-right: 4px; padding: 3px; border: 0; border-radius: 50%; background: none; color: inherit; cursor: pointer; } .anomalous-ai-task-link-x:hover { background: color-mix(in srgb, var(--amb-link) 18%, transparent); } .anomalous-ai-task-links-menu { position: fixed;