dsh-persona-forge 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +87 -2
- package/README.en.md +515 -0
- package/README.md +201 -248
- package/cards/muscle-crew.yml +3 -3
- package/lib/client.js +409 -13
- package/lib/index.js +16 -3
- package/lib/preserve.js +138 -0
- package/lib/prompts.js +164 -3
- package/lib/routes.js +169 -9
- package/lib/shared/protocol.js +10 -0
- package/lib/store.js +24 -2
- package/package.json +3 -3
- package/README.zh-CN.md +0 -297
package/lib/client.js
CHANGED
|
@@ -50,6 +50,20 @@ window.__ModuleLoader__.load({
|
|
|
50
50
|
'control.sendMode.direct': '直接发出',
|
|
51
51
|
'control.sendMode.hint.review': '改写结果先在下方的对比面板显示,你确认后再发送;原草稿不受影响。',
|
|
52
52
|
'control.sendMode.hint.direct': '改写完成后立即作为你的消息发出,不再确认。',
|
|
53
|
+
'control.mode': '改写方式',
|
|
54
|
+
'control.mode.card': '跟随卡片',
|
|
55
|
+
'control.mode.llm': '模型改写',
|
|
56
|
+
'control.mode.template': '模板套用',
|
|
57
|
+
'control.mode.hint': '跟随卡片 = 用卡片自己声明的方式。临时切换只影响这个会话。',
|
|
58
|
+
'control.fidelity': '保真度',
|
|
59
|
+
'control.fidelity.card': '跟随卡片',
|
|
60
|
+
'control.fidelity.style': '只改语气',
|
|
61
|
+
'control.fidelity.hint.style': '这张卡声明的是「只改语气」,不能在这里放宽——放宽是改卡的属性,请在设置页改。',
|
|
62
|
+
'control.fidelity.hint.strategy': '这张卡允许追加行为约束。这里可以临时收窄成「只改语气」,但收不回去。',
|
|
63
|
+
'control.modelCheck': '模型校验',
|
|
64
|
+
'control.modelCheck.on': '开',
|
|
65
|
+
'control.modelCheck.off': '关',
|
|
66
|
+
'control.modelCheck.hint': '开启后每次改写会多调用一次模型,检查「需求被悄悄改弱」这类本地检查看不出的问题——代价是延迟和 token 翻倍。关闭时仍会做本地检查(标识符、路径、版本号、数字),不耗 token。',
|
|
53
67
|
'panel.title': '角色改写',
|
|
54
68
|
'panel.loading': '正在按「{name}」改写…',
|
|
55
69
|
'panel.original': '你写的',
|
|
@@ -67,6 +81,10 @@ window.__ModuleLoader__.load({
|
|
|
67
81
|
'panel.drift': '技术事实可能被改动',
|
|
68
82
|
'panel.checkUnavailable': '未完成事实校验',
|
|
69
83
|
'panel.checkOk': '技术事实已保留',
|
|
84
|
+
'panel.factsDrift': '检测到技术内容变动',
|
|
85
|
+
'panel.factsMissing': '丢失:',
|
|
86
|
+
'panel.factsAdded': '新增:',
|
|
87
|
+
'panel.factsOnly': '已做本地检查(不耗 token):标识符、路径、版本号、数字均未丢失。但它看不出「需求被悄悄改弱」或「用散文追加了约束」——要覆盖这些请开启模型校验。',
|
|
70
88
|
'panel.stale': '草稿在改写期间有改动——结果基于改写前的文本。',
|
|
71
89
|
'error.empty': '输入框是空的,先写下你的需求。',
|
|
72
90
|
'error.phase': '输入框正忙(提交中),请稍后再试。',
|
|
@@ -82,11 +100,24 @@ window.__ModuleLoader__.load({
|
|
|
82
100
|
'settings.dir': '角色卡目录',
|
|
83
101
|
'settings.reveal': '打开目录',
|
|
84
102
|
'settings.new': '新建角色卡',
|
|
85
|
-
'settings.
|
|
86
|
-
'settings.
|
|
103
|
+
'settings.ai': 'AI 代填',
|
|
104
|
+
'settings.ai.tip': '新建角色卡时可以让 AI 代填:写一句角色思路,风格和四档范例都会自动写好。',
|
|
105
|
+
'settings.ai.model': '使用模型:{provider} / {model}(设置页没有会话上下文,用的是 harness 默认模型;改写本身跟随当前会话的模型)',
|
|
106
|
+
'settings.ai.desc': '用一句话描述角色的思路,AI 会按同一套规范把表单填好:风格、四档强度的转换范例都写好。生成结果只填入表单,不会自动保存。',
|
|
107
|
+
'settings.ai.briefLabel': '角色思路(一句话或几句话都行)',
|
|
108
|
+
'settings.ai.briefPlaceholder': '例如:一个脾气暴躁但极其可靠的老灯塔守夜人,说话简短、爱用航海比喻',
|
|
109
|
+
'settings.ai.generate': '生成草稿',
|
|
110
|
+
'settings.ai.busy': 'AI 正在撰写角色卡…',
|
|
111
|
+
'settings.ai.hint': '生成的是草稿,改完再保存。',
|
|
112
|
+
'settings.ai.needBrief': '请先写下角色的思路。',
|
|
113
|
+
'settings.ai.failed': '生成失败:{message}',
|
|
114
|
+
'settings.origin.builtin': '内置',
|
|
115
|
+
'settings.origin.edited': '已修改',
|
|
116
|
+
'settings.origin.custom': '自定义',
|
|
87
117
|
'settings.edit': '编辑',
|
|
88
118
|
'settings.delete': '删除',
|
|
89
119
|
'settings.deleteConfirm': '确定删除这张角色卡?',
|
|
120
|
+
'settings.deleteEditedConfirm': '这张卡改自内置版本。删除会丢弃你的修改,恢复成内置版本。继续?',
|
|
90
121
|
'settings.reset': '还原为内置',
|
|
91
122
|
'settings.resetConfirm': '用插件内置的版本覆盖这张卡?你对它的本地修改会丢失。',
|
|
92
123
|
'settings.resetHint': '把这张卡恢复成插件内置的版本,丢弃本地修改。',
|
|
@@ -115,6 +146,9 @@ window.__ModuleLoader__.load({
|
|
|
115
146
|
'settings.field.style': '角色风格描述(给改写模型的指令)',
|
|
116
147
|
'settings.field.template': '模板(用 {{input}} 代表你的原话)',
|
|
117
148
|
'settings.field.examples': '转换范例(每行一条,格式:原话 => 改写后)',
|
|
149
|
+
'settings.field.byIntensity': '分强度范例(每档各一组;留空则回退到上面的通用范例)',
|
|
150
|
+
'settings.field.byIntensity.empty': '留空 = 这一档沿用上面的通用范例',
|
|
151
|
+
'settings.hint.byIntensity': '四档强度各自用哪组范例。范例越贴合该档语气,改写越稳定。留空的档位会回退到通用范例。',
|
|
118
152
|
'settings.hint.fidelity': '只改语气:只换说法,需求一字不动。允许增加行为约束:改写时还能追加符合角色的做事风格(例如「别做冗长规划」)——这会真的改变你的需求,建议配合「先审查」。',
|
|
119
153
|
'settings.hint.examples': '范例是最强的风格锚点。每条范例里原话的技术事实必须出现在改写结果中——否则模型会学会"用氛围替换需求"。',
|
|
120
154
|
'settings.diagnostics': '加载问题',
|
|
@@ -140,6 +174,20 @@ window.__ModuleLoader__.load({
|
|
|
140
174
|
'control.sendMode.direct': 'Send directly',
|
|
141
175
|
'control.sendMode.hint.review': 'The rewrite appears in the comparison panel below; you confirm before it is sent, and your draft is untouched.',
|
|
142
176
|
'control.sendMode.hint.direct': 'The rewrite is sent as your message immediately, without confirmation.',
|
|
177
|
+
'control.mode': 'Rewrite mode',
|
|
178
|
+
'control.mode.card': 'Card default',
|
|
179
|
+
'control.mode.llm': 'Model rewrite',
|
|
180
|
+
'control.mode.template': 'Template',
|
|
181
|
+
'control.mode.hint': 'Card default = use whatever the card declares. A temporary switch applies to this session only.',
|
|
182
|
+
'control.fidelity': 'Fidelity',
|
|
183
|
+
'control.fidelity.card': 'Card default',
|
|
184
|
+
'control.fidelity.style': 'Voice only',
|
|
185
|
+
'control.fidelity.hint.style': 'This card declares voice-only, and it cannot be widened here — widening is a property of the card, so change it in the settings page.',
|
|
186
|
+
'control.fidelity.hint.strategy': 'This card may add behavioural constraints. You can narrow it to voice-only for this session, but not widen it back.',
|
|
187
|
+
'control.modelCheck': 'Model check',
|
|
188
|
+
'control.modelCheck.on': 'On',
|
|
189
|
+
'control.modelCheck.off': 'Off',
|
|
190
|
+
'control.modelCheck.hint': 'On: every rewrite makes one extra model call to catch what the local check cannot see, such as a requirement quietly weakened — at the cost of doubled latency and tokens. Off: the free local check still runs (identifiers, paths, versions, numbers).',
|
|
143
191
|
'panel.title': 'Persona rewrite',
|
|
144
192
|
'panel.loading': 'Rewriting as “{name}”…',
|
|
145
193
|
'panel.original': 'You wrote',
|
|
@@ -157,6 +205,10 @@ window.__ModuleLoader__.load({
|
|
|
157
205
|
'panel.drift': 'Technical facts may have changed',
|
|
158
206
|
'panel.checkUnavailable': 'Preservation check did not complete',
|
|
159
207
|
'panel.checkOk': 'Technical facts preserved',
|
|
208
|
+
'panel.factsDrift': 'Technical content changed',
|
|
209
|
+
'panel.factsMissing': 'Lost: ',
|
|
210
|
+
'panel.factsAdded': 'Added: ',
|
|
211
|
+
'panel.factsOnly': 'Local check only (no tokens spent): no identifier, path, version or number was lost. It cannot see a requirement quietly weakened, or a constraint added in prose — turn on the model check for those.',
|
|
160
212
|
'panel.stale': 'The draft changed while rewriting — the result is based on the earlier text.',
|
|
161
213
|
'error.empty': 'The input box is empty — write your request first.',
|
|
162
214
|
'error.phase': 'The input box is busy (submitting) — try again shortly.',
|
|
@@ -172,11 +224,24 @@ window.__ModuleLoader__.load({
|
|
|
172
224
|
'settings.dir': 'Card directory',
|
|
173
225
|
'settings.reveal': 'Open directory',
|
|
174
226
|
'settings.new': 'New character card',
|
|
175
|
-
'settings.
|
|
176
|
-
'settings.
|
|
227
|
+
'settings.ai': 'AI draft',
|
|
228
|
+
'settings.ai.tip': 'You can have the AI draft a card for you: describe the character in a sentence and the style text plus all four intensity example sets are written for you.',
|
|
229
|
+
'settings.ai.model': 'Model: {provider} / {model} (the settings page has no session context, so this is the harness default; the rewrite itself follows the current session\'s model)',
|
|
230
|
+
'settings.ai.desc': 'Describe the character in a sentence and the AI fills the form for you: the style text and conversion examples for all four intensity levels. The result only fills the form — it is never saved automatically.',
|
|
231
|
+
'settings.ai.briefLabel': 'Character idea (a sentence or a few)',
|
|
232
|
+
'settings.ai.briefPlaceholder': 'e.g. a grumpy but utterly reliable old lighthouse keeper who speaks in short sentences and nautical metaphors',
|
|
233
|
+
'settings.ai.generate': 'Generate draft',
|
|
234
|
+
'settings.ai.busy': 'Writing the character card…',
|
|
235
|
+
'settings.ai.hint': 'This is a draft — edit it before saving.',
|
|
236
|
+
'settings.ai.needBrief': 'Write the character idea first.',
|
|
237
|
+
'settings.ai.failed': 'Generation failed: {message}',
|
|
238
|
+
'settings.origin.builtin': 'Built-in',
|
|
239
|
+
'settings.origin.edited': 'Edited',
|
|
240
|
+
'settings.origin.custom': 'Custom',
|
|
177
241
|
'settings.edit': 'Edit',
|
|
178
242
|
'settings.delete': 'Delete',
|
|
179
243
|
'settings.deleteConfirm': 'Delete this character card?',
|
|
244
|
+
'settings.deleteEditedConfirm': 'This card is an edit of a bundled one. Deleting it discards your changes and restores the bundled version. Continue?',
|
|
180
245
|
'settings.reset': 'Restore bundled',
|
|
181
246
|
'settings.resetConfirm': 'Replace this card with the bundled version? Your local edits to it will be lost.',
|
|
182
247
|
'settings.resetHint': 'Restore this card to the version the plugin ships, discarding local edits.',
|
|
@@ -205,6 +270,9 @@ window.__ModuleLoader__.load({
|
|
|
205
270
|
'settings.field.style': 'Character style (instructions for the rewriting model)',
|
|
206
271
|
'settings.field.template': 'Template (use {{input}} for the user\'s text)',
|
|
207
272
|
'settings.field.examples': 'Conversion examples (one per line, format: original => rewritten)',
|
|
273
|
+
'settings.field.byIntensity': 'Per-intensity examples (one set each; empty falls back to the list above)',
|
|
274
|
+
'settings.field.byIntensity.empty': 'Empty = this level reuses the examples above',
|
|
275
|
+
'settings.hint.byIntensity': 'Which demonstrations each intensity level uses. The closer a set matches that level\'s tone, the steadier the rewrite. A level left empty falls back to the shared list.',
|
|
208
276
|
'settings.hint.fidelity': 'Voice only: wording changes, the request does not. May add behavioural constraints: the rewrite can also add the character\'s working style (e.g. "no long planning") — that genuinely changes your request, so pair it with Review first.',
|
|
209
277
|
'settings.hint.examples': 'Examples are the strongest style anchor. Every technical fact in an example\'s input must appear in its output — otherwise the model learns to replace requirements with atmosphere.',
|
|
210
278
|
'settings.diagnostics': 'Load problems',
|
|
@@ -327,6 +395,11 @@ window.__ModuleLoader__.load({
|
|
|
327
395
|
.pf-card-desc{display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;font-size:11px;line-height:16px;color:var(--dsw-alias-label-tertiary);word-break:break-word}
|
|
328
396
|
.pf-tag{font-size:10px;line-height:15px;padding:1px 5px;border-radius:var(--dsw-radius-xs);background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-secondary)}
|
|
329
397
|
.pf-form{display:grid;gap:10px;max-width:720px;padding:12px;border:.5px solid var(--dsw-alias-border-l2);border-radius:var(--dsw-radius-lg);background:var(--dsw-alias-bg-layer-2)}
|
|
398
|
+
/* The AI box inside the new-card form: a quiet inset panel, not a second
|
|
399
|
+
* competing call to action. */
|
|
400
|
+
.pf-ai{border:.5px solid var(--dsw-alias-border-l2);border-radius:var(--dsw-radius-md);background:var(--dsw-alias-bg-layer-1);overflow:hidden}
|
|
401
|
+
.pf-ai-head{display:flex;align-items:center;gap:6px;width:100%;padding:7px 10px;border:none;background:transparent;color:var(--dsw-alias-label-primary);font:inherit;font-size:12px;line-height:18px;font-weight:500;text-align:left;cursor:pointer}
|
|
402
|
+
.pf-ai-head:hover{background:var(--dsw-alias-interactive-bg-hover)}
|
|
330
403
|
.pf-field{display:grid;gap:4px}
|
|
331
404
|
.pf-field label{font-size:11px;line-height:15px;color:var(--dsw-alias-label-tertiary)}
|
|
332
405
|
.pf-field input,.pf-field select,.pf-field textarea{width:100%;box-sizing:border-box;padding:6px 8px;border:.5px solid var(--dsw-alias-border-l3);border-radius:var(--dsw-radius-sm);background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-primary);font:inherit;font-size:12px;line-height:18px;outline:none}
|
|
@@ -393,6 +466,7 @@ window.__ModuleLoader__.load({
|
|
|
393
466
|
const resetCard = (id) => postJson(`${PREFIX}/cards/reset`, { id })
|
|
394
467
|
const diffCards = () => postJson(`${PREFIX}/cards/diff`, {})
|
|
395
468
|
const revealDir = () => postJson(`${PREFIX}/reveal`, {})
|
|
469
|
+
const draftCard = (payload) => postJson(`${PREFIX}/cards/draft`, payload)
|
|
396
470
|
|
|
397
471
|
// ---------------------------------------------------------------------
|
|
398
472
|
// Shared UI state
|
|
@@ -424,6 +498,28 @@ window.__ModuleLoader__.load({
|
|
|
424
498
|
const sendModeKey = (key) => `persona-forge:sendMode:${key}`
|
|
425
499
|
const cardKey = (key) => `persona-forge:card:${key}`
|
|
426
500
|
const intensityKey = (key) => `persona-forge:intensity:${key}`
|
|
501
|
+
const modeKey = (key) => `persona-forge:mode:${key}`
|
|
502
|
+
const fidelityKey = (key) => `persona-forge:fidelity:${key}`
|
|
503
|
+
const modelCheckKey = (key) => `persona-forge:modelCheck:${key}`
|
|
504
|
+
|
|
505
|
+
/** Effective mode for a card: the session's pick when the card can honour it, else the card's. */
|
|
506
|
+
function effectiveModeOf(card, override) {
|
|
507
|
+
if (card === null) return 'llm'
|
|
508
|
+
if (override === 'template') return typeof card.template === 'string' && card.template.trim() !== '' ? 'template' : card.mode
|
|
509
|
+
if (override === 'llm') return typeof card.style === 'string' && card.style.trim() !== '' ? 'llm' : card.mode
|
|
510
|
+
return card.mode
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Effective fidelity for a card: the session may NARROW a strategy card to
|
|
515
|
+
* voice-only, never widen a voice-only card to strategy. The server enforces
|
|
516
|
+
* the same rule; this keeps the UI from offering a choice it would ignore.
|
|
517
|
+
*/
|
|
518
|
+
function effectiveFidelityOf(card, override) {
|
|
519
|
+
if (card === null) return 'style'
|
|
520
|
+
if (card.fidelity === 'strategy' && override === 'style') return 'style'
|
|
521
|
+
return card.fidelity
|
|
522
|
+
}
|
|
427
523
|
|
|
428
524
|
/**
|
|
429
525
|
* The "no persona" selection. It is a real, persisted state rather than the
|
|
@@ -511,7 +607,11 @@ window.__ModuleLoader__.load({
|
|
|
511
607
|
diagnostics: [],
|
|
512
608
|
directory: '',
|
|
513
609
|
hostSendMode: 'review',
|
|
514
|
-
|
|
610
|
+
// Mirrors the host default in lib/index.js: the model check is opt-in
|
|
611
|
+
// because it is paid for on every send, and the local check always runs.
|
|
612
|
+
factCheck: false,
|
|
613
|
+
/** Which model an AI draft would use, as a ready-to-render sentence. */
|
|
614
|
+
draftModelLabel: '',
|
|
515
615
|
panel: null,
|
|
516
616
|
}
|
|
517
617
|
function notify(next) {
|
|
@@ -539,7 +639,13 @@ window.__ModuleLoader__.load({
|
|
|
539
639
|
diagnostics: Array.isArray(value.diagnostics) ? value.diagnostics : [],
|
|
540
640
|
directory: typeof value.directory === 'string' ? value.directory : '',
|
|
541
641
|
hostSendMode: value.sendMode === 'direct' ? 'direct' : 'review',
|
|
542
|
-
factCheck: value.factCheck
|
|
642
|
+
factCheck: value.factCheck === true,
|
|
643
|
+
// Say which model an AI draft runs on. The settings slot is
|
|
644
|
+
// root-scoped and receives no session id, so this is the harness
|
|
645
|
+
// default — stating it beats letting the user assume.
|
|
646
|
+
draftModelLabel: value.draftModel === null || value.draftModel === undefined
|
|
647
|
+
? ''
|
|
648
|
+
: t('settings.ai.model', { provider: value.draftModel.provider, model: value.draftModel.model }),
|
|
543
649
|
catalogError: null,
|
|
544
650
|
})
|
|
545
651
|
return state.catalog
|
|
@@ -572,6 +678,9 @@ window.__ModuleLoader__.load({
|
|
|
572
678
|
cardName: options.cardName,
|
|
573
679
|
original: options.original,
|
|
574
680
|
intensity: options.intensity,
|
|
681
|
+
mode: options.mode,
|
|
682
|
+
fidelity: options.fidelity,
|
|
683
|
+
modelCheck: options.modelCheck,
|
|
575
684
|
sendMode: options.sendMode,
|
|
576
685
|
},
|
|
577
686
|
})
|
|
@@ -580,6 +689,9 @@ window.__ModuleLoader__.load({
|
|
|
580
689
|
cardId: options.cardId,
|
|
581
690
|
text: options.original,
|
|
582
691
|
intensity: options.intensity,
|
|
692
|
+
mode: options.mode,
|
|
693
|
+
fidelity: options.fidelity,
|
|
694
|
+
modelCheck: options.modelCheck,
|
|
583
695
|
}).then(
|
|
584
696
|
(value) => {
|
|
585
697
|
const current = state.panel
|
|
@@ -638,6 +750,18 @@ window.__ModuleLoader__.load({
|
|
|
638
750
|
})
|
|
639
751
|
const [cardId, setCardId] = useState(() => readLocal(cardKey(sessionKey)) ?? OFF)
|
|
640
752
|
const [intensity, setIntensity] = useState(() => readLocal(intensityKey(sessionKey)))
|
|
753
|
+
// Per-session overrides for the card's own mode and fidelity. Both are
|
|
754
|
+
// stored as "unset" until the user actually picks something, so following
|
|
755
|
+
// the card stays the default rather than an explicit choice.
|
|
756
|
+
const [modeOverride, setModeOverride] = useState(() => readLocal(modeKey(sessionKey)))
|
|
757
|
+
const [fidelityOverride, setFidelityOverride] = useState(() => readLocal(fidelityKey(sessionKey)))
|
|
758
|
+
// Tri-state: undefined = follow the host config, true/false = this
|
|
759
|
+
// session's explicit choice. Stored as a string because localStorage has
|
|
760
|
+
// no booleans.
|
|
761
|
+
const [modelCheckOverride, setModelCheckOverride] = useState(() => {
|
|
762
|
+
const stored = readLocal(modelCheckKey(sessionKey))
|
|
763
|
+
return stored === 'on' ? true : stored === 'off' ? false : undefined
|
|
764
|
+
})
|
|
641
765
|
|
|
642
766
|
useEffect(() => {
|
|
643
767
|
if (shared.catalog === null && shared.catalogError === null) {
|
|
@@ -651,6 +775,9 @@ window.__ModuleLoader__.load({
|
|
|
651
775
|
const selected = isOff ? null : cards.find((card) => card.id === cardId) ?? null
|
|
652
776
|
const busy = shared.panel !== null && shared.panel.phase === 'loading'
|
|
653
777
|
const effectiveIntensity = intensity ?? selected?.intensity ?? 'medium'
|
|
778
|
+
const effectiveMode = effectiveModeOf(selected, modeOverride)
|
|
779
|
+
const effectiveFidelity = effectiveFidelityOf(selected, fidelityOverride)
|
|
780
|
+
const effectiveModelCheck = modelCheckOverride ?? shared.factCheck
|
|
654
781
|
|
|
655
782
|
// The send-gesture listener is attached once per session but must read
|
|
656
783
|
// CURRENT values. A ref carries them across renders without re-attaching
|
|
@@ -690,6 +817,21 @@ window.__ModuleLoader__.load({
|
|
|
690
817
|
writeLocal(intensityKey(sessionKey), level)
|
|
691
818
|
}, [sessionKey])
|
|
692
819
|
|
|
820
|
+
const setMode = useCallback((mode) => {
|
|
821
|
+
setModeOverride(mode)
|
|
822
|
+
writeLocal(modeKey(sessionKey), mode)
|
|
823
|
+
}, [sessionKey])
|
|
824
|
+
|
|
825
|
+
const setFidelity = useCallback((fidelity) => {
|
|
826
|
+
setFidelityOverride(fidelity)
|
|
827
|
+
writeLocal(fidelityKey(sessionKey), fidelity)
|
|
828
|
+
}, [sessionKey])
|
|
829
|
+
|
|
830
|
+
const setModelCheck = useCallback((enabled) => {
|
|
831
|
+
setModelCheckOverride(enabled)
|
|
832
|
+
writeLocal(modelCheckKey(sessionKey), enabled ? 'on' : 'off')
|
|
833
|
+
}, [sessionKey])
|
|
834
|
+
|
|
693
835
|
// Position the menu above the button, clamped to the viewport.
|
|
694
836
|
useEffect(() => {
|
|
695
837
|
if (!open) return
|
|
@@ -745,9 +887,12 @@ window.__ModuleLoader__.load({
|
|
|
745
887
|
cardName: selected.name,
|
|
746
888
|
original: text,
|
|
747
889
|
intensity: effectiveIntensity,
|
|
890
|
+
mode: effectiveMode,
|
|
891
|
+
fidelity: effectiveFidelity,
|
|
892
|
+
modelCheck: effectiveModelCheck,
|
|
748
893
|
sendMode: effectiveSendMode,
|
|
749
894
|
})
|
|
750
|
-
}, [busy, cardId, draft, effectiveIntensity, effectiveSendMode, phase, props.sessionId, selected, sessionKey])
|
|
895
|
+
}, [busy, cardId, draft, effectiveFidelity, effectiveIntensity, effectiveMode, effectiveModelCheck, effectiveSendMode, phase, props.sessionId, selected, sessionKey])
|
|
751
896
|
|
|
752
897
|
// Intercept the composer's OWN send action.
|
|
753
898
|
//
|
|
@@ -967,6 +1112,80 @@ window.__ModuleLoader__.load({
|
|
|
967
1112
|
),
|
|
968
1113
|
),
|
|
969
1114
|
h('div', { className: 'pf-hint' }, t(`control.sendMode.hint.${effectiveSendMode}`)),
|
|
1115
|
+
// Mode and fidelity, per session. Both defer to the card until the
|
|
1116
|
+
// user picks: following the card is the default, not a choice the
|
|
1117
|
+
// user has to rediscover.
|
|
1118
|
+
h(
|
|
1119
|
+
'div',
|
|
1120
|
+
{ className: 'pf-row' },
|
|
1121
|
+
h('span', { className: 'pf-row-label' }, t('control.mode')),
|
|
1122
|
+
h(
|
|
1123
|
+
'span',
|
|
1124
|
+
{ className: 'pf-seg' },
|
|
1125
|
+
[['', t('control.mode.card')], ['llm', t('control.mode.llm')], ['template', t('control.mode.template')]].map(([value, label]) => h(
|
|
1126
|
+
'button',
|
|
1127
|
+
{
|
|
1128
|
+
key: value === '' ? 'card' : value,
|
|
1129
|
+
type: 'button',
|
|
1130
|
+
'aria-pressed': (modeOverride ?? '') === value ? 'true' : 'false',
|
|
1131
|
+
className: (modeOverride ?? '') === value ? 'is-on' : '',
|
|
1132
|
+
onClick: () => setMode(value === '' ? undefined : value),
|
|
1133
|
+
},
|
|
1134
|
+
label,
|
|
1135
|
+
)),
|
|
1136
|
+
),
|
|
1137
|
+
),
|
|
1138
|
+
h('div', { className: 'pf-hint' }, t('control.mode.hint')),
|
|
1139
|
+
h(
|
|
1140
|
+
'div',
|
|
1141
|
+
{ className: 'pf-row' },
|
|
1142
|
+
h('span', { className: 'pf-row-label' }, t('control.fidelity')),
|
|
1143
|
+
h(
|
|
1144
|
+
'span',
|
|
1145
|
+
{ className: 'pf-seg' },
|
|
1146
|
+
[['', t('control.fidelity.card')], ['style', t('control.fidelity.style')]].map(([value, label]) => h(
|
|
1147
|
+
'button',
|
|
1148
|
+
{
|
|
1149
|
+
key: value === '' ? 'card' : value,
|
|
1150
|
+
type: 'button',
|
|
1151
|
+
'aria-pressed': (fidelityOverride ?? '') === value ? 'true' : 'false',
|
|
1152
|
+
className: (fidelityOverride ?? '') === value ? 'is-on' : '',
|
|
1153
|
+
onClick: () => setFidelity(value === '' ? undefined : value),
|
|
1154
|
+
},
|
|
1155
|
+
label,
|
|
1156
|
+
)),
|
|
1157
|
+
),
|
|
1158
|
+
),
|
|
1159
|
+
// A voice-only card can never be widened from here, so the hint
|
|
1160
|
+
// says which card-level rule is in force rather than offering a
|
|
1161
|
+
// control that would be ignored.
|
|
1162
|
+
h('div', { className: 'pf-hint' }, selected?.fidelity === 'strategy'
|
|
1163
|
+
? t('control.fidelity.hint.strategy')
|
|
1164
|
+
: t('control.fidelity.hint.style')),
|
|
1165
|
+
// The model preservation check costs a second call per send, so
|
|
1166
|
+
// it is a visible switch rather than a hidden config file edit.
|
|
1167
|
+
// The free local check runs either way and cannot be turned off.
|
|
1168
|
+
h(
|
|
1169
|
+
'div',
|
|
1170
|
+
{ className: 'pf-row' },
|
|
1171
|
+
h('span', { className: 'pf-row-label' }, t('control.modelCheck')),
|
|
1172
|
+
h(
|
|
1173
|
+
'span',
|
|
1174
|
+
{ className: 'pf-seg' },
|
|
1175
|
+
[[true, t('control.modelCheck.on')], [false, t('control.modelCheck.off')]].map(([value, label]) => h(
|
|
1176
|
+
'button',
|
|
1177
|
+
{
|
|
1178
|
+
key: String(value),
|
|
1179
|
+
type: 'button',
|
|
1180
|
+
'aria-pressed': effectiveModelCheck === value ? 'true' : 'false',
|
|
1181
|
+
className: effectiveModelCheck === value ? 'is-on' : '',
|
|
1182
|
+
onClick: () => setModelCheck(value),
|
|
1183
|
+
},
|
|
1184
|
+
label,
|
|
1185
|
+
)),
|
|
1186
|
+
),
|
|
1187
|
+
),
|
|
1188
|
+
h('div', { className: 'pf-hint' }, t('control.modelCheck.hint')),
|
|
970
1189
|
h('div', { className: 'pf-menu-sep' }),
|
|
971
1190
|
h(
|
|
972
1191
|
'button',
|
|
@@ -1100,18 +1319,33 @@ window.__ModuleLoader__.load({
|
|
|
1100
1319
|
const text = typeof result.text === 'string' ? result.text : ''
|
|
1101
1320
|
const isTemplate = result.mode === 'template'
|
|
1102
1321
|
const check = result.factCheck ?? { state: 'skipped' }
|
|
1322
|
+
const facts = result.facts ?? { state: 'ok', missing: [], added: [] }
|
|
1103
1323
|
const stale = typeof draft === 'string' && draft !== panel.original
|
|
1104
1324
|
const direct = panel.sendMode === 'direct'
|
|
1105
1325
|
const intensityLabel = t(`control.intensity.${result.intensity ?? panel.intensity ?? 'medium'}`)
|
|
1106
1326
|
|
|
1107
1327
|
const notes = []
|
|
1108
1328
|
if (stale && !direct) notes.push(h('div', { key: 'stale', className: 'pf-note is-muted' }, t('panel.stale')))
|
|
1329
|
+
// The deterministic verdict comes first: it is always available, it costs
|
|
1330
|
+
// nothing, and it names the exact token that went missing — which is more
|
|
1331
|
+
// actionable than a model's sentence about it.
|
|
1332
|
+
if (facts.state === 'drift') {
|
|
1333
|
+
const parts = []
|
|
1334
|
+
if (facts.missing.length > 0) parts.push(`${t('panel.factsMissing')}${facts.missing.join('、')}`)
|
|
1335
|
+
if (facts.added.length > 0) parts.push(`${t('panel.factsAdded')}${facts.added.join('、')}`)
|
|
1336
|
+
notes.push(h('div', { key: 'facts', className: 'pf-note is-warn' }, `${t('panel.factsDrift')} — ${parts.join(';')}`))
|
|
1337
|
+
}
|
|
1109
1338
|
if (check.state === 'drift') {
|
|
1110
1339
|
notes.push(h('div', { key: 'drift', className: 'pf-note is-warn' }, `${t('panel.drift')}${check.reason !== undefined ? `:${check.reason}` : ''}`))
|
|
1111
1340
|
} else if (check.state === 'unavailable') {
|
|
1112
1341
|
notes.push(h('div', { key: 'unavail', className: 'pf-note is-muted' }, `${t('panel.checkUnavailable')}${check.reason !== undefined ? `:${check.reason}` : ''}`))
|
|
1113
1342
|
} else if (check.state === 'ok') {
|
|
1114
1343
|
notes.push(h('div', { key: 'ok', className: 'pf-note is-ok' }, t('panel.checkOk')))
|
|
1344
|
+
} else if (check.state === 'skipped' && facts.state === 'ok') {
|
|
1345
|
+
// With the model check off, the free check still reported. Say so, and
|
|
1346
|
+
// say plainly what it did NOT cover — a clean verdict here is narrower
|
|
1347
|
+
// than a clean verdict with the model check on.
|
|
1348
|
+
notes.push(h('div', { key: 'factsonly', className: 'pf-note is-muted' }, t('panel.factsOnly')))
|
|
1115
1349
|
}
|
|
1116
1350
|
if (direct) notes.push(h('div', { key: 'sent', className: 'pf-note is-muted' }, `${t('panel.sent')} — ${t('panel.sent.hint')}`))
|
|
1117
1351
|
|
|
@@ -1171,6 +1405,9 @@ window.__ModuleLoader__.load({
|
|
|
1171
1405
|
cardName: panel.cardName,
|
|
1172
1406
|
original: panel.original,
|
|
1173
1407
|
intensity: panel.intensity,
|
|
1408
|
+
mode: panel.mode,
|
|
1409
|
+
fidelity: panel.fidelity,
|
|
1410
|
+
modelCheck: panel.modelCheck,
|
|
1174
1411
|
sendMode: panel.sendMode,
|
|
1175
1412
|
}),
|
|
1176
1413
|
},
|
|
@@ -1196,6 +1433,7 @@ window.__ModuleLoader__.load({
|
|
|
1196
1433
|
style: '',
|
|
1197
1434
|
template: '',
|
|
1198
1435
|
examplesText: '',
|
|
1436
|
+
byIntensityText: { light: '', medium: '', strong: '', zealot: '' },
|
|
1199
1437
|
}
|
|
1200
1438
|
|
|
1201
1439
|
/** Render a card into the form's flat shape. */
|
|
@@ -1211,6 +1449,16 @@ window.__ModuleLoader__.load({
|
|
|
1211
1449
|
style: card.style ?? '',
|
|
1212
1450
|
template: card.template ?? '',
|
|
1213
1451
|
examplesText: (card.examples ?? []).map((example) => `${example.from} => ${example.to}`).join('\n'),
|
|
1452
|
+
// The per-intensity demonstrations are part of the card, so the form
|
|
1453
|
+
// carries them. Leaving them out of the form dropped them on the next
|
|
1454
|
+
// save: editing a bundled card's name silently discarded all four
|
|
1455
|
+
// intensity example sets, and the card fell back to the flat list.
|
|
1456
|
+
byIntensityText: Object.fromEntries(
|
|
1457
|
+
INTENSITY_LEVELS.map((level) => [
|
|
1458
|
+
level,
|
|
1459
|
+
(card.examplesByIntensity?.[level] ?? []).map((example) => `${example.from} => ${example.to}`).join('\n'),
|
|
1460
|
+
]),
|
|
1461
|
+
),
|
|
1214
1462
|
}
|
|
1215
1463
|
}
|
|
1216
1464
|
|
|
@@ -1230,7 +1478,7 @@ window.__ModuleLoader__.load({
|
|
|
1230
1478
|
return out
|
|
1231
1479
|
}
|
|
1232
1480
|
|
|
1233
|
-
function CardsSettings() {
|
|
1481
|
+
function CardsSettings(props) {
|
|
1234
1482
|
const shared = useSyncExternalStore(subscribe, getState)
|
|
1235
1483
|
const [form, setForm] = useState(null)
|
|
1236
1484
|
const [status, setStatus] = useState(null)
|
|
@@ -1238,6 +1486,13 @@ window.__ModuleLoader__.load({
|
|
|
1238
1486
|
// Nothing here runs on its own: the drift check is an explicit action so
|
|
1239
1487
|
// the list stays clean, and only a drifted card grows a restore button.
|
|
1240
1488
|
const [drift, setDrift] = useState(null)
|
|
1489
|
+
// AI 代填: a rough idea in, a prefilled form out.
|
|
1490
|
+
const [brief, setBrief] = useState('')
|
|
1491
|
+
const [drafting, setDrafting] = useState(false)
|
|
1492
|
+
// The AI box belongs to the NEW-card flow, not to the page: showing it
|
|
1493
|
+
// permanently put two unrelated "make a card" actions side by side and
|
|
1494
|
+
// left the user guessing which one to press. It opens with 新建角色卡.
|
|
1495
|
+
const [aiOpen, setAiOpen] = useState(false)
|
|
1241
1496
|
|
|
1242
1497
|
useEffect(() => {
|
|
1243
1498
|
void loadCatalog(false).catch(() => {})
|
|
@@ -1260,6 +1515,15 @@ window.__ModuleLoader__.load({
|
|
|
1260
1515
|
if (form.icon.trim() !== '') payload.icon = form.icon.trim()
|
|
1261
1516
|
if (form.description.trim() !== '') payload.description = form.description.trim()
|
|
1262
1517
|
if (form.mode === 'template') payload.template = form.template
|
|
1518
|
+
// Only rungs the user actually filled are sent. An empty textarea means
|
|
1519
|
+
// "no per-intensity set for this level", which makes `examplesFor` fall
|
|
1520
|
+
// back to the flat list — the documented behaviour, not an empty demo.
|
|
1521
|
+
const byIntensity = {}
|
|
1522
|
+
for (const level of INTENSITY_LEVELS) {
|
|
1523
|
+
const parsed = parseExamples(form.byIntensityText?.[level])
|
|
1524
|
+
if (parsed.length > 0) byIntensity[level] = parsed
|
|
1525
|
+
}
|
|
1526
|
+
if (Object.keys(byIntensity).length > 0) payload.examplesByIntensity = byIntensity
|
|
1263
1527
|
saveCard(payload).then(
|
|
1264
1528
|
() => {
|
|
1265
1529
|
setStatus({ kind: 'ok', text: t('settings.saved') })
|
|
@@ -1271,7 +1535,11 @@ window.__ModuleLoader__.load({
|
|
|
1271
1535
|
}
|
|
1272
1536
|
|
|
1273
1537
|
const remove = (card) => {
|
|
1274
|
-
|
|
1538
|
+
// Deleting an edited card is not the same act as deleting a card the
|
|
1539
|
+
// user wrote from scratch: it falls back to the bundled version rather
|
|
1540
|
+
// than removing the character entirely, and the wording has to say so.
|
|
1541
|
+
const confirmKey = card.origin === 'edited' ? 'settings.deleteEditedConfirm' : 'settings.deleteConfirm'
|
|
1542
|
+
if (!window.confirm(t(confirmKey))) return
|
|
1275
1543
|
deleteCard(card.id).then(
|
|
1276
1544
|
() => {
|
|
1277
1545
|
setStatus({ kind: 'ok', text: t('settings.saved') })
|
|
@@ -1297,6 +1565,59 @@ window.__ModuleLoader__.load({
|
|
|
1297
1565
|
)
|
|
1298
1566
|
}
|
|
1299
1567
|
|
|
1568
|
+
/**
|
|
1569
|
+
* Ask the host to write a card from the user's rough idea, then drop the
|
|
1570
|
+
* result into the form.
|
|
1571
|
+
*
|
|
1572
|
+
* The generated card is NEVER saved here. It only prefills the form, so
|
|
1573
|
+
* the user reviews the wording, the examples and the id before anything
|
|
1574
|
+
* reaches the card directory — the same standard the rewrite panel is
|
|
1575
|
+
* held to. A generation the user dislikes costs one reload.
|
|
1576
|
+
*/
|
|
1577
|
+
const generate = () => {
|
|
1578
|
+
const text = brief.trim()
|
|
1579
|
+
if (text === '') {
|
|
1580
|
+
setStatus({ kind: 'error', text: t('settings.ai.needBrief') })
|
|
1581
|
+
return
|
|
1582
|
+
}
|
|
1583
|
+
setDrafting(true)
|
|
1584
|
+
setStatus(null)
|
|
1585
|
+
draftCard({ brief: text, sessionId: typeof props?.sessionId === 'string' && props.sessionId !== '' ? props.sessionId : undefined }).then(
|
|
1586
|
+
(value) => {
|
|
1587
|
+
setDrafting(false)
|
|
1588
|
+
setForm(toForm(value?.card ?? {}))
|
|
1589
|
+
setStatus({ kind: 'ok', text: t('settings.ai.hint') })
|
|
1590
|
+
},
|
|
1591
|
+
(error) => {
|
|
1592
|
+
setDrafting(false)
|
|
1593
|
+
setStatus({ kind: 'error', text: t('settings.ai.failed', { message: error?.message ?? t('error.internal') }) })
|
|
1594
|
+
},
|
|
1595
|
+
)
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
/**
|
|
1599
|
+
* Start a new card: open a blank form AND offer the AI box beside it.
|
|
1600
|
+
*
|
|
1601
|
+
* Both live in the same flow on purpose. A blank form and an AI box
|
|
1602
|
+
* presented as two equal toolbar buttons made the user choose between
|
|
1603
|
+
* them before knowing they were related; opening them together makes
|
|
1604
|
+
* "write it yourself, or describe it and let the AI start you off" one
|
|
1605
|
+
* decision instead of two.
|
|
1606
|
+
*/
|
|
1607
|
+
const startNew = () => {
|
|
1608
|
+
setForm({ ...BLANK_CARD })
|
|
1609
|
+
setBrief('')
|
|
1610
|
+
setAiOpen(true)
|
|
1611
|
+
setStatus(null)
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1614
|
+
/** Close the new-card flow entirely (form and AI box together). */
|
|
1615
|
+
const cancelForm = () => {
|
|
1616
|
+
setForm(null)
|
|
1617
|
+
setAiOpen(false)
|
|
1618
|
+
setBrief('')
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1300
1621
|
/** Ask the host which user cards differ from their bundled counterpart. */
|
|
1301
1622
|
const check = () => {
|
|
1302
1623
|
setDrift(undefined)
|
|
@@ -1321,7 +1642,7 @@ window.__ModuleLoader__.load({
|
|
|
1321
1642
|
h(
|
|
1322
1643
|
'div',
|
|
1323
1644
|
{ className: 'pf-settings-bar' },
|
|
1324
|
-
h('button', { type: 'button', className: 'pf-action', onClick:
|
|
1645
|
+
h('button', { type: 'button', className: 'pf-action', onClick: startNew }, t('settings.new')),
|
|
1325
1646
|
h(
|
|
1326
1647
|
'button',
|
|
1327
1648
|
{
|
|
@@ -1348,6 +1669,10 @@ window.__ModuleLoader__.load({
|
|
|
1348
1669
|
),
|
|
1349
1670
|
h('span', { className: 'pf-small' }, t('settings.count', { n: cards.length })),
|
|
1350
1671
|
),
|
|
1672
|
+
// Discoverability, not a second action. The AI box lives inside the
|
|
1673
|
+
// new-card flow; this line is what tells a user it exists, so they do
|
|
1674
|
+
// not have to press a button to find out.
|
|
1675
|
+
h('div', { className: 'pf-small', style: { margin: '0 0 10px' } }, t('settings.ai.tip')),
|
|
1351
1676
|
// The check result, stated once above the list.
|
|
1352
1677
|
drift === null
|
|
1353
1678
|
? h('div', { className: 'pf-small', style: { marginBottom: '10px' } }, t('settings.check.desc'))
|
|
@@ -1381,6 +1706,49 @@ window.__ModuleLoader__.load({
|
|
|
1381
1706
|
? h(
|
|
1382
1707
|
'div',
|
|
1383
1708
|
{ className: 'pf-form', style: { margin: '12px 0' } },
|
|
1709
|
+
// The AI box, first thing in the new-card flow: describe it, or
|
|
1710
|
+
// fill the fields below by hand. It is collapsible so a user who
|
|
1711
|
+
// came to type does not have to scroll past it.
|
|
1712
|
+
h(
|
|
1713
|
+
'div',
|
|
1714
|
+
{ className: 'pf-ai' },
|
|
1715
|
+
h(
|
|
1716
|
+
'button',
|
|
1717
|
+
{
|
|
1718
|
+
type: 'button',
|
|
1719
|
+
className: 'pf-ai-head',
|
|
1720
|
+
'aria-expanded': aiOpen ? 'true' : 'false',
|
|
1721
|
+
onClick: () => setAiOpen((value) => !value),
|
|
1722
|
+
},
|
|
1723
|
+
h('span', { 'aria-hidden': true }, aiOpen ? '▾' : '▸'),
|
|
1724
|
+
h('span', null, t('settings.ai')),
|
|
1725
|
+
),
|
|
1726
|
+
aiOpen
|
|
1727
|
+
? h(
|
|
1728
|
+
'div',
|
|
1729
|
+
{ style: { display: 'grid', gap: '8px', padding: '8px 10px 10px' } },
|
|
1730
|
+
h('textarea', {
|
|
1731
|
+
value: brief,
|
|
1732
|
+
placeholder: t('settings.ai.briefPlaceholder'),
|
|
1733
|
+
disabled: drafting,
|
|
1734
|
+
onChange: (event) => setBrief(event.target.value),
|
|
1735
|
+
style: { width: '100%', boxSizing: 'border-box', minHeight: '60px', padding: '6px 8px', border: '.5px solid var(--dsw-alias-border-l3)', borderRadius: 'var(--dsw-radius-sm)', background: 'var(--dsw-alias-bg-base)', color: 'var(--dsw-alias-label-primary)', font: 'inherit', fontSize: '12px', lineHeight: '18px', outline: 'none', resize: 'vertical' },
|
|
1736
|
+
}),
|
|
1737
|
+
h('div', { className: 'pf-small' }, t('settings.ai.desc')),
|
|
1738
|
+
h(
|
|
1739
|
+
'div',
|
|
1740
|
+
{ className: 'pf-actions' },
|
|
1741
|
+
h('button', {
|
|
1742
|
+
type: 'button',
|
|
1743
|
+
className: 'pf-action',
|
|
1744
|
+
disabled: drafting || brief.trim() === '',
|
|
1745
|
+
onClick: generate,
|
|
1746
|
+
}, drafting ? t('settings.ai.busy') : t('settings.ai.generate')),
|
|
1747
|
+
h('span', { className: 'pf-small' }, shared.draftModelLabel),
|
|
1748
|
+
),
|
|
1749
|
+
)
|
|
1750
|
+
: null,
|
|
1751
|
+
),
|
|
1384
1752
|
h(
|
|
1385
1753
|
'div',
|
|
1386
1754
|
{ className: 'pf-grid2' },
|
|
@@ -1453,11 +1821,33 @@ window.__ModuleLoader__.load({
|
|
|
1453
1821
|
h('div', { className: 'pf-small' }, t('settings.hint.examples')),
|
|
1454
1822
|
)
|
|
1455
1823
|
: null,
|
|
1824
|
+
// The four intensity sets. These are part of the card, so the form
|
|
1825
|
+
// has to show them: an editable card whose strongest feature is
|
|
1826
|
+
// invisible is a card the user cannot trust an edit to preserve.
|
|
1827
|
+
form.mode === 'llm'
|
|
1828
|
+
? h(
|
|
1829
|
+
'div',
|
|
1830
|
+
{ className: 'pf-field' },
|
|
1831
|
+
h('label', null, t('settings.field.byIntensity')),
|
|
1832
|
+
...INTENSITY_LEVELS.map((level) => h(
|
|
1833
|
+
'div',
|
|
1834
|
+
{ key: level, style: { display: 'grid', gap: '2px', marginBottom: '6px' } },
|
|
1835
|
+
h('div', { className: 'pf-small' }, t(`control.intensity.${level}`)),
|
|
1836
|
+
h('textarea', {
|
|
1837
|
+
value: form.byIntensityText?.[level] ?? '',
|
|
1838
|
+
placeholder: t('settings.field.byIntensity.empty'),
|
|
1839
|
+
onChange: (event) => set({ byIntensityText: { ...(form.byIntensityText ?? {}), [level]: event.target.value } }),
|
|
1840
|
+
style: { minHeight: '60px' },
|
|
1841
|
+
}),
|
|
1842
|
+
)),
|
|
1843
|
+
h('div', { className: 'pf-small' }, t('settings.hint.byIntensity')),
|
|
1844
|
+
)
|
|
1845
|
+
: null,
|
|
1456
1846
|
h(
|
|
1457
1847
|
'div',
|
|
1458
1848
|
{ className: 'pf-actions' },
|
|
1459
1849
|
h('button', { type: 'button', className: 'pf-action is-primary', onClick: commit, disabled: form.id.trim() === '' || form.name.trim() === '' }, t('settings.save')),
|
|
1460
|
-
h('button', { type: 'button', className: 'pf-action', onClick:
|
|
1850
|
+
h('button', { type: 'button', className: 'pf-action', onClick: cancelForm }, t('settings.cancel')),
|
|
1461
1851
|
),
|
|
1462
1852
|
)
|
|
1463
1853
|
: null,
|
|
@@ -1475,7 +1865,7 @@ window.__ModuleLoader__.load({
|
|
|
1475
1865
|
'span',
|
|
1476
1866
|
{ className: 'pf-card-name' },
|
|
1477
1867
|
card.name,
|
|
1478
|
-
h('span', { className: 'pf-tag' },
|
|
1868
|
+
h('span', { className: 'pf-tag' }, t(`settings.origin.${card.origin ?? 'custom'}`)),
|
|
1479
1869
|
h('span', { className: 'pf-tag' }, card.mode === 'template' ? 'template' : 'llm'),
|
|
1480
1870
|
h('span', { className: 'pf-tag' }, t(`control.intensity.${card.intensity}`)),
|
|
1481
1871
|
),
|
|
@@ -1491,7 +1881,13 @@ window.__ModuleLoader__.load({
|
|
|
1491
1881
|
onClick: () => reset(card),
|
|
1492
1882
|
}, t('settings.reset'))
|
|
1493
1883
|
: null,
|
|
1494
|
-
|
|
1884
|
+
// Delete is offered only where it can actually take effect. A card
|
|
1885
|
+
// whose content still matches the bundle has no user-owned file:
|
|
1886
|
+
// removing it just clears a copied seed that the next start writes
|
|
1887
|
+
// back, so the button would report success and change nothing.
|
|
1888
|
+
card.origin === 'builtin'
|
|
1889
|
+
? null
|
|
1890
|
+
: h('button', { type: 'button', className: 'pf-action', onClick: () => remove(card) }, t('settings.delete')),
|
|
1495
1891
|
)),
|
|
1496
1892
|
),
|
|
1497
1893
|
)
|