tavern-harness 0.1.24 → 0.1.26
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 +42 -0
- package/package.json +1 -1
- package/src/App.tsx +4 -3
- package/src/components/CharacterBookEditor.tsx +11 -0
- package/src/components/CharactersView.tsx +43 -35
- package/src/components/ChatView.tsx +1 -1
- package/src/components/ConfirmationDialog.tsx +3 -3
- package/src/components/DeleteConfirmDialog.tsx +1 -1
- package/src/components/FileDisplayModal.tsx +1 -1
- package/src/components/GameplayDialogs.tsx +2 -2
- package/src/components/NewSessionMenu.tsx +2 -2
- package/src/components/SettingsView.tsx +4 -4
- package/src/components/Sidebar.tsx +2 -2
- package/src/components/StatsView.tsx +5 -5
- package/src/components/VersionHistoryModal.tsx +1 -1
- package/src/components/WorkspaceFileManagerModal.tsx +1 -1
- package/src/core/i18n.ts +6 -6
- package/src/generated/changelog.json +65 -1
- package/src/theme/chat.css +165 -159
- package/src/theme/global.css +290 -140
- package/src/theme/views.css +293 -201
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,46 @@
|
|
|
3
3
|
> 本文件由 `scripts/changelog.mjs` 从 `changelog/v*.md` 生成,请勿手动编辑。
|
|
4
4
|
> 版本对比链接见文末;源文件位于仓库 `changelog/` 目录。
|
|
5
5
|
|
|
6
|
+
## [0.1.26] - 2026-10-27
|
|
7
|
+
|
|
8
|
+
### 中文
|
|
9
|
+
|
|
10
|
+
#### 改进
|
|
11
|
+
|
|
12
|
+
- 角色卡与世界书的知识库编辑器现在支持 JSON 语法高亮,键名、字符串、数字等用不同颜色区分,长内容更容易扫读
|
|
13
|
+
- 知识库 JSON 格式有误时,提示会指出具体出错的第几行,方便直接定位修改
|
|
14
|
+
|
|
15
|
+
#### 修复
|
|
16
|
+
|
|
17
|
+
- 修复手机上编辑角色时,切换「角色 / 知识库」标签会收起键盘、底部按钮随之闪烁跳动的问题
|
|
18
|
+
- 修复技能卡片里过长的工具名在手机上撑出屏幕、把操作按钮挤到看不见的问题
|
|
19
|
+
|
|
20
|
+
### English
|
|
21
|
+
|
|
22
|
+
#### Changed
|
|
23
|
+
|
|
24
|
+
- The character-card and world-book knowledge editors now highlight JSON syntax, coloring keys, strings, and numbers so long entries are easier to scan
|
|
25
|
+
- When knowledge-base JSON is invalid, the error now names the exact line, so the problem can be found and fixed directly
|
|
26
|
+
|
|
27
|
+
#### Fixed
|
|
28
|
+
|
|
29
|
+
- Fixed an issue on mobile where switching between the Character and Knowledge tabs while editing a character dismissed the keyboard and made the bottom buttons flicker
|
|
30
|
+
- Fixed overly long tool names in skill cards overflowing the screen on mobile and pushing the action buttons out of view
|
|
31
|
+
|
|
32
|
+
## [0.1.25] - 2026-10-27
|
|
33
|
+
|
|
34
|
+
### 中文
|
|
35
|
+
|
|
36
|
+
#### 内部
|
|
37
|
+
|
|
38
|
+
- 统一了标题、对话框、工具卡片等界面文字的字体排印样式,改用共享的排版类和样式变量,后续调整字号、字重只需改一处
|
|
39
|
+
|
|
40
|
+
### English
|
|
41
|
+
|
|
42
|
+
#### Internal
|
|
43
|
+
|
|
44
|
+
- Unified the typography of headings, dialogs, tool cards, and other interface text behind shared classes and style variables, so future font-size and weight changes happen in one place
|
|
45
|
+
|
|
6
46
|
## [0.1.24] - 2026-10-27
|
|
7
47
|
|
|
8
48
|
### 中文
|
|
@@ -702,6 +742,8 @@
|
|
|
702
742
|
- Vite + React + Zustand + Dexie stack with browser-side local persistence
|
|
703
743
|
|
|
704
744
|
<!-- 链接定义 -->
|
|
745
|
+
[0.1.26]: https://github.com/Rannichan/Tavern-Harness/compare/v0.1.25...v0.1.26
|
|
746
|
+
[0.1.25]: https://github.com/Rannichan/Tavern-Harness/compare/v0.1.24...v0.1.25
|
|
705
747
|
[0.1.24]: https://github.com/Rannichan/Tavern-Harness/compare/v0.1.23...v0.1.24
|
|
706
748
|
[0.1.23]: https://github.com/Rannichan/Tavern-Harness/compare/v0.1.22...v0.1.23
|
|
707
749
|
[0.1.22]: https://github.com/Rannichan/Tavern-Harness/compare/v0.1.21...v0.1.22
|
package/package.json
CHANGED
package/src/App.tsx
CHANGED
|
@@ -165,7 +165,8 @@ export default function App() {
|
|
|
165
165
|
type="button"
|
|
166
166
|
disabled={takingControl}
|
|
167
167
|
onClick={() => void handleTakeOverControl()}
|
|
168
|
-
|
|
168
|
+
className="control-takeover-btn"
|
|
169
|
+
style={{ cursor: takingControl ? 'wait' : 'pointer' }}
|
|
169
170
|
>
|
|
170
171
|
{t('control.takeOverHere')}
|
|
171
172
|
</button>
|
|
@@ -448,7 +449,7 @@ function SessionHeader({
|
|
|
448
449
|
{confirmReset && (
|
|
449
450
|
<Modal onClose={() => setConfirmReset(false)} size="compact">
|
|
450
451
|
<div className="modal-head">
|
|
451
|
-
<span
|
|
452
|
+
<span className="typo-dialog-title">{t('header.resetSessionTitle')}</span>
|
|
452
453
|
<button className="icon-btn" onClick={() => setConfirmReset(false)}><Icon name="x" /></button>
|
|
453
454
|
</div>
|
|
454
455
|
<div className="modal-body">
|
|
@@ -473,7 +474,7 @@ function SessionHeader({
|
|
|
473
474
|
{confirmDelete && (
|
|
474
475
|
<Modal onClose={() => setConfirmDelete(false)} size="compact">
|
|
475
476
|
<div className="modal-head">
|
|
476
|
-
<span
|
|
477
|
+
<span className="typo-dialog-title">{t('header.deleteTitle')}</span>
|
|
477
478
|
<button className="icon-btn" onClick={() => setConfirmDelete(false)}><Icon name="x" /></button>
|
|
478
479
|
</div>
|
|
479
480
|
<div className="modal-body">
|
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
import { memo, useEffect, useRef } from 'react';
|
|
2
2
|
import { Compartment, EditorState } from '@codemirror/state';
|
|
3
3
|
import { EditorView, lineNumbers } from '@codemirror/view';
|
|
4
|
+
import { HighlightStyle, syntaxHighlighting } from '@codemirror/language';
|
|
4
5
|
import { json } from '@codemirror/lang-json';
|
|
6
|
+
import { tags } from '@lezer/highlight';
|
|
7
|
+
|
|
8
|
+
const characterBookJsonHighlight = syntaxHighlighting(HighlightStyle.define([
|
|
9
|
+
{ tag: tags.propertyName, color: 'var(--primary-dim)', fontWeight: '600' },
|
|
10
|
+
{ tag: tags.string, color: 'var(--success)' },
|
|
11
|
+
{ tag: [tags.number, tags.bool, tags.null], color: 'var(--think)' },
|
|
12
|
+
{ tag: tags.keyword, color: 'var(--primary)' },
|
|
13
|
+
{ tag: tags.punctuation, color: 'var(--text-dim)' },
|
|
14
|
+
]));
|
|
5
15
|
|
|
6
16
|
const CharacterBookEditor = memo(function CharacterBookEditor({
|
|
7
17
|
value,
|
|
@@ -37,6 +47,7 @@ const CharacterBookEditor = memo(function CharacterBookEditor({
|
|
|
37
47
|
extensions: [
|
|
38
48
|
lineNumbers(),
|
|
39
49
|
json(),
|
|
50
|
+
characterBookJsonHighlight,
|
|
40
51
|
wrappingRef.current.of(lineWrapping ? EditorView.lineWrapping : []),
|
|
41
52
|
readOnlyRef.current.of(readOnly ? [EditorState.readOnly.of(true), EditorView.editable.of(false)] : []),
|
|
42
53
|
EditorView.updateListener.of((update) => {
|
|
@@ -35,6 +35,19 @@ const CharacterBookEditor = lazy(() => import('./CharacterBookEditor'));
|
|
|
35
35
|
|
|
36
36
|
type WorkshopTab = 'characters' | 'worldbooks' | 'skills';
|
|
37
37
|
|
|
38
|
+
function invalidJsonLine(value: string): number | null {
|
|
39
|
+
const trimmed = value.trim();
|
|
40
|
+
if (!trimmed) return null;
|
|
41
|
+
try {
|
|
42
|
+
JSON.parse(trimmed);
|
|
43
|
+
return null;
|
|
44
|
+
} catch (error) {
|
|
45
|
+
const position = /position (\d+)/i.exec(String(error))?.[1];
|
|
46
|
+
if (!position) return 1;
|
|
47
|
+
return trimmed.slice(0, Number(position)).split('\n').length;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
38
51
|
// 内置角色「酒馆老板」默认启用所有技能
|
|
39
52
|
const ALL_DEFAULT_SKILLS = [...ALL_BUILTIN_TOOL_NAMES];
|
|
40
53
|
|
|
@@ -274,13 +287,10 @@ function CharacterEditorModal({ npc, isNew, onClose, onSaved }: { npc: NpcCharac
|
|
|
274
287
|
addToast(t('toast.nameRequired'), 'error');
|
|
275
288
|
return;
|
|
276
289
|
}
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
addToast(t('toast.characterBookInvalidJson'), 'error');
|
|
282
|
-
return;
|
|
283
|
-
}
|
|
290
|
+
const characterBookInvalidLine = invalidJsonLine(characterBook);
|
|
291
|
+
if (characterBookInvalidLine != null) {
|
|
292
|
+
addToast(t('toast.characterBookInvalidJson', { line: characterBookInvalidLine }), 'error');
|
|
293
|
+
return;
|
|
284
294
|
}
|
|
285
295
|
// 清理空白/重复项:第一项作为主开场白,其余作为备用
|
|
286
296
|
const cleaned = greetings.map((g) => g.trim()).filter((g, i, arr) => g && arr.indexOf(g) === i);
|
|
@@ -316,11 +326,12 @@ function CharacterEditorModal({ npc, isNew, onClose, onSaved }: { npc: NpcCharac
|
|
|
316
326
|
return (
|
|
317
327
|
<Modal onClose={onClose} className="modal-editor editor-modal-with-tabs character-editor-modal">
|
|
318
328
|
<div className="modal-head">
|
|
319
|
-
<span
|
|
329
|
+
<span className="typo-dialog-title">{isNew ? t('workshop.newCharTitle') : t('workshop.editCharTitle')}</span>
|
|
320
330
|
<div className="raw-tabs">
|
|
321
|
-
<button className={`raw-tab ${editorTab === 'character' ? 'active' : ''}`} onClick={() => setEditorTab('character')}>{t('workshop.characterTab')}</button>
|
|
331
|
+
<button className={`raw-tab ${editorTab === 'character' ? 'active' : ''}`} onPointerDown={(event) => event.preventDefault()} onClick={() => setEditorTab('character')}>{t('workshop.characterTab')}</button>
|
|
322
332
|
<button
|
|
323
333
|
className={`raw-tab ${editorTab === 'knowledge' ? 'active' : ''}`}
|
|
334
|
+
onPointerDown={(event) => event.preventDefault()}
|
|
324
335
|
onClick={() => setEditorTab('knowledge')}
|
|
325
336
|
>
|
|
326
337
|
{t('workshop.knowledgeTab')}
|
|
@@ -337,8 +348,8 @@ function CharacterEditorModal({ npc, isNew, onClose, onSaved }: { npc: NpcCharac
|
|
|
337
348
|
<button className="icon-btn" onClick={onClose}><Icon name="x" /></button>
|
|
338
349
|
</div>
|
|
339
350
|
</div>
|
|
340
|
-
<div className=
|
|
341
|
-
{editorTab === 'character'
|
|
351
|
+
<div className="modal-body character-editor-body">
|
|
352
|
+
<div className={`character-editor-tab-panel ${editorTab === 'character' ? 'active' : ''}`}>
|
|
342
353
|
<div style={{ display: 'flex', gap: 14, alignItems: 'center' }}>
|
|
343
354
|
<Avatar name={name || t('chat.speakerChar')} colorOrdinal={npc.avatarColorOrdinal} imageUrl={avatarDataUrl} size="sm" />
|
|
344
355
|
<div style={{ display: 'flex', gap: 8 }}>
|
|
@@ -371,7 +382,7 @@ function CharacterEditorModal({ npc, isNew, onClose, onSaved }: { npc: NpcCharac
|
|
|
371
382
|
<textarea className="textarea grow-textarea" value={prompt} onChange={(e) => setPrompt(e.target.value)} placeholder={t('workshop.promptPh')} />
|
|
372
383
|
) : (
|
|
373
384
|
<div className="bubble preview-scroll wb-render-pane char-prompt-render-pane">
|
|
374
|
-
{prompt ? <Markdown text={prompt} /> : <span
|
|
385
|
+
{prompt ? <Markdown text={prompt} /> : <span className="typo-label text-muted">{t('workshop.promptPh')}</span>}
|
|
375
386
|
</div>
|
|
376
387
|
)}
|
|
377
388
|
</div>
|
|
@@ -391,7 +402,7 @@ function CharacterEditorModal({ npc, isNew, onClose, onSaved }: { npc: NpcCharac
|
|
|
391
402
|
/>
|
|
392
403
|
) : (
|
|
393
404
|
<div className="bubble preview-scroll wb-render-pane greeting-render-pane">
|
|
394
|
-
{g ? <Markdown text={g} /> : <span
|
|
405
|
+
{g ? <Markdown text={g} /> : <span className="typo-label text-muted">{t('workshop.greetingPh')}</span>}
|
|
395
406
|
</div>
|
|
396
407
|
)}
|
|
397
408
|
{!renderMarkdown && (
|
|
@@ -415,7 +426,7 @@ function CharacterEditorModal({ npc, isNew, onClose, onSaved }: { npc: NpcCharac
|
|
|
415
426
|
<div className="field" style={{ flexShrink: 0 }}>
|
|
416
427
|
<label>{t('workshop.enableSkills', { a: enabledToolNames.filter((s) => tools.some((t) => t.name === s)).length, b: tools.length })}</label>
|
|
417
428
|
{tools.length === 0 ? (
|
|
418
|
-
<div
|
|
429
|
+
<div className="typo-label text-muted">{t('workshop.noSkills')}</div>
|
|
419
430
|
) : (
|
|
420
431
|
<div className="skill-list-scroll">
|
|
421
432
|
{tools.map((t) => (
|
|
@@ -427,8 +438,8 @@ function CharacterEditorModal({ npc, isNew, onClose, onSaved }: { npc: NpcCharac
|
|
|
427
438
|
</div>
|
|
428
439
|
)}
|
|
429
440
|
</div>
|
|
430
|
-
|
|
431
|
-
{editorTab === 'knowledge'
|
|
441
|
+
</div>
|
|
442
|
+
<div className={`character-editor-tab-panel knowledge-tab ${editorTab === 'knowledge' ? 'active' : ''}`}>
|
|
432
443
|
<div className="field prompt-field character-book-field">
|
|
433
444
|
<label>{t('workshop.characterBookLabel')}</label>
|
|
434
445
|
<Suspense fallback={<div className="character-book-editor-loading" />}>
|
|
@@ -439,7 +450,7 @@ function CharacterEditorModal({ npc, isNew, onClose, onSaved }: { npc: NpcCharac
|
|
|
439
450
|
/>
|
|
440
451
|
</Suspense>
|
|
441
452
|
</div>
|
|
442
|
-
|
|
453
|
+
</div>
|
|
443
454
|
</div>
|
|
444
455
|
<div className="modal-foot">
|
|
445
456
|
{npc.isBuiltIn && (
|
|
@@ -448,7 +459,7 @@ function CharacterEditorModal({ npc, isNew, onClose, onSaved }: { npc: NpcCharac
|
|
|
448
459
|
</button>
|
|
449
460
|
)}
|
|
450
461
|
<button className="btn" onClick={onClose}>{t('common.cancel')}</button>
|
|
451
|
-
<button className="btn btn-primary" onClick={save}>{t('common.save')}</button>
|
|
462
|
+
<button className="btn btn-primary" type="button" onPointerDown={(event) => event.preventDefault()} onClick={save}>{t('common.save')}</button>
|
|
452
463
|
</div>
|
|
453
464
|
</Modal>
|
|
454
465
|
);
|
|
@@ -501,13 +512,10 @@ function WorldBookList({ books, onChanged }: {
|
|
|
501
512
|
addToast(t('toast.wbNameContentRequired'), 'error');
|
|
502
513
|
return;
|
|
503
514
|
}
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
addToast(t('toast.knowledgeInvalidJson'), 'error');
|
|
509
|
-
return;
|
|
510
|
-
}
|
|
515
|
+
const knowledgeInvalidLine = invalidJsonLine(knowledge);
|
|
516
|
+
if (knowledgeInvalidLine != null) {
|
|
517
|
+
addToast(t('toast.knowledgeInvalidJson', { line: knowledgeInvalidLine }), 'error');
|
|
518
|
+
return;
|
|
511
519
|
}
|
|
512
520
|
const data = {
|
|
513
521
|
name: name.trim(),
|
|
@@ -595,7 +603,7 @@ function WorldBookList({ books, onChanged }: {
|
|
|
595
603
|
return (
|
|
596
604
|
<div key={b.id} className="char-card wb-card fade-up">
|
|
597
605
|
<div className="cname">
|
|
598
|
-
<span
|
|
606
|
+
<span className="character-book-icon">📖</span>
|
|
599
607
|
<ScrollingCardName name={b.name} />
|
|
600
608
|
{isBuiltin && <span className="tag">{t('common.builtin')}</span>}
|
|
601
609
|
</div>
|
|
@@ -712,7 +720,7 @@ function WorldBookForm({
|
|
|
712
720
|
return (
|
|
713
721
|
<>
|
|
714
722
|
<div className="modal-head">
|
|
715
|
-
<span
|
|
723
|
+
<span className="typo-dialog-title">
|
|
716
724
|
{isEditing ? t('workshop.editWbTitle') : t('workshop.newWbTitle')}
|
|
717
725
|
</span>
|
|
718
726
|
<div className="raw-tabs">
|
|
@@ -755,7 +763,7 @@ function WorldBookForm({
|
|
|
755
763
|
<textarea className="textarea grow-textarea" value={content} onChange={(e) => setContent(e.target.value)} placeholder={t('workshop.wbContentPh')} />
|
|
756
764
|
) : (
|
|
757
765
|
<div className="bubble preview-scroll wb-render-pane">
|
|
758
|
-
{content ? <Markdown text={content} /> : <span
|
|
766
|
+
{content ? <Markdown text={content} /> : <span className="typo-label text-muted">{t('workshop.wbContentPh')}</span>}
|
|
759
767
|
</div>
|
|
760
768
|
)}
|
|
761
769
|
</div>
|
|
@@ -791,7 +799,7 @@ function WorldBookForm({
|
|
|
791
799
|
</button>
|
|
792
800
|
)}
|
|
793
801
|
<button className="btn" onClick={onClose}>{t('common.cancel')}</button>
|
|
794
|
-
<button className="btn btn-primary" onClick={() => onSave(name, content, characterBook)}>{t('common.save')}</button>
|
|
802
|
+
<button className="btn btn-primary" type="button" onPointerDown={(event) => event.preventDefault()} onClick={() => onSave(name, content, characterBook)}>{t('common.save')}</button>
|
|
795
803
|
</div>
|
|
796
804
|
</>
|
|
797
805
|
);
|
|
@@ -960,14 +968,14 @@ function SkillSortItem({
|
|
|
960
968
|
{...attributes}
|
|
961
969
|
{...listeners}
|
|
962
970
|
>
|
|
963
|
-
<div
|
|
971
|
+
<div className="tool-type-icon">{tool.isBuiltIn ? '🧰' : '⚙️'}</div>
|
|
964
972
|
<div className="l-main">
|
|
965
973
|
<div className="l-title">
|
|
966
974
|
<span className="skill-card-name">{tool.name}</span>
|
|
967
975
|
{tool.isBuiltIn && <span className="tag">{t('common.builtin')}</span>}
|
|
968
976
|
</div>
|
|
969
977
|
<div className="l-sub">{desc}</div>
|
|
970
|
-
{tool.isBuiltIn && impl !== 'native' && <div
|
|
978
|
+
{tool.isBuiltIn && impl !== 'native' && <div className="tool-implementation-type">{t('workshop.implType', { t: impl })}</div>}
|
|
971
979
|
</div>
|
|
972
980
|
<div className="skill-item-actions">
|
|
973
981
|
<button className="btn btn-sm" onClick={(e) => { e.stopPropagation(); onDetail(); }} title={t('workshop.viewDetails')}>
|
|
@@ -1063,7 +1071,7 @@ function SkillDetailModal({ tool, onClose }: { tool: McpTool; onClose: () => voi
|
|
|
1063
1071
|
return (
|
|
1064
1072
|
<Modal onClose={onClose}>
|
|
1065
1073
|
<div className="modal-head">
|
|
1066
|
-
<span
|
|
1074
|
+
<span className="typo-dialog-title">
|
|
1067
1075
|
{t('workshop.skillDetails')}
|
|
1068
1076
|
{tool.isBuiltIn && <span className="tag" style={{ marginLeft: 8 }}>{t('common.builtin')}</span>}
|
|
1069
1077
|
{!tool.isBuiltIn && execType !== 'native' && <span className="tag" style={{ marginLeft: 8 }}>{t('workshop.implType', { t: execType })}</span>}
|
|
@@ -1081,17 +1089,17 @@ function SkillDetailModal({ tool, onClose }: { tool: McpTool; onClose: () => voi
|
|
|
1081
1089
|
</div>
|
|
1082
1090
|
<div className="field">
|
|
1083
1091
|
<label>{t('workshop.skillParameters')}</label>
|
|
1084
|
-
{editable ? <textarea className="textarea mono skill-detail-textarea" value={parametersText} wrap="soft" spellCheck={false} onChange={(e) => setParametersText(e.target.value)} /> : parametersText ? <pre className="skill-detail-json">{parametersText}</pre> : <div className="skill-detail-text
|
|
1092
|
+
{editable ? <textarea className="textarea mono skill-detail-textarea" value={parametersText} wrap="soft" spellCheck={false} onChange={(e) => setParametersText(e.target.value)} /> : parametersText ? <pre className="skill-detail-json">{parametersText}</pre> : <div className="skill-detail-text text-muted">{t('workshop.skillNoParams')}</div>}
|
|
1085
1093
|
</div>
|
|
1086
1094
|
<div className="field">
|
|
1087
1095
|
<label>{t('workshop.skillExecution')}</label>
|
|
1088
|
-
{exec ? <pre className="skill-detail-json">{JSON.stringify(exec, null, 2)}</pre> : <div className="skill-detail-text
|
|
1096
|
+
{exec ? <pre className="skill-detail-json">{JSON.stringify(exec, null, 2)}</pre> : <div className="skill-detail-text text-muted">{t('workshop.skillNoExecution')}</div>}
|
|
1089
1097
|
</div>
|
|
1090
1098
|
<div className="field">
|
|
1091
1099
|
<label>{t('workshop.skillRawJson')}</label>
|
|
1092
1100
|
<pre className={`skill-detail-json${rawJson ? '' : ' err'}`}>{rawJson ?? schema.error}</pre>
|
|
1093
1101
|
</div>
|
|
1094
|
-
{error && <div className="field-hint
|
|
1102
|
+
{error && <div className="field-hint text-danger">{error}</div>}
|
|
1095
1103
|
</div>
|
|
1096
1104
|
<div className="modal-foot">
|
|
1097
1105
|
<button className="btn" onClick={onClose}>{t('common.cancel')}</button>
|
|
@@ -1465,7 +1465,7 @@ export function MessageMenu() {
|
|
|
1465
1465
|
<Icon name={a.icon} size={13} /> {a.label}
|
|
1466
1466
|
</button>
|
|
1467
1467
|
))}
|
|
1468
|
-
{actions.length === 0 && <div
|
|
1468
|
+
{actions.length === 0 && <div className="chat-no-actions">{t('chat.noActions')}</div>}
|
|
1469
1469
|
</PopoverMenu>
|
|
1470
1470
|
);
|
|
1471
1471
|
}
|
|
@@ -23,16 +23,16 @@ export function ConfirmationDialog() {
|
|
|
23
23
|
<div className="modal-root">
|
|
24
24
|
<div className="modal card confirm fade-up">
|
|
25
25
|
<div className="modal-head">
|
|
26
|
-
<span
|
|
26
|
+
<span className="typo-dialog-title dialog-title-inline dialog-title-compact">
|
|
27
27
|
<Icon name="settings" size={15} /> {isLimit ? pending.title : t('confirm.title')}
|
|
28
28
|
</span>
|
|
29
29
|
</div>
|
|
30
30
|
<div className="modal-body" style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
|
|
31
|
-
{!isLimit && <div
|
|
31
|
+
{!isLimit && <div className="confirmation-tool-name">{t('confirm.tool')}<span className="mono text-accent">{pending.toolName}</span></div>}
|
|
32
32
|
{!pending.inlineCode && <pre className="confirm-args mono">{argsText}</pre>}
|
|
33
33
|
<div className={isLimit ? undefined : 'confirm-reason'}>
|
|
34
34
|
{!isLimit && <div className="confirm-reason-title">{pending.title}</div>}
|
|
35
|
-
<div
|
|
35
|
+
<div className="typo-confirm-reason">
|
|
36
36
|
{pending.inlineCode && <code className="confirm-inline-code">{pending.inlineCode}</code>}
|
|
37
37
|
{pending.message || (isLimit ? '' : t('confirm.modify'))}
|
|
38
38
|
</div>
|
|
@@ -25,7 +25,7 @@ export function DeleteConfirmDialog({
|
|
|
25
25
|
return (
|
|
26
26
|
<Modal onClose={onCancel} size="compact">
|
|
27
27
|
<div className="modal-head">
|
|
28
|
-
<span
|
|
28
|
+
<span className="typo-dialog-title">{title}</span>
|
|
29
29
|
<button className="icon-btn" onClick={onCancel}><Icon name="x" /></button>
|
|
30
30
|
</div>
|
|
31
31
|
<div className="modal-body">
|
|
@@ -466,7 +466,7 @@ export function FileDisplayModal() {
|
|
|
466
466
|
>
|
|
467
467
|
<span className="display-modal-title">
|
|
468
468
|
<Icon name="file" size={15} />
|
|
469
|
-
<span className="mono
|
|
469
|
+
<span className="mono typo-body-sm text-secondary">{active.path}</span>
|
|
470
470
|
<span className="display-title-text">{title}</span>
|
|
471
471
|
</span>
|
|
472
472
|
<div className="display-head-actions">
|
|
@@ -50,7 +50,7 @@ export function GameplayExportModal({
|
|
|
50
50
|
return (
|
|
51
51
|
<Modal onClose={onClose} size="compact">
|
|
52
52
|
<div className="modal-head">
|
|
53
|
-
<span
|
|
53
|
+
<span className="typo-dialog-title dialog-title-inline">
|
|
54
54
|
<Icon name="share" size={15} /> {t('gameplay.exportTitle')}
|
|
55
55
|
</span>
|
|
56
56
|
<button className="icon-btn" onClick={onClose}><Icon name="x" /></button>
|
|
@@ -155,7 +155,7 @@ export function GameplayImportModal({
|
|
|
155
155
|
return (
|
|
156
156
|
<Modal onClose={onClose} size="compact">
|
|
157
157
|
<div className="modal-head">
|
|
158
|
-
<span
|
|
158
|
+
<span className="typo-dialog-title dialog-title-inline">
|
|
159
159
|
<Icon name="import" size={15} /> {t('gameplay.importTitle')}
|
|
160
160
|
</span>
|
|
161
161
|
<button className="icon-btn" onClick={onClose}><Icon name="x" /></button>
|
|
@@ -160,7 +160,7 @@ export function NewSessionMenu({ onClose, editingSession }: { onClose: () => voi
|
|
|
160
160
|
return (
|
|
161
161
|
<Modal onClose={onClose} className="new-session-modal">
|
|
162
162
|
<div className="modal-head">
|
|
163
|
-
<span
|
|
163
|
+
<span className="typo-dialog-title">{isEditing ? t('nav.editSession') : t('newSession.title')}</span>
|
|
164
164
|
<button className="icon-btn" onClick={onClose}><Icon name="x" /></button>
|
|
165
165
|
</div>
|
|
166
166
|
<div className="modal-body" style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
|
|
@@ -203,7 +203,7 @@ export function NewSessionMenu({ onClose, editingSession }: { onClose: () => voi
|
|
|
203
203
|
</SortableContext>
|
|
204
204
|
</DndContext>
|
|
205
205
|
{npcs.length === 0 ? (
|
|
206
|
-
<div
|
|
206
|
+
<div className="new-session-warning">
|
|
207
207
|
{t('newSession.noNpcWarn')}
|
|
208
208
|
</div>
|
|
209
209
|
) : (
|
|
@@ -323,7 +323,7 @@ export const SettingsView = memo(function SettingsView() {
|
|
|
323
323
|
{showResetConfirm && (
|
|
324
324
|
<Modal onClose={() => !resetting && setShowResetConfirm(false)} size="compact">
|
|
325
325
|
<div className="modal-head">
|
|
326
|
-
<span
|
|
326
|
+
<span className="typo-dialog-title">{t('settings.resetConfirmTitle')}</span>
|
|
327
327
|
<button className="icon-btn" disabled={resetting} onClick={() => setShowResetConfirm(false)}><Icon name="x" /></button>
|
|
328
328
|
</div>
|
|
329
329
|
<div className="modal-body">{t('settings.resetConfirm')}</div>
|
|
@@ -435,7 +435,7 @@ function ProviderManager({ providers, onChanged }: { providers: ApiProvider[]; o
|
|
|
435
435
|
<div className="set-section">
|
|
436
436
|
<h3>{t('settings.providerTitle')}</h3>
|
|
437
437
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
|
|
438
|
-
{providers.length === 0 && <div
|
|
438
|
+
{providers.length === 0 && <div className="typo-body-sm text-muted">{t('settings.noProvider')}</div>}
|
|
439
439
|
{providers.map((p) => {
|
|
440
440
|
const aliases = modelAliases(p);
|
|
441
441
|
const enabledModels = enabledModelIds(p);
|
|
@@ -457,7 +457,7 @@ function ProviderManager({ providers, onChanged }: { providers: ApiProvider[]; o
|
|
|
457
457
|
</div>
|
|
458
458
|
</div>
|
|
459
459
|
<div className="p-url">{p.baseUrl}</div>
|
|
460
|
-
<div
|
|
460
|
+
<div className="provider-inline-detail">
|
|
461
461
|
<b>{t('settings.models')}</b>
|
|
462
462
|
{visibleModels.length > 0
|
|
463
463
|
? visibleModels.slice(0, 12).map((model) => aliases[model]?.trim() || model).join(', ')
|
|
@@ -475,7 +475,7 @@ function ProviderManager({ providers, onChanged }: { providers: ApiProvider[]; o
|
|
|
475
475
|
{editing && (
|
|
476
476
|
<Modal onClose={() => setEditing(null)}>
|
|
477
477
|
<div className="modal-head">
|
|
478
|
-
<span
|
|
478
|
+
<span className="typo-dialog-title">{editing.id != null ? t('settings.editProvider') : t('settings.addProvider')}</span>
|
|
479
479
|
<button className="icon-btn" onClick={() => setEditing(null)}><Icon name="x" /></button>
|
|
480
480
|
</div>
|
|
481
481
|
<ProviderForm
|
|
@@ -527,13 +527,13 @@ export function Sidebar() {
|
|
|
527
527
|
{confirmDelete && (
|
|
528
528
|
<Modal onClose={() => setConfirmDelete(null)} size="compact">
|
|
529
529
|
<div className="modal-head">
|
|
530
|
-
<span
|
|
530
|
+
<span className="typo-dialog-title dialog-title-inline">
|
|
531
531
|
<Icon name="trash" size={15} /> {t('nav.deleteSession')}
|
|
532
532
|
</span>
|
|
533
533
|
<button className="icon-btn" onClick={() => setConfirmDelete(null)}><Icon name="x" /></button>
|
|
534
534
|
</div>
|
|
535
535
|
<div className="modal-body">
|
|
536
|
-
<div
|
|
536
|
+
<div className="sidebar-import-description">
|
|
537
537
|
{t('nav.deleteSessionConfirm', { title: confirmDelete.title })}
|
|
538
538
|
</div>
|
|
539
539
|
</div>
|
|
@@ -134,18 +134,18 @@ function StatCards({ data }: { data: StatsData }) {
|
|
|
134
134
|
{(() => {
|
|
135
135
|
const mostActive = [...data.npcStats].sort((a, b) => b.rounds - a.rounds)[0];
|
|
136
136
|
return (
|
|
137
|
-
<div
|
|
138
|
-
<div
|
|
137
|
+
<div className="stats-most-active">
|
|
138
|
+
<div className="stats-most-active-icon">🎭</div>
|
|
139
139
|
<div>
|
|
140
|
-
<div
|
|
141
|
-
<div
|
|
140
|
+
<div className="stats-most-active-name">{mostActive.npcName}</div>
|
|
141
|
+
<div className="stats-most-active-rounds">{t('stats.roundsCount', { n: mostActive.rounds })}</div>
|
|
142
142
|
</div>
|
|
143
143
|
</div>
|
|
144
144
|
);
|
|
145
145
|
})()}
|
|
146
146
|
</>
|
|
147
147
|
) : (
|
|
148
|
-
<div
|
|
148
|
+
<div className="stats-no-data">{t('stats.noData')}</div>
|
|
149
149
|
)}
|
|
150
150
|
</div>
|
|
151
151
|
|
|
@@ -93,7 +93,7 @@ export function VersionHistoryModal({ onClose }: { onClose: () => void }) {
|
|
|
93
93
|
return (
|
|
94
94
|
<Modal onClose={onClose} size="wide">
|
|
95
95
|
<div className="modal-head">
|
|
96
|
-
<span
|
|
96
|
+
<span className="typo-dialog-title">{t('changelog.title')}</span>
|
|
97
97
|
<button className="icon-btn" onClick={onClose} aria-label={t('common.close')}>
|
|
98
98
|
<Icon name="x" />
|
|
99
99
|
</button>
|
|
@@ -241,7 +241,7 @@ export function WorkspaceFileManagerModal({ sessionId, onClose }: { sessionId: n
|
|
|
241
241
|
<div className="ws-fm-delete-layer" role="dialog" aria-modal="true" aria-labelledby="ws-fm-delete-title">
|
|
242
242
|
<div className="ws-fm-delete-dialog card fade-up">
|
|
243
243
|
<div className="modal-head">
|
|
244
|
-
<span id="ws-fm-delete-title"
|
|
244
|
+
<span id="ws-fm-delete-title" className="typo-dialog-title">{t('header.fileManagerDeleteTitle')}</span>
|
|
245
245
|
<button className="icon-btn" onClick={() => setPendingDelete(null)} disabled={deleting} title={t('common.close')}>
|
|
246
246
|
<Icon name="x" />
|
|
247
247
|
</button>
|
package/src/core/i18n.ts
CHANGED
|
@@ -471,13 +471,13 @@ const MESSAGES: Record<LangKey, Record<string, string>> = {
|
|
|
471
471
|
'toast.importFailed': '导入失败: {msg}',
|
|
472
472
|
'toast.charDeleted': '已删除角色「{name}」',
|
|
473
473
|
'toast.nameRequired': '名称不能为空',
|
|
474
|
-
'toast.characterBookInvalidJson': '
|
|
474
|
+
'toast.characterBookInvalidJson': '角色卡知识库 JSON 格式无效(第 {line} 行),请修改后再保存',
|
|
475
475
|
'toast.charCreated': '已创建角色「{name}」',
|
|
476
476
|
'toast.charUpdated': '已更新角色「{name}」',
|
|
477
477
|
'toast.wbDraftImported': '已解析角色卡内的世界书「{name}」,请确认后保存',
|
|
478
478
|
'toast.wbNameRequired': '世界书名不能为空',
|
|
479
479
|
'toast.wbNameContentRequired': '世界书名不能为空,且内容与知识库至少填写一项',
|
|
480
|
-
'toast.knowledgeInvalidJson': '
|
|
480
|
+
'toast.knowledgeInvalidJson': '世界书知识库 JSON 格式无效(第 {line} 行),请修改后再保存',
|
|
481
481
|
'toast.wbImported': '已导入世界书「{name}」({total} 条知识,{enabled} 条启用),请确认后保存',
|
|
482
482
|
'toast.wbImportFailed': '世界书 JSON 导入失败: {msg}',
|
|
483
483
|
'toast.wbUpdated': '已更新世界书',
|
|
@@ -989,13 +989,13 @@ const MESSAGES: Record<LangKey, Record<string, string>> = {
|
|
|
989
989
|
'toast.importFailed': '匯入失敗: {msg}',
|
|
990
990
|
'toast.charDeleted': '已刪除角色「{name}」',
|
|
991
991
|
'toast.nameRequired': '名稱不能為空',
|
|
992
|
-
'toast.characterBookInvalidJson': 'Character Book
|
|
992
|
+
'toast.characterBookInvalidJson': 'Character Book JSON 格式無效(第 {line} 行),請修改後再儲存',
|
|
993
993
|
'toast.charCreated': '已建立角色「{name}」',
|
|
994
994
|
'toast.charUpdated': '已更新角色「{name}」',
|
|
995
995
|
'toast.wbDraftImported': '已解析角色卡內的世界書「{name}」,請確認後儲存',
|
|
996
996
|
'toast.wbNameRequired': '世界書名不能為空',
|
|
997
997
|
'toast.wbNameContentRequired': '世界書名不能為空,且內容與知識庫至少填寫一項',
|
|
998
|
-
'toast.knowledgeInvalidJson': '
|
|
998
|
+
'toast.knowledgeInvalidJson': '世界書知識庫 JSON 格式無效(第 {line} 行),請修改後再儲存',
|
|
999
999
|
'toast.wbImported': '已匯入世界書「{name}」({total} 條知識,{enabled} 條啟用),請確認後儲存',
|
|
1000
1000
|
'toast.wbImportFailed': '世界書 JSON 匯入失敗: {msg}',
|
|
1001
1001
|
'toast.wbUpdated': '已更新世界書',
|
|
@@ -1505,13 +1505,13 @@ const MESSAGES: Record<LangKey, Record<string, string>> = {
|
|
|
1505
1505
|
'toast.importFailed': 'Import failed: {msg}',
|
|
1506
1506
|
'toast.charDeleted': 'Deleted character "{name}"',
|
|
1507
1507
|
'toast.nameRequired': 'Name cannot be empty',
|
|
1508
|
-
'toast.characterBookInvalidJson': 'Character Book
|
|
1508
|
+
'toast.characterBookInvalidJson': 'Character Book JSON is invalid (line {line}). Correct it before saving.',
|
|
1509
1509
|
'toast.charCreated': 'Created character "{name}"',
|
|
1510
1510
|
'toast.charUpdated': 'Updated character "{name}"',
|
|
1511
1511
|
'toast.wbDraftImported': 'Parsed the Lorebook "{name}" from the character card — review and save',
|
|
1512
1512
|
'toast.wbNameRequired': 'Lorebook name cannot be empty',
|
|
1513
1513
|
'toast.wbNameContentRequired': 'Lorebook name is required, and content or knowledge base must be filled in',
|
|
1514
|
-
'toast.knowledgeInvalidJson': '
|
|
1514
|
+
'toast.knowledgeInvalidJson': 'Lorebook knowledge-base JSON is invalid (line {line}). Correct it before saving.',
|
|
1515
1515
|
'toast.wbImported': 'Imported world book "{name}" ({total} entries, {enabled} enabled) — review and save',
|
|
1516
1516
|
'toast.wbImportFailed': 'World book JSON import failed: {msg}',
|
|
1517
1517
|
'toast.wbUpdated': 'Lorebook updated',
|