@waterwx/dsh-novel-forge 0.1.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.
Files changed (50) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +180 -0
  3. package/cordis.patch.yml +13 -0
  4. package/lib/client.js +3924 -0
  5. package/lib/client.js.map +1 -0
  6. package/lib/index.js +3120 -0
  7. package/lib/index.js.map +1 -0
  8. package/lib/types/assets.d.ts +35 -0
  9. package/lib/types/assistant.d.ts +43 -0
  10. package/lib/types/bookshelf.d.ts +35 -0
  11. package/lib/types/client/api.d.ts +68 -0
  12. package/lib/types/client/docx.d.ts +15 -0
  13. package/lib/types/client/index.d.ts +14 -0
  14. package/lib/types/client/locales.d.ts +139 -0
  15. package/lib/types/client/mount.d.ts +9 -0
  16. package/lib/types/client/panel/AssetsTab.d.ts +7 -0
  17. package/lib/types/client/panel/AssistantTab.d.ts +7 -0
  18. package/lib/types/client/panel/BookshelfBar.d.ts +11 -0
  19. package/lib/types/client/panel/NovelPanel.d.ts +13 -0
  20. package/lib/types/client/panel/controller.d.ts +19 -0
  21. package/lib/types/client/panel/helpers.d.ts +8 -0
  22. package/lib/types/client/sidebar-entry.d.ts +13 -0
  23. package/lib/types/docx.d.ts +19 -0
  24. package/lib/types/engine.d.ts +95 -0
  25. package/lib/types/index.d.ts +55 -0
  26. package/lib/types/protocol.d.ts +521 -0
  27. package/lib/types/routes.d.ts +29 -0
  28. package/package.json +105 -0
  29. package/src/assets.ts +518 -0
  30. package/src/assistant.ts +547 -0
  31. package/src/bookshelf.ts +137 -0
  32. package/src/client/api.ts +254 -0
  33. package/src/client/css-modules.d.ts +8 -0
  34. package/src/client/docx.ts +69 -0
  35. package/src/client/index.ts +34 -0
  36. package/src/client/locales.ts +271 -0
  37. package/src/client/mount.tsx +97 -0
  38. package/src/client/panel/AssetsTab.tsx +341 -0
  39. package/src/client/panel/AssistantTab.tsx +188 -0
  40. package/src/client/panel/BookshelfBar.tsx +116 -0
  41. package/src/client/panel/NovelPanel.tsx +990 -0
  42. package/src/client/panel/controller.ts +45 -0
  43. package/src/client/panel/helpers.ts +17 -0
  44. package/src/client/panel/panel.module.css +894 -0
  45. package/src/client/sidebar-entry.ts +122 -0
  46. package/src/docx.ts +83 -0
  47. package/src/engine.ts +1019 -0
  48. package/src/index.ts +184 -0
  49. package/src/protocol.ts +539 -0
  50. package/src/routes.ts +955 -0
package/lib/client.js ADDED
@@ -0,0 +1,3924 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "@ryan/dsh-novel-forge",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
+ let react_dom_client = require("react-dom/client");
8
+ let react = require("react");
9
+ let react_jsx_runtime = require("react/jsx-runtime");
10
+ //#region src/protocol.ts
11
+ /**
12
+ * dsh-novel-forge — shared protocol between the host half (Node) and the
13
+ * browser half (web GUI). Route paths, request/response shapes, the project
14
+ * state file format, and the NDJSON generation stream frames all live here so
15
+ * both halves spell exactly one vocabulary.
16
+ */
17
+ /** The /api/dsh-novel-forge route family (same-origin, loopback-fenced). */
18
+ const NOVEL_API = {
19
+ status: "/api/dsh-novel-forge/status",
20
+ loadOutline: "/api/dsh-novel-forge/load-outline",
21
+ saveOutline: "/api/dsh-novel-forge/save-outline",
22
+ plan: "/api/dsh-novel-forge/plan",
23
+ volumes: "/api/dsh-novel-forge/volumes",
24
+ bible: "/api/dsh-novel-forge/bible",
25
+ assets: "/api/dsh-novel-forge/assets",
26
+ styleEngine: "/api/dsh-novel-forge/style-engine",
27
+ generate: "/api/dsh-novel-forge/generate",
28
+ review: "/api/dsh-novel-forge/review",
29
+ rewrite: "/api/dsh-novel-forge/rewrite",
30
+ polish: "/api/dsh-novel-forge/polish",
31
+ summary: "/api/dsh-novel-forge/summary",
32
+ foreshadow: "/api/dsh-novel-forge/foreshadow",
33
+ exportBook: "/api/dsh-novel-forge/export",
34
+ chapter: "/api/dsh-novel-forge/chapter",
35
+ assistant: "/api/dsh-novel-forge/assistant",
36
+ assistantHistory: "/api/dsh-novel-forge/assistant-history",
37
+ bookshelf: "/api/dsh-novel-forge/bookshelf",
38
+ config: "/api/dsh-novel-forge/config",
39
+ openFolder: "/api/dsh-novel-forge/open-folder"
40
+ };
41
+ //#endregion
42
+ //#region src/client/api.ts
43
+ /**
44
+ * Browser-side API client for the /api/dsh-novel-forge route family. Plain
45
+ * fetch, same origin; generation/rewrite/polish ride NDJSON streams read
46
+ * incrementally.
47
+ */
48
+ /** Error carrying the route's JSON error message. */
49
+ var NovelApiError = class extends Error {
50
+ constructor(message) {
51
+ super(message);
52
+ this.name = "NovelApiError";
53
+ }
54
+ };
55
+ /** Parse a JSON response or throw a NovelApiError. */
56
+ async function readJson(response) {
57
+ let body;
58
+ try {
59
+ body = await response.json();
60
+ } catch {
61
+ throw new NovelApiError(`HTTP ${response.status}: invalid JSON response`);
62
+ }
63
+ if (!response.ok) throw new NovelApiError(typeof body === "object" && body !== null && typeof body.error === "string" ? body.error : `HTTP ${response.status}`);
64
+ return body;
65
+ }
66
+ /** POST JSON, return parsed JSON. */
67
+ async function postJson(path, payload) {
68
+ return readJson(await fetch(path, {
69
+ method: "POST",
70
+ headers: { "content-type": "application/json" },
71
+ body: JSON.stringify(payload)
72
+ }));
73
+ }
74
+ /** The browser half's only data entry point. */
75
+ var NovelApi = class {
76
+ async status() {
77
+ return readJson(await fetch(NOVEL_API.status));
78
+ }
79
+ async loadOutline(path, text) {
80
+ return postJson(NOVEL_API.loadOutline, {
81
+ path,
82
+ text
83
+ });
84
+ }
85
+ async saveOutline(text) {
86
+ return postJson(NOVEL_API.saveOutline, { text });
87
+ }
88
+ async plan(outline, chapterCount, volume) {
89
+ return postJson(NOVEL_API.plan, {
90
+ outline,
91
+ chapterCount,
92
+ volume
93
+ });
94
+ }
95
+ async volumes(outline) {
96
+ return postJson(NOVEL_API.volumes, { outline });
97
+ }
98
+ async bible(outline) {
99
+ return postJson(NOVEL_API.bible, { outline });
100
+ }
101
+ async review(chapterNo) {
102
+ return postJson(NOVEL_API.review, { chapterNo });
103
+ }
104
+ async summarize(chapterNo) {
105
+ return postJson(NOVEL_API.summary, { chapterNo });
106
+ }
107
+ async foreshadow(req) {
108
+ return postJson(NOVEL_API.foreshadow, req);
109
+ }
110
+ async exportBook(format) {
111
+ return postJson(NOVEL_API.exportBook, { format });
112
+ }
113
+ async chapter(no) {
114
+ return readJson(await fetch(`${NOVEL_API.chapter}?no=${no}`));
115
+ }
116
+ async patchConfig(patch) {
117
+ return postJson(NOVEL_API.config, patch);
118
+ }
119
+ async openFolder() {
120
+ await fetch(NOVEL_API.openFolder, {
121
+ method: "POST",
122
+ headers: { "content-type": "application/json" },
123
+ body: "{}"
124
+ });
125
+ }
126
+ /** 书架快照。 */
127
+ async bookshelf() {
128
+ return readJson(await fetch(NOVEL_API.bookshelf));
129
+ }
130
+ /** 新建书并激活。 */
131
+ async bookCreate(bookName, outputDir) {
132
+ return postJson(NOVEL_API.bookshelf, {
133
+ bookName,
134
+ outputDir
135
+ });
136
+ }
137
+ /** 切换当前书。 */
138
+ async bookActivate(id) {
139
+ return postJson("/api/dsh-novel-forge/bookshelf/activate", { id });
140
+ }
141
+ /** 移除书架条目。 */
142
+ async bookRemove(id) {
143
+ return postJson("/api/dsh-novel-forge/bookshelf/remove", { id });
144
+ }
145
+ /** Get project writing assets + built-in libraries. */
146
+ async assets() {
147
+ return readJson(await fetch(NOVEL_API.assets));
148
+ }
149
+ /** Patch project writing assets. */
150
+ async patchAssets(patch) {
151
+ return postJson(NOVEL_API.assets, patch);
152
+ }
153
+ /** Extract a style asset from sample text. */
154
+ async styleEngine(req) {
155
+ return postJson(NOVEL_API.styleEngine, req);
156
+ }
157
+ /**
158
+ * Consume an NDJSON job stream (generate / rewrite / polish).
159
+ * @param path - the route to POST to.
160
+ * @param payload - the JSON body.
161
+ * @param onFrame - receives every frame as it lands.
162
+ */
163
+ async streamJob(path, payload, onFrame) {
164
+ const response = await fetch(path, {
165
+ method: "POST",
166
+ headers: { "content-type": "application/json" },
167
+ body: JSON.stringify(payload)
168
+ });
169
+ if (!response.ok) {
170
+ await readJson(response);
171
+ return;
172
+ }
173
+ if (response.body === null) throw new NovelApiError("job: no response body");
174
+ const reader = response.body.getReader();
175
+ const decoder = new TextDecoder();
176
+ let buffer = "";
177
+ for (;;) {
178
+ const { done, value } = await reader.read();
179
+ if (done) break;
180
+ buffer += decoder.decode(value, { stream: true });
181
+ const lines = buffer.split("\n");
182
+ buffer = lines.pop() ?? "";
183
+ for (const line of lines) {
184
+ if (line.trim() === "") continue;
185
+ let frame;
186
+ try {
187
+ frame = JSON.parse(line);
188
+ } catch {
189
+ continue;
190
+ }
191
+ onFrame(frame);
192
+ if (frame.type === "error") throw new NovelApiError(frame.message);
193
+ }
194
+ }
195
+ }
196
+ /** Generate one chapter. */
197
+ async generate(chapterNo, skipReview, onFrame) {
198
+ await this.streamJob(NOVEL_API.generate, {
199
+ chapterNo,
200
+ skipReview
201
+ }, onFrame);
202
+ }
203
+ /** Rewrite one chapter (whole-chapter, or local when `target` is given). */
204
+ async rewrite(chapterNo, instructions, target, onFrame) {
205
+ await this.streamJob(NOVEL_API.rewrite, {
206
+ chapterNo,
207
+ instructions,
208
+ target
209
+ }, onFrame);
210
+ }
211
+ /** Polish (de-AI-ify) one chapter. */
212
+ async polish(chapterNo, onFrame) {
213
+ await this.streamJob(NOVEL_API.polish, { chapterNo }, onFrame);
214
+ }
215
+ /** Run one assistant turn (NDJSON stream). */
216
+ async assistant(message, onFrame) {
217
+ const response = await fetch(NOVEL_API.assistant, {
218
+ method: "POST",
219
+ headers: { "content-type": "application/json" },
220
+ body: JSON.stringify({ message })
221
+ });
222
+ if (!response.ok) {
223
+ await readJson(response);
224
+ return;
225
+ }
226
+ if (response.body === null) throw new NovelApiError("assistant: no response body");
227
+ const reader = response.body.getReader();
228
+ const decoder = new TextDecoder();
229
+ let buffer = "";
230
+ for (;;) {
231
+ const { done, value } = await reader.read();
232
+ if (done) break;
233
+ buffer += decoder.decode(value, { stream: true });
234
+ const lines = buffer.split("\n");
235
+ buffer = lines.pop() ?? "";
236
+ for (const line of lines) {
237
+ if (line.trim() === "") continue;
238
+ let frame;
239
+ try {
240
+ frame = JSON.parse(line);
241
+ } catch {
242
+ continue;
243
+ }
244
+ onFrame(frame);
245
+ if (frame.type === "error") throw new NovelApiError(frame.message);
246
+ }
247
+ }
248
+ }
249
+ /** Load the persisted assistant conversation. */
250
+ async assistantHistory() {
251
+ return (await readJson(await fetch(NOVEL_API.assistantHistory))).messages;
252
+ }
253
+ };
254
+ //#endregion
255
+ //#region src/client/locales.ts
256
+ /**
257
+ * dsh-novel-forge — locale dictionaries (zh / en).
258
+ */
259
+ /** zh dictionary. */
260
+ const zh$1 = {
261
+ "entry.label": "小说工坊",
262
+ "entry.tooltip": "AI 编译小说工作台:大纲 → 设定圣经 → 卷计划 → 章节计划 → 逐章生成+审稿",
263
+ "panel.title": "小说工坊",
264
+ "common.close": "关闭",
265
+ "common.loading": "加载中…",
266
+ "common.save": "保存",
267
+ "common.error": "错误",
268
+ "common.success": "成功",
269
+ "common.generating": "生成中…",
270
+ "common.chars": "字",
271
+ "tab.workflow": "工作流",
272
+ "tab.overview": "大纲",
273
+ "tab.plan": "章节",
274
+ "tab.bible": "设定库",
275
+ "tab.foreshadow": "伏笔",
276
+ "tab.assistant": "AI 助手",
277
+ "tab.settings": "设置",
278
+ "workflow.title": "创作工作流",
279
+ "workflow.step1": "① 加载大纲",
280
+ "workflow.step2": "② 提炼设定圣经",
281
+ "workflow.step3": "③ 规划卷",
282
+ "workflow.step4": "④ 生成章节计划",
283
+ "workflow.step5": "⑤ 逐章写作 + AI 审稿",
284
+ "workflow.step6": "⑥ 润色 / 导出",
285
+ "workflow.loadOutline": "读取大纲",
286
+ "workflow.genBible": "提炼设定圣经",
287
+ "workflow.genVolumes": "生成卷计划",
288
+ "workflow.genPlan": "生成章节计划",
289
+ "workflow.done": "已完成",
290
+ "workflow.todo": "待办",
291
+ "workflow.bibleDone": "设定圣经已生成({n} 条规则 / {c} 个角色 / {r} 条红线)",
292
+ "workflow.volumesDone": "卷计划已生成({n} 卷)",
293
+ "workflow.planDone": "章节计划已生成({n} 章)",
294
+ "workflow.progress": "进度:大纲 ✓ · 设定 {bible} · 卷 {volumes} · 计划 {plan} · 已完成 {done}/{total} 章",
295
+ "overview.loadDocx": "从 docx 读取大纲",
296
+ "overview.loadDocxDefault": "读取默认大纲",
297
+ "overview.loadingOutline": "正在解析 docx…",
298
+ "overview.outlineHint": "大纲文本(可编辑)",
299
+ "overview.outlineChars": "大纲字数",
300
+ "overview.saveOutline": "保存大纲",
301
+ "overview.saved": "大纲已保存",
302
+ "overview.bookName": "书名",
303
+ "overview.loadCustom": "指定 docx 路径",
304
+ "overview.loadCustomHint": "绝对路径,留空使用默认",
305
+ "plan.generate": "生成章节计划",
306
+ "plan.generateHint": "章节数量",
307
+ "plan.count": "章",
308
+ "plan.empty": "暂无章节计划,请先生成",
309
+ "plan.chapter": "章",
310
+ "plan.pending": "待生成",
311
+ "plan.generating": "生成中",
312
+ "plan.written": "待审稿",
313
+ "plan.reviewing": "审稿中",
314
+ "plan.approved": "已通过",
315
+ "plan.rejected": "待修订",
316
+ "plan.error": "失败",
317
+ "plan.write": "生成本章",
318
+ "plan.rewrite": "修订",
319
+ "plan.review": "审稿",
320
+ "plan.polish": "去AI味",
321
+ "plan.writeAll": "批量生成全部",
322
+ "plan.writeAllPending": "批量生成剩余",
323
+ "plan.generated": "已生成",
324
+ "plan.progress": "进度",
325
+ "plan.beats": "剧情要点",
326
+ "plan.reviewReport": "审稿报告",
327
+ "plan.reviewScore": "评分",
328
+ "plan.reviewVerdict": "总评",
329
+ "plan.reviewIssues": "问题清单",
330
+ "plan.reviewPass": "通过",
331
+ "plan.reviewFail": "未通过",
332
+ "plan.approve": "手动通过",
333
+ "plan.summary": "章节摘要",
334
+ "plan.volumes": "卷",
335
+ "plan.noVolume": "未分卷",
336
+ "bible.title": "设定圣经",
337
+ "bible.gen": "AI 提炼设定圣经",
338
+ "bible.genre": "题材基调",
339
+ "bible.worldRules": "世界规则",
340
+ "bible.characters": "角色卡",
341
+ "bible.redLines": "写作红线",
342
+ "bible.style": "风格要求",
343
+ "bible.none": "尚未生成设定圣经。生成后写作会严格遵守人设与金手指规则,审稿也会按红线检查。",
344
+ "foreshadow.title": "伏笔管理",
345
+ "foreshadow.suggest": "AI 建议伏笔",
346
+ "foreshadow.none": "暂无伏笔",
347
+ "foreshadow.status": "状态",
348
+ "foreshadow.planned": "计划中",
349
+ "foreshadow.planted": "已埋设",
350
+ "foreshadow.progressing": "推进中",
351
+ "foreshadow.resolved": "已回收",
352
+ "foreshadow.abandoned": "已放弃",
353
+ "foreshadow.target": "预计回收",
354
+ "foreshadow.plantedAt": "埋设于",
355
+ "foreshadow.setPlanted": "标记已埋设",
356
+ "foreshadow.setResolved": "标记已回收",
357
+ "settings.title": "设置",
358
+ "settings.outlinePath": "默认大纲路径",
359
+ "settings.outputDir": "输出目录",
360
+ "settings.provider": "模型提供商",
361
+ "settings.model": "模型",
362
+ "settings.chapterChars": "每章目标字数",
363
+ "settings.maxTokens": "单章最大输出 tokens",
364
+ "settings.reviewPassScore": "审稿通过分数(0-100)",
365
+ "settings.autoReview": "生成后自动审稿",
366
+ "settings.save": "保存设置",
367
+ "settings.saved": "设置已保存",
368
+ "settings.openFolder": "打开输出文件夹",
369
+ "settings.export": "导出",
370
+ "settings.exportTxt": "导出 TXT",
371
+ "settings.exportMd": "导出 Markdown",
372
+ "settings.exported": "已导出:{file}({chars} 字,{chapters} 章)",
373
+ "progress.generating": "正在生成第 {no} 章《{title}》…",
374
+ "progress.done": "第 {no} 章完成({chars} 字)→ {file}",
375
+ "progress.reviewed": "第 {no} 章审稿:{score} 分 — {verdict}",
376
+ "progress.error": "第 {no} 章失败:{message}",
377
+ "progress.rewriting": "正在修订第 {no} 章…",
378
+ "progress.polishing": "正在润色第 {no} 章…",
379
+ "progress.empty": "生成/审稿进度将显示在这里",
380
+ "assistant.hint": "和 AI 编辑讨论剧情、人设、伏笔;达成一致后可让它直接修改大纲、设定圣经、章节内容。",
381
+ "assistant.placeholder": "例如:我想让第 2 章结尾加一个悬念——墟境里传来爷爷的声音…",
382
+ "assistant.send": "发送",
383
+ "assistant.toolStart": "⚙ 执行操作:{name}…",
384
+ "assistant.toolDone": "✓ {name} 完成:{detail}",
385
+ "assistant.toolError": "✗ {name} 失败:{detail}",
386
+ "assistant.empty": "还没有对话。和 AI 编辑聊聊剧情吧。",
387
+ "status.projectNone": "输出目录中还没有项目。请先加载大纲。",
388
+ "status.files": "已生成文件",
389
+ "api.error": "请求失败"
390
+ };
391
+ /** en dictionary (fallback). */
392
+ const en = {
393
+ "entry.label": "Novel Forge",
394
+ "entry.tooltip": "AI novel workbench: outline → bible → volumes → plan → write + review",
395
+ "panel.title": "Novel Forge",
396
+ "common.close": "Close",
397
+ "common.loading": "Loading…",
398
+ "common.save": "Save",
399
+ "common.error": "Error",
400
+ "common.success": "Success",
401
+ "common.generating": "Generating…",
402
+ "common.chars": " chars",
403
+ "tab.workflow": "Workflow",
404
+ "tab.overview": "Outline",
405
+ "tab.plan": "Chapters",
406
+ "tab.bible": "Bible",
407
+ "tab.foreshadow": "Foreshadow",
408
+ "tab.settings": "Settings",
409
+ "workflow.title": "Writing workflow",
410
+ "workflow.step1": "① Load outline",
411
+ "workflow.step2": "② Extract story bible",
412
+ "workflow.step3": "③ Plan volumes",
413
+ "workflow.step4": "④ Plan chapters",
414
+ "workflow.step5": "⑤ Write + AI review",
415
+ "workflow.step6": "⑥ Polish / export",
416
+ "workflow.loadOutline": "Load outline",
417
+ "workflow.genBible": "Extract bible",
418
+ "workflow.genVolumes": "Plan volumes",
419
+ "workflow.genPlan": "Plan chapters",
420
+ "workflow.done": "done",
421
+ "workflow.todo": "todo",
422
+ "workflow.bibleDone": "Bible ready ({n} rules / {c} characters / {r} red lines)",
423
+ "workflow.volumesDone": "Volumes ready ({n})",
424
+ "workflow.planDone": "Plan ready ({n} chapters)",
425
+ "workflow.progress": "Outline ✓ · bible {bible} · volumes {volumes} · plan {plan} · {done}/{total} chapters",
426
+ "overview.loadDocx": "Load outline from docx",
427
+ "overview.loadDocxDefault": "Load default outline",
428
+ "overview.loadingOutline": "Parsing docx…",
429
+ "overview.outlineHint": "Outline text (editable)",
430
+ "overview.outlineChars": "Outline length",
431
+ "overview.saveOutline": "Save outline",
432
+ "overview.saved": "Outline saved",
433
+ "overview.bookName": "Book",
434
+ "overview.loadCustom": "Custom docx path",
435
+ "overview.loadCustomHint": "Absolute path; empty = default",
436
+ "plan.generate": "Plan chapters",
437
+ "plan.generateHint": "Chapter count",
438
+ "plan.count": " chapters",
439
+ "plan.empty": "No plan yet — generate one first",
440
+ "plan.chapter": "Ch.",
441
+ "plan.pending": "pending",
442
+ "plan.generating": "writing",
443
+ "plan.written": "to review",
444
+ "plan.reviewing": "reviewing",
445
+ "plan.approved": "approved",
446
+ "plan.rejected": "to revise",
447
+ "plan.error": "failed",
448
+ "plan.write": "Write",
449
+ "plan.rewrite": "Revise",
450
+ "plan.review": "Review",
451
+ "plan.polish": "De-AI",
452
+ "plan.writeAll": "Write all",
453
+ "plan.writeAllPending": "Write remaining",
454
+ "plan.generated": "generated",
455
+ "plan.progress": "progress",
456
+ "plan.beats": "Beats",
457
+ "plan.reviewReport": "Review report",
458
+ "plan.reviewScore": "Score",
459
+ "plan.reviewVerdict": "Verdict",
460
+ "plan.reviewIssues": "Issues",
461
+ "plan.reviewPass": "Passed",
462
+ "plan.reviewFail": "Failed",
463
+ "plan.approve": "Approve",
464
+ "plan.summary": "Summary",
465
+ "plan.volumes": "Volumes",
466
+ "plan.noVolume": "No volume",
467
+ "bible.title": "Story bible",
468
+ "bible.gen": "Extract bible with AI",
469
+ "bible.genre": "Genre",
470
+ "bible.worldRules": "World rules",
471
+ "bible.characters": "Characters",
472
+ "bible.redLines": "Red lines",
473
+ "bible.style": "Style",
474
+ "bible.none": "No bible yet. Generation and review follow it strictly once extracted.",
475
+ "foreshadow.title": "Foreshadowing",
476
+ "foreshadow.suggest": "Suggest with AI",
477
+ "foreshadow.none": "No foreshadows",
478
+ "foreshadow.status": "Status",
479
+ "foreshadow.planned": "planned",
480
+ "foreshadow.planted": "planted",
481
+ "foreshadow.progressing": "progressing",
482
+ "foreshadow.resolved": "resolved",
483
+ "foreshadow.abandoned": "abandoned",
484
+ "foreshadow.target": "target",
485
+ "foreshadow.plantedAt": "planted at",
486
+ "foreshadow.setPlanted": "Mark planted",
487
+ "foreshadow.setResolved": "Mark resolved",
488
+ "settings.title": "Settings",
489
+ "settings.outlinePath": "Default outline path",
490
+ "settings.outputDir": "Output directory",
491
+ "settings.provider": "Provider",
492
+ "settings.model": "Model",
493
+ "settings.chapterChars": "Chars per chapter",
494
+ "settings.maxTokens": "Max output tokens",
495
+ "settings.reviewPassScore": "Review pass score (0-100)",
496
+ "settings.autoReview": "Auto-review after writing",
497
+ "settings.save": "Save settings",
498
+ "settings.saved": "Settings saved",
499
+ "settings.openFolder": "Open output folder",
500
+ "settings.export": "Export",
501
+ "settings.exportTxt": "Export TXT",
502
+ "settings.exportMd": "Export Markdown",
503
+ "settings.exported": "Exported: {file} ({chars} chars, {chapters} chapters)",
504
+ "progress.generating": "Writing chapter {no} “{title}”…",
505
+ "progress.done": "Chapter {no} done ({chars} chars) → {file}",
506
+ "progress.reviewed": "Chapter {no} review: {score} — {verdict}",
507
+ "progress.error": "Chapter {no} failed: {message}",
508
+ "progress.rewriting": "Revising chapter {no}…",
509
+ "progress.polishing": "Polishing chapter {no}…",
510
+ "progress.empty": "Generation/review progress appears here",
511
+ "assistant.hint": "Discuss plot, characters, foreshadowing with the AI editor; once agreed, let it edit the outline, bible, or chapters directly.",
512
+ "assistant.placeholder": "e.g. Add a hook at the end of chapter 2…",
513
+ "assistant.send": "Send",
514
+ "assistant.toolStart": "⚙ Running {name}…",
515
+ "assistant.toolDone": "✓ {name} done: {detail}",
516
+ "assistant.toolError": "✗ {name} failed: {detail}",
517
+ "assistant.empty": "No conversation yet. Chat with the AI editor.",
518
+ "status.projectNone": "No project in the output directory yet. Load an outline first.",
519
+ "status.files": "Generated files",
520
+ "api.error": "Request failed"
521
+ };
522
+ //#endregion
523
+ //#region src/client/panel/helpers.ts
524
+ /**
525
+ * Tiny translation helper for the panel: reads the zh dict with the en dict
526
+ * as fallback (the family plugins use a full locale registry; the panel keeps
527
+ * a dependency-free helper so the client bundle stays self-contained).
528
+ */
529
+ /** Translate one key with optional {placeholder} substitution. */
530
+ function tt(key, params) {
531
+ let text = zh$1[key] ?? en[key] ?? key;
532
+ if (params !== void 0) for (const [name, value] of Object.entries(params)) text = text.replaceAll(`{${name}}`, String(value));
533
+ return text;
534
+ }
535
+ //#endregion
536
+ //#region \0dsh-css:C:\Users\Ryan\Desktop\ai xiaoshuo\src\client\panel\panel.module.css.mjs
537
+ const css = "._8EKcRG_entry{width:100%;color:var(--dsw-alias-label-primary,#1f1f23);cursor:pointer;text-align:left;background:0 0;border:none;border-radius:6px;align-items:center;gap:8px;padding:8px 12px;font-size:13px;transition:background .15s;display:flex}body[data-ds-dark-theme] ._8EKcRG_entry{color:var(--dsw-alias-label-primary,#ececf1)}._8EKcRG_entry:hover{background:var(--dsw-alias-interactive-bg-hover,#7f7f7f1f)}._8EKcRG_entry[data-active]{background:var(--dsw-alias-interactive-bg-active,#7f7f7f33)}._8EKcRG_entryIcon{flex-shrink:0;justify-content:center;align-items:center;display:inline-flex}._8EKcRG_entryLabel{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}[data-pane=conversation]{position:relative}._8EKcRG_view{z-index:60;color-scheme:light;--nf-bg:#fafafc;--nf-bg-raise:#fff;--nf-bg-inset:#f1f1f5;--nf-border:#0000001a;--nf-border-strong:#00000038;--nf-text:#1f1f23;--nf-text-2:#5b5b66;--nf-text-3:#8a8a94;--nf-accent:#4d6bfe;--nf-accent-hover:#3d5bf0;--nf-accent-soft:#4d6bfe1f;--nf-accent-fg:#fff;--nf-hover:#0000000f;--nf-success:#16a34a;--nf-error:#e5484d;--nf-warn:#d97706;--nf-info:#7c5cf6;--nf-shadow:0 1px 2px #1018280d, 0 4px 14px #1018280f;--nf-shadow-lg:0 4px 10px #1018280f, 0 12px 28px #1018281a;background:var(--nf-bg);color:var(--nf-text);display:none;position:absolute;inset:0;overflow:auto}body[data-ds-dark-theme] ._8EKcRG_view{color-scheme:dark;--nf-bg:#121216;--nf-bg-raise:#1a1a20;--nf-bg-inset:#232329;--nf-border:#ffffff1a;--nf-border-strong:#ffffff3d;--nf-text:#ececf1;--nf-text-2:#b0b0ba;--nf-text-3:#7d7d88;--nf-accent:#5b7cff;--nf-accent-hover:#7390ff;--nf-accent-soft:#5b7cff2e;--nf-accent-fg:#fff;--nf-hover:#ffffff14;--nf-success:#4ade80;--nf-error:#f87171;--nf-warn:#fbbf24;--nf-info:#a78bfa;--nf-shadow:0 1px 2px #0006, 0 4px 14px #00000059;--nf-shadow-lg:0 4px 10px #0006, 0 12px 28px #00000080}html[data-dsh-novelforge-active]:not([data-dsh-taskboard-active]):not([data-dsh-ssh-active]) ._8EKcRG_view{display:block}._8EKcRG_panel{flex-direction:column;min-width:0;height:100%;font-size:14px;line-height:1.6;display:flex}._8EKcRG_panelHeader{border-bottom:1px solid var(--nf-border);background:var(--nf-bg-raise);z-index:5;flex-shrink:0;justify-content:space-between;align-items:center;padding:12px 18px;display:flex;position:sticky;top:0}._8EKcRG_panelTitle{letter-spacing:.2px;align-items:center;gap:8px;margin:0;font-size:16px;font-weight:700;display:flex}._8EKcRG_panelTitle:before{content:\"\";background:linear-gradient(180deg, var(--nf-accent), var(--nf-info));border-radius:2px;width:4px;height:16px}._8EKcRG_iconButton{cursor:pointer;color:var(--nf-text-2);background:0 0;border:none;border-radius:8px;padding:4px 9px;font-size:16px;transition:background .15s,color .15s}._8EKcRG_iconButton:hover{background:var(--nf-hover);color:var(--nf-text)}._8EKcRG_tabBar{border-bottom:1px solid var(--nf-border);background:var(--nf-bg-raise);z-index:4;flex-shrink:0;gap:2px;padding:8px 14px 0;display:flex;position:sticky;top:45px}._8EKcRG_tab{cursor:pointer;color:var(--nf-text-2);background:0 0;border:none;border-radius:8px 8px 0 0;padding:7px 13px;font-size:13.5px;font-weight:500;transition:color .15s,background .15s;position:relative}._8EKcRG_tab:hover{color:var(--nf-text);background:var(--nf-hover)}._8EKcRG_tab[data-active]{color:var(--nf-accent);font-weight:600}._8EKcRG_tab[data-active]:after{content:\"\";background:var(--nf-accent);border-radius:2px 2px 0 0;height:2.5px;position:absolute;bottom:-1px;left:10px;right:10px}._8EKcRG_panelContent{flex-direction:column;flex:1;gap:14px;padding:16px 18px 20px;display:flex;overflow:auto}._8EKcRG_card{border:1px solid var(--nf-border);background:var(--nf-bg-raise);box-shadow:var(--nf-shadow);border-radius:12px;flex-direction:column;gap:10px;padding:14px 16px;transition:border-color .2s,box-shadow .2s;display:flex}._8EKcRG_cardTitle{color:var(--nf-text);align-items:center;gap:8px;margin:0;font-size:14px;font-weight:650;display:flex}._8EKcRG_button{border:1px solid var(--nf-border-strong);background:var(--nf-bg-inset);color:var(--nf-text);cursor:pointer;white-space:nowrap;border-radius:8px;padding:6px 14px;font-size:13px;font-weight:500;transition:background .15s,border-color .15s,transform .1s,box-shadow .15s}._8EKcRG_button:hover:not([disabled]){background:var(--nf-hover);border-color:var(--nf-border-strong)}._8EKcRG_button:active:not([disabled]){transform:translateY(1px)}._8EKcRG_button[disabled]{opacity:.45;cursor:not-allowed}._8EKcRG_buttonPrimary{border-color:var(--nf-accent);background:linear-gradient(180deg, var(--nf-accent), var(--nf-accent-hover));color:var(--nf-accent-fg);box-shadow:0 1px 3px #4d6bfe40}._8EKcRG_buttonPrimary:hover:not([disabled]){background:linear-gradient(180deg, var(--nf-accent-hover), var(--nf-accent));border-color:var(--nf-accent-hover)}._8EKcRG_buttonDanger{border-color:var(--nf-error);color:var(--nf-error)}._8EKcRG_buttonDanger:hover:not([disabled]){background:#e5484d1a}._8EKcRG_buttonSmall{border-radius:6px;padding:3px 10px;font-size:12px}._8EKcRG_field{flex-direction:column;gap:5px;display:flex}._8EKcRG_fieldLabel{color:var(--nf-text-2);font-size:12px;font-weight:500}._8EKcRG_input{border:1px solid var(--nf-border-strong);background:var(--nf-bg-inset);color:var(--nf-text);box-sizing:border-box;border-radius:8px;width:100%;padding:7px 11px;font-size:13px;transition:border-color .15s,box-shadow .15s}._8EKcRG_input:focus,._8EKcRG_textarea:focus{border-color:var(--nf-accent);box-shadow:0 0 0 3px var(--nf-accent-soft);outline:none}._8EKcRG_input::placeholder,._8EKcRG_textarea::placeholder{color:var(--nf-text-3)}._8EKcRG_textarea{border:1px solid var(--nf-border-strong);background:var(--nf-bg-inset);color:var(--nf-text);box-sizing:border-box;resize:vertical;border-radius:8px;width:100%;min-height:200px;padding:9px 11px;font-family:inherit;font-size:13px;line-height:1.7;transition:border-color .15s,box-shadow .15s}._8EKcRG_row{flex-wrap:wrap;align-items:center;gap:8px;display:flex}._8EKcRG_spaceBetween{justify-content:space-between}._8EKcRG_chapterList{flex-direction:column;gap:8px;display:flex}._8EKcRG_chapter{border:1px solid var(--nf-border);background:var(--nf-bg);border-radius:10px;align-items:center;gap:10px;padding:9px 12px;transition:border-color .15s,box-shadow .15s,transform .1s;display:flex}._8EKcRG_chapter:hover{border-color:var(--nf-border-strong);box-shadow:var(--nf-shadow)}._8EKcRG_chapterNum{background:var(--nf-bg-inset);border:1px solid var(--nf-border);min-width:28px;height:28px;color:var(--nf-text-2);border-radius:8px;flex-shrink:0;justify-content:center;align-items:center;padding:0 8px;font-size:12px;font-weight:700;display:inline-flex}._8EKcRG_chapterMain{flex:1;min-width:0}._8EKcRG_chapterTitle{color:var(--nf-text);align-items:center;gap:8px;font-size:13px;font-weight:600;display:flex}._8EKcRG_chapterBeats{color:var(--nf-text-2);opacity:.85;text-overflow:ellipsis;white-space:nowrap;font-size:12px;overflow:hidden}._8EKcRG_chapterActions{opacity:.85;flex-shrink:0;gap:4px;transition:opacity .15s;display:flex}._8EKcRG_chapter:hover ._8EKcRG_chapterActions{opacity:1}._8EKcRG_badge{white-space:nowrap;letter-spacing:.2px;border:1px solid;border-radius:999px;padding:2px 9px;font-size:11px;font-weight:600}._8EKcRG_badgePending{color:var(--nf-info);border-color:var(--nf-info);background:color-mix(in srgb, var(--nf-info) 10%, transparent)}._8EKcRG_badgeGenerating{color:var(--nf-accent);border-color:var(--nf-accent);background:color-mix(in srgb, var(--nf-accent) 12%, transparent);animation:1.2s ease-in-out infinite _8EKcRG_pulse}._8EKcRG_badgeWritten{color:var(--nf-warn);border-color:var(--nf-warn);background:color-mix(in srgb, var(--nf-warn) 10%, transparent)}._8EKcRG_badgeRejected{color:var(--nf-error);border-color:var(--nf-error);background:color-mix(in srgb, var(--nf-error) 10%, transparent)}._8EKcRG_badgeDone{color:var(--nf-success);border-color:var(--nf-success);background:color-mix(in srgb, var(--nf-success) 10%, transparent)}._8EKcRG_badgeError{color:var(--nf-error);border-color:var(--nf-error);background:color-mix(in srgb, var(--nf-error) 10%, transparent)}._8EKcRG_reviewBox{border:1px solid var(--nf-border);background:var(--nf-bg-inset);border-radius:10px;flex-direction:column;gap:6px;padding:10px 12px;font-size:12.5px;display:flex}._8EKcRG_chapterPreview{white-space:pre-wrap;word-break:break-all;max-height:320px;color:var(--nf-text);background:var(--nf-bg-inset);border:1px solid var(--nf-border);border-radius:10px;margin:0;padding:10px 12px;font-family:inherit;font-size:12.5px;line-height:1.8;overflow:auto}._8EKcRG_chatScroll{border:1px solid var(--nf-border);background:var(--nf-bg-inset);border-radius:12px;flex-direction:column;flex:1;gap:10px;min-height:240px;max-height:480px;padding:14px;display:flex;overflow-y:auto}._8EKcRG_chatBubbleUser{background:linear-gradient(180deg, var(--nf-accent), var(--nf-accent-hover));max-width:90%;color:var(--nf-accent-fg);border-radius:14px 14px 4px;align-self:flex-end;padding:9px 14px;font-size:13px;box-shadow:0 2px 6px #4d6bfe33}._8EKcRG_chatBubbleAssistant{background:var(--nf-bg-raise);max-width:92%;color:var(--nf-text);border:1px solid var(--nf-border);box-shadow:var(--nf-shadow);border-radius:14px 14px 14px 4px;align-self:flex-start;padding:9px 14px;font-size:13px}._8EKcRG_chatRole{color:var(--nf-text-3);margin-bottom:3px;font-size:11px;font-weight:600}._8EKcRG_toolLive{background:var(--nf-bg-inset);border:1px dashed var(--nf-border-strong);white-space:pre-wrap;word-break:break-all;color:var(--nf-text-2);border-radius:8px;max-height:180px;margin-top:6px;padding:8px 10px;font-size:12px;line-height:1.7;overflow-y:auto}._8EKcRG_progress{border:1px solid var(--nf-border);white-space:pre-wrap;word-break:break-all;min-height:60px;max-height:220px;color:var(--nf-text-2);background:var(--nf-bg-inset);border-radius:10px;padding:10px 14px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12px;line-height:1.8;overflow:auto}._8EKcRG_progressLine{color:var(--nf-text-2);opacity:.9}._8EKcRG_progressLineDone{color:var(--nf-success);font-weight:600}._8EKcRG_progressLineError{color:var(--nf-error);font-weight:600}._8EKcRG_meta{color:var(--nf-text-2);opacity:.9;font-size:12px}._8EKcRG_fileList{color:var(--nf-text-2);opacity:.9;word-break:break-all;flex-direction:column;gap:3px;font-size:12px;display:flex}._8EKcRG_panelContent::-webkit-scrollbar,._8EKcRG_chatScroll::-webkit-scrollbar,._8EKcRG_chapterPreview::-webkit-scrollbar,._8EKcRG_progress::-webkit-scrollbar,._8EKcRG_toolLive::-webkit-scrollbar{width:8px;height:8px}._8EKcRG_panelContent::-webkit-scrollbar-thumb,._8EKcRG_chatScroll::-webkit-scrollbar-thumb,._8EKcRG_chapterPreview::-webkit-scrollbar-thumb,._8EKcRG_progress::-webkit-scrollbar-thumb,._8EKcRG_toolLive::-webkit-scrollbar-thumb{background:var(--nf-border-strong);border-radius:4px}._8EKcRG_panelContent::-webkit-scrollbar-thumb:hover,._8EKcRG_chatScroll::-webkit-scrollbar-thumb:hover,._8EKcRG_chapterPreview::-webkit-scrollbar-thumb:hover,._8EKcRG_progress::-webkit-scrollbar-thumb:hover,._8EKcRG_toolLive::-webkit-scrollbar-thumb:hover{background:var(--nf-text-3)}._8EKcRG_workflowList{flex-direction:column;gap:0;display:flex}._8EKcRG_workflowRow{align-items:flex-start;gap:12px;padding:8px 4px;display:flex;position:relative}._8EKcRG_workflowRow:before{content:\"\";background:var(--nf-border);width:2px;position:absolute;top:34px;bottom:-8px;left:13px}._8EKcRG_workflowRow:last-child:before{display:none}._8EKcRG_workflowDot{border:2px solid var(--nf-border-strong);background:var(--nf-bg-raise);width:28px;height:28px;color:var(--nf-text-2);z-index:1;border-radius:50%;flex-shrink:0;justify-content:center;align-items:center;font-size:12px;font-weight:700;display:inline-flex}._8EKcRG_workflowDotDone{border-color:var(--nf-success);background:color-mix(in srgb, var(--nf-success) 15%, var(--nf-bg-raise));color:var(--nf-success)}._8EKcRG_workflowDotActive{border-color:var(--nf-accent);background:var(--nf-accent);color:var(--nf-accent-fg);box-shadow:0 0 0 4px var(--nf-accent-soft)}._8EKcRG_workflowBody{flex-direction:column;flex:1;gap:4px;min-width:0;padding-top:2px;display:flex}._8EKcRG_workflowLabel{color:var(--nf-text);font-size:13px;font-weight:600}._8EKcRG_workflowHint{color:var(--nf-text-2);font-size:12px}@keyframes _8EKcRG_pulse{0%,to{opacity:1}50%{opacity:.45}}@keyframes _8EKcRG_fadeIn{0%{opacity:0;transform:translateY(4px)}to{opacity:1;transform:translateY(0)}}._8EKcRG_card{animation:.2s _8EKcRG_fadeIn}._8EKcRG_bookshelf{border-bottom:1px solid var(--nf-border);background:var(--nf-bg-raise);flex-wrap:wrap;flex-shrink:0;align-items:center;gap:10px;padding:8px 18px;display:flex}._8EKcRG_bookshelfLabel{color:var(--nf-text-3);letter-spacing:1px;flex-shrink:0;font-size:12px;font-weight:700}._8EKcRG_bookshelfList{flex-wrap:wrap;flex:1;align-items:center;gap:6px;min-width:0;display:flex}._8EKcRG_bookChip{border:1px solid var(--nf-border);background:var(--nf-bg-inset);cursor:pointer;border-radius:999px;align-items:center;gap:6px;max-width:220px;padding:4px 8px 4px 10px;font-size:12px;transition:border-color .15s,background .15s;display:flex}._8EKcRG_bookChip:hover{border-color:var(--nf-border-strong)}._8EKcRG_bookChipActive{border-color:var(--nf-accent);background:var(--nf-accent-soft);color:var(--nf-accent)}._8EKcRG_bookChipName{text-overflow:ellipsis;white-space:nowrap;font-weight:600;overflow:hidden}._8EKcRG_bookChipMeta{opacity:.7;white-space:nowrap;font-size:11px}._8EKcRG_bookChipRemove{color:var(--nf-text-3);cursor:pointer;background:0 0;border:none;border-radius:4px;padding:0 2px;font-size:13px;line-height:1}._8EKcRG_bookChipRemove:hover{color:var(--nf-error)}._8EKcRG_bookAdd{border:1px dashed var(--nf-border-strong);color:var(--nf-text-2);cursor:pointer;background:0 0;border-radius:999px;padding:4px 12px;font-size:12px;transition:border-color .15s,color .15s}._8EKcRG_bookAdd:hover{border-color:var(--nf-accent);color:var(--nf-accent)}._8EKcRG_bookCreateForm{align-items:center;gap:6px;display:flex}._8EKcRG_dropzone{border:2px dashed var(--nf-border-strong);text-align:center;color:var(--nf-text-2);cursor:pointer;border-radius:10px;flex-direction:column;align-items:center;gap:6px;padding:18px 14px;font-size:13px;transition:border-color .15s,background .15s;display:flex}._8EKcRG_dropzone:hover,._8EKcRG_dropzoneActive{border-color:var(--nf-accent);background:var(--nf-accent-soft);color:var(--nf-accent)}._8EKcRG_dropzoneIcon{font-size:22px;line-height:1}";
538
+ const tagId = "@ryan/dsh-novel-forge/panel.module.css";
539
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
540
+ const tag = document.createElement("style");
541
+ tag.dataset.plugin = "@ryan/dsh-novel-forge";
542
+ tag.dataset.pluginCss = tagId;
543
+ tag.textContent = css;
544
+ document.head.appendChild(tag);
545
+ }
546
+ var panel_module_css_default = {
547
+ "buttonPrimary": "_8EKcRG_buttonPrimary",
548
+ "badgeDone": "_8EKcRG_badgeDone",
549
+ "bookChipName": "_8EKcRG_bookChipName",
550
+ "field": "_8EKcRG_field",
551
+ "chapterBeats": "_8EKcRG_chapterBeats",
552
+ "bookshelf": "_8EKcRG_bookshelf",
553
+ "panelContent": "_8EKcRG_panelContent",
554
+ "chatBubbleAssistant": "_8EKcRG_chatBubbleAssistant",
555
+ "fieldLabel": "_8EKcRG_fieldLabel",
556
+ "badgeGenerating": "_8EKcRG_badgeGenerating",
557
+ "badge": "_8EKcRG_badge",
558
+ "chatScroll": "_8EKcRG_chatScroll",
559
+ "workflowLabel": "_8EKcRG_workflowLabel",
560
+ "workflowHint": "_8EKcRG_workflowHint",
561
+ "bookAdd": "_8EKcRG_bookAdd",
562
+ "dropzoneIcon": "_8EKcRG_dropzoneIcon",
563
+ "workflowDotActive": "_8EKcRG_workflowDotActive",
564
+ "dropzoneActive": "_8EKcRG_dropzoneActive",
565
+ "panel": "_8EKcRG_panel",
566
+ "workflowDot": "_8EKcRG_workflowDot",
567
+ "row": "_8EKcRG_row",
568
+ "chapterNum": "_8EKcRG_chapterNum",
569
+ "workflowDotDone": "_8EKcRG_workflowDotDone",
570
+ "button": "_8EKcRG_button",
571
+ "entry": "_8EKcRG_entry",
572
+ "entryIcon": "_8EKcRG_entryIcon",
573
+ "entryLabel": "_8EKcRG_entryLabel",
574
+ "tab": "_8EKcRG_tab",
575
+ "card": "_8EKcRG_card",
576
+ "buttonSmall": "_8EKcRG_buttonSmall",
577
+ "progress": "_8EKcRG_progress",
578
+ "buttonDanger": "_8EKcRG_buttonDanger",
579
+ "input": "_8EKcRG_input",
580
+ "badgePending": "_8EKcRG_badgePending",
581
+ "badgeWritten": "_8EKcRG_badgeWritten",
582
+ "badgeError": "_8EKcRG_badgeError",
583
+ "dropzone": "_8EKcRG_dropzone",
584
+ "progressLine": "_8EKcRG_progressLine",
585
+ "panelTitle": "_8EKcRG_panelTitle",
586
+ "reviewBox": "_8EKcRG_reviewBox",
587
+ "chatBubbleUser": "_8EKcRG_chatBubbleUser",
588
+ "workflowBody": "_8EKcRG_workflowBody",
589
+ "workflowRow": "_8EKcRG_workflowRow",
590
+ "chapterMain": "_8EKcRG_chapterMain",
591
+ "tabBar": "_8EKcRG_tabBar",
592
+ "pulse": "_8EKcRG_pulse",
593
+ "badgeRejected": "_8EKcRG_badgeRejected",
594
+ "textarea": "_8EKcRG_textarea",
595
+ "bookshelfList": "_8EKcRG_bookshelfList",
596
+ "bookshelfLabel": "_8EKcRG_bookshelfLabel",
597
+ "fadeIn": "_8EKcRG_fadeIn",
598
+ "progressLineError": "_8EKcRG_progressLineError",
599
+ "bookChipActive": "_8EKcRG_bookChipActive",
600
+ "chapterPreview": "_8EKcRG_chapterPreview",
601
+ "bookChip": "_8EKcRG_bookChip",
602
+ "meta": "_8EKcRG_meta",
603
+ "view": "_8EKcRG_view",
604
+ "workflowList": "_8EKcRG_workflowList",
605
+ "cardTitle": "_8EKcRG_cardTitle",
606
+ "iconButton": "_8EKcRG_iconButton",
607
+ "chapterList": "_8EKcRG_chapterList",
608
+ "spaceBetween": "_8EKcRG_spaceBetween",
609
+ "progressLineDone": "_8EKcRG_progressLineDone",
610
+ "chapterActions": "_8EKcRG_chapterActions",
611
+ "bookChipRemove": "_8EKcRG_bookChipRemove",
612
+ "chapterTitle": "_8EKcRG_chapterTitle",
613
+ "fileList": "_8EKcRG_fileList",
614
+ "bookCreateForm": "_8EKcRG_bookCreateForm",
615
+ "bookChipMeta": "_8EKcRG_bookChipMeta",
616
+ "chapter": "_8EKcRG_chapter",
617
+ "toolLive": "_8EKcRG_toolLive",
618
+ "panelHeader": "_8EKcRG_panelHeader",
619
+ "chatRole": "_8EKcRG_chatRole"
620
+ };
621
+ //#endregion
622
+ //#region src/client/panel/AssistantTab.tsx
623
+ /**
624
+ * AI 助手页签:与 AI 编辑对话讨论剧情,助手可通过动作指令直接修改
625
+ * 大纲 / 设定圣经 / 章节。流式渲染回复,工具调用以事件行展示。
626
+ */
627
+ /** The assistant conversation tab. */
628
+ function AssistantTab({ api }) {
629
+ const [lines, setLines] = (0, react.useState)([]);
630
+ const [input, setInput] = (0, react.useState)("");
631
+ const [busy, setBusy] = (0, react.useState)(false);
632
+ const [error, setError] = (0, react.useState)("");
633
+ const idRef = (0, react.useRef)(0);
634
+ const scrollRef = (0, react.useRef)(null);
635
+ /** Append a bubble (or extend the current assistant bubble). */
636
+ const pushLine = (0, react.useCallback)((line) => {
637
+ setLines((prev) => {
638
+ const last = prev[prev.length - 1];
639
+ if (line.role === "assistant" && last !== void 0 && last.role === "assistant" && last.tools.length === 0) return [...prev.slice(0, -1), {
640
+ ...last,
641
+ text: last.text + line.text
642
+ }];
643
+ return [...prev, {
644
+ ...line,
645
+ id: idRef.current++
646
+ }];
647
+ });
648
+ }, []);
649
+ /** Push a tool event onto the current assistant bubble. */
650
+ const pushTool = (0, react.useCallback)((tool) => {
651
+ setLines((prev) => {
652
+ const last = prev[prev.length - 1];
653
+ if (last === void 0 || last.role !== "assistant") return [...prev, {
654
+ id: idRef.current++,
655
+ role: "assistant",
656
+ text: "",
657
+ tools: [tool]
658
+ }];
659
+ return [...prev.slice(0, -1), {
660
+ ...last,
661
+ tools: [...last.tools, tool],
662
+ live: void 0
663
+ }];
664
+ });
665
+ }, []);
666
+ /** Append live tool output onto the current assistant bubble. */
667
+ const pushToolDelta = (0, react.useCallback)((text) => {
668
+ setLines((prev) => {
669
+ const last = prev[prev.length - 1];
670
+ if (last === void 0 || last.role !== "assistant") return prev;
671
+ return [...prev.slice(0, -1), {
672
+ ...last,
673
+ live: (last.live ?? "") + text
674
+ }];
675
+ });
676
+ }, []);
677
+ /** Load persisted history on mount. */
678
+ (0, react.useEffect)(() => {
679
+ let cancelled = false;
680
+ (async () => {
681
+ try {
682
+ const history = await api.assistantHistory();
683
+ if (cancelled) return;
684
+ const restored = [];
685
+ for (const entry of history) if (entry.role === "user") restored.push({
686
+ id: idRef.current++,
687
+ role: "user",
688
+ text: entry.content,
689
+ tools: []
690
+ });
691
+ else if (entry.role === "assistant") restored.push({
692
+ id: idRef.current++,
693
+ role: "assistant",
694
+ text: entry.content,
695
+ tools: []
696
+ });
697
+ else if (entry.role === "tool") {
698
+ const last = restored[restored.length - 1];
699
+ if (last !== void 0 && last.role === "assistant") last.tools.push({
700
+ name: entry.tool ?? "tool",
701
+ status: "done",
702
+ detail: entry.content.slice(0, 120)
703
+ });
704
+ }
705
+ setLines(restored);
706
+ } catch (err) {
707
+ if (!cancelled) setError(err.message);
708
+ }
709
+ })();
710
+ return () => {
711
+ cancelled = true;
712
+ };
713
+ }, [api]);
714
+ /** Auto-scroll to the newest line. */
715
+ (0, react.useEffect)(() => {
716
+ scrollRef.current?.scrollTo({ top: scrollRef.current.scrollHeight });
717
+ }, [lines]);
718
+ /** Send one message. */
719
+ const handleSend = async () => {
720
+ const message = input.trim();
721
+ if (message === "" || busy) return;
722
+ setInput("");
723
+ setError("");
724
+ pushLine({
725
+ role: "user",
726
+ text: message,
727
+ tools: []
728
+ });
729
+ setLines((prev) => [...prev, {
730
+ id: idRef.current++,
731
+ role: "assistant",
732
+ text: "",
733
+ tools: []
734
+ }]);
735
+ setBusy(true);
736
+ try {
737
+ await api.assistant(message, (frame) => {
738
+ if (frame.type === "delta") pushLine({
739
+ role: "assistant",
740
+ text: frame.text,
741
+ tools: []
742
+ });
743
+ else if (frame.type === "tool") pushTool({
744
+ name: frame.name,
745
+ status: frame.status,
746
+ detail: frame.detail
747
+ });
748
+ else if (frame.type === "toolDelta") pushToolDelta(frame.text);
749
+ else if (frame.type === "error") pushTool({
750
+ name: "error",
751
+ status: "error",
752
+ detail: frame.message
753
+ });
754
+ });
755
+ } catch (err) {
756
+ setError(err.message);
757
+ } finally {
758
+ setBusy(false);
759
+ }
760
+ };
761
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
762
+ className: panel_module_css_default.card,
763
+ style: {
764
+ flex: 1,
765
+ minHeight: 0,
766
+ display: "flex",
767
+ flexDirection: "column"
768
+ },
769
+ children: [
770
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
771
+ className: panel_module_css_default.cardTitle,
772
+ children: tt("tab.assistant")
773
+ }),
774
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
775
+ className: panel_module_css_default.meta,
776
+ children: tt("assistant.hint")
777
+ }),
778
+ error !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
779
+ style: {
780
+ color: "var(--nf-error)",
781
+ fontSize: 12
782
+ },
783
+ children: [
784
+ tt("common.error"),
785
+ ": ",
786
+ error
787
+ ]
788
+ }),
789
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
790
+ ref: scrollRef,
791
+ className: panel_module_css_default.chatScroll,
792
+ children: [
793
+ lines.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
794
+ className: panel_module_css_default.meta,
795
+ children: tt("assistant.empty")
796
+ }),
797
+ lines.map((line) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
798
+ className: line.role === "user" ? panel_module_css_default.chatBubbleUser : panel_module_css_default.chatBubbleAssistant,
799
+ children: [
800
+ line.role === "user" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
801
+ className: panel_module_css_default.chatRole,
802
+ children: "你"
803
+ }),
804
+ line.text !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
805
+ style: {
806
+ whiteSpace: "pre-wrap",
807
+ wordBreak: "break-word"
808
+ },
809
+ children: line.text
810
+ }),
811
+ line.live !== void 0 && line.live !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
812
+ className: panel_module_css_default.toolLive,
813
+ children: line.live
814
+ }),
815
+ line.tools.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
816
+ style: {
817
+ display: "flex",
818
+ flexDirection: "column",
819
+ gap: 2,
820
+ marginTop: 4,
821
+ fontSize: 11
822
+ },
823
+ children: line.tools.map((tool, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
824
+ style: { color: tool.status === "error" ? "var(--nf-error)" : tool.status === "start" ? "var(--nf-accent)" : "var(--nf-success)" },
825
+ children: tool.status === "start" ? tt("assistant.toolStart", { name: tool.name }) : tool.status === "done" ? tt("assistant.toolDone", {
826
+ name: tool.name,
827
+ detail: tool.detail ?? ""
828
+ }) : tt("assistant.toolError", {
829
+ name: tool.name,
830
+ detail: tool.detail ?? ""
831
+ })
832
+ }, i))
833
+ })
834
+ ]
835
+ }, line.id)),
836
+ busy && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
837
+ className: panel_module_css_default.meta,
838
+ style: { color: "var(--nf-accent)" },
839
+ children: "…"
840
+ })
841
+ ]
842
+ }),
843
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
844
+ className: panel_module_css_default.row,
845
+ style: { marginTop: 8 },
846
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
847
+ className: panel_module_css_default.textarea,
848
+ style: {
849
+ minHeight: 64,
850
+ flex: 1
851
+ },
852
+ placeholder: tt("assistant.placeholder"),
853
+ value: input,
854
+ onChange: (e) => {
855
+ setInput(e.target.value);
856
+ },
857
+ onKeyDown: (e) => {
858
+ if (e.key === "Enter" && !e.shiftKey) {
859
+ e.preventDefault();
860
+ handleSend();
861
+ }
862
+ }
863
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
864
+ type: "button",
865
+ className: `${panel_module_css_default.button} ${panel_module_css_default.buttonPrimary}`,
866
+ disabled: busy || input.trim() === "",
867
+ onClick: () => {
868
+ handleSend();
869
+ },
870
+ children: tt("assistant.send")
871
+ })]
872
+ })
873
+ ]
874
+ });
875
+ }
876
+ //#endregion
877
+ //#region src/client/panel/AssetsTab.tsx
878
+ /**
879
+ * 写作资产页签:题材基底库 / 推进模式库 / 反 AI 规则 / 写法引擎。
880
+ * 学习自 AI-Novel-Writing-Assistant 的四大资产模块,注入到生成与审稿提示词中。
881
+ */
882
+ /** 渲染题材树(带勾选当前题材)。 */
883
+ function GenreTree({ node, selected, onSelect }) {
884
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
885
+ style: {
886
+ display: "flex",
887
+ alignItems: "flex-start",
888
+ gap: 6,
889
+ cursor: "pointer"
890
+ },
891
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
892
+ type: "radio",
893
+ name: "genre",
894
+ checked: selected === node.name,
895
+ onChange: () => {
896
+ onSelect(node);
897
+ }
898
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("b", { children: node.name }), node.description !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
899
+ className: panel_module_css_default.meta,
900
+ children: [" — ", node.description]
901
+ })] })]
902
+ }), node.children.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
903
+ style: {
904
+ marginLeft: 22,
905
+ display: "flex",
906
+ flexDirection: "column",
907
+ gap: 4
908
+ },
909
+ children: node.children.map((child) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GenreTree, {
910
+ node: child,
911
+ selected,
912
+ onSelect
913
+ }, child.name))
914
+ })] });
915
+ }
916
+ /** 子页签定义。 */
917
+ const SUB_TABS = [
918
+ {
919
+ id: "genre",
920
+ label: "题材基底"
921
+ },
922
+ {
923
+ id: "progression",
924
+ label: "推进模式"
925
+ },
926
+ {
927
+ id: "templates",
928
+ label: "预置写法"
929
+ },
930
+ {
931
+ id: "rules",
932
+ label: "反 AI 规则"
933
+ },
934
+ {
935
+ id: "style",
936
+ label: "自定义写法"
937
+ }
938
+ ];
939
+ /** 写作资产页签。 */
940
+ function AssetsTab({ api }) {
941
+ const [assetTab, setAssetTab] = (0, react.useState)("genre");
942
+ const [data, setData] = (0, react.useState)(null);
943
+ const [busy, setBusy] = (0, react.useState)(false);
944
+ const [error, setError] = (0, react.useState)("");
945
+ const [notice, setNotice] = (0, react.useState)("");
946
+ const [sampleText, setSampleText] = (0, react.useState)("");
947
+ const [styleName, setStyleName] = (0, react.useState)("");
948
+ const [newRule, setNewRule] = (0, react.useState)("");
949
+ const [newProgression, setNewProgression] = (0, react.useState)("");
950
+ (0, react.useRef)(0);
951
+ /** Load assets (or reset from a new call). */
952
+ const refresh = (0, react.useCallback)(async () => {
953
+ try {
954
+ const result = await api.assets();
955
+ setData(result);
956
+ } catch (err) {
957
+ setError(err.message);
958
+ }
959
+ }, [api]);
960
+ (0, react.useEffect)(() => {
961
+ refresh();
962
+ }, [refresh]);
963
+ /** Patch assets and refresh. */
964
+ const patch = async (patch) => {
965
+ setBusy(true);
966
+ setError("");
967
+ try {
968
+ const result = await api.patchAssets(patch);
969
+ setData(result);
970
+ setNotice("已保存");
971
+ } catch (err) {
972
+ setError(err.message);
973
+ } finally {
974
+ setBusy(false);
975
+ }
976
+ };
977
+ /** 提取写法资产。 */
978
+ const handleExtractStyle = async () => {
979
+ if (sampleText.trim().length < 50) {
980
+ setError(tt("settings.exported") === "" ? "样本文本过短" : "样本文本过短(<50 字符)");
981
+ return;
982
+ }
983
+ setBusy(true);
984
+ setError("");
985
+ try {
986
+ const result = await api.styleEngine({
987
+ sampleText,
988
+ name: styleName
989
+ });
990
+ setData((prev) => prev === null ? prev : {
991
+ ...prev,
992
+ projectAssets: {
993
+ ...prev.projectAssets,
994
+ styleAssets: [...prev.projectAssets.styleAssets ?? [], result.styleAsset],
995
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
996
+ }
997
+ });
998
+ setNotice(`写法资产「${result.styleAsset.name}」已提取并绑定`);
999
+ setSampleText("");
1000
+ setStyleName("");
1001
+ } catch (err) {
1002
+ setError(err.message);
1003
+ } finally {
1004
+ setBusy(false);
1005
+ }
1006
+ };
1007
+ /** 添加自定义反 AI 规则(一行 "名称:要避免的" 简单格式由用户填写 JSON)。 */
1008
+ const handleAddRule = async () => {
1009
+ const text = newRule.trim();
1010
+ if (text === "") return;
1011
+ let rule;
1012
+ try {
1013
+ const parsed = JSON.parse(text);
1014
+ rule = {
1015
+ name: parsed.name ?? "自定义规则",
1016
+ avoid: parsed.avoid ?? "",
1017
+ fix: parsed.fix ?? ""
1018
+ };
1019
+ } catch {
1020
+ rule = {
1021
+ name: `自定义规则 ${(data?.projectAssets.antiAiRules ?? []).length + 1}`,
1022
+ avoid: text,
1023
+ fix: ""
1024
+ };
1025
+ }
1026
+ if (rule.avoid === "" && rule.fix === "") return;
1027
+ const next = [...data?.projectAssets.antiAiRules ?? [], rule];
1028
+ await patch({ antiAiRules: next });
1029
+ setNewRule("");
1030
+ };
1031
+ /** 设置题材。 */
1032
+ const handleSelectGenre = (node) => {
1033
+ patch({ genre: node });
1034
+ };
1035
+ /** 添加推进模式(从内置库选择)。 */
1036
+ const handleAddProgression = async (mode) => {
1037
+ const current = data?.projectAssets;
1038
+ if ((data?.projectAssets.primaryProgression ?? void 0) === void 0) await patch({ primaryProgression: {
1039
+ ...mode,
1040
+ primary: true
1041
+ } });
1042
+ else await patch({ auxiliaryProgressions: [...current?.auxiliaryProgressions ?? [], {
1043
+ ...mode,
1044
+ primary: false
1045
+ }] });
1046
+ };
1047
+ if (data === null) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1048
+ className: panel_module_css_default.card,
1049
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1050
+ className: panel_module_css_default.meta,
1051
+ children: tt("common.loading")
1052
+ })
1053
+ });
1054
+ const assets = data.projectAssets;
1055
+ const builtinRules = data.antiAiLibrary;
1056
+ const customRules = assets.antiAiRules ?? [];
1057
+ const genreLibrary = data.genreLibrary;
1058
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1059
+ style: {
1060
+ display: "flex",
1061
+ flexDirection: "column",
1062
+ gap: 12
1063
+ },
1064
+ children: [
1065
+ error !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1066
+ className: panel_module_css_default.card,
1067
+ style: { borderColor: "var(--nf-error)" },
1068
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1069
+ style: { color: "var(--nf-error)" },
1070
+ children: [
1071
+ tt("common.error"),
1072
+ ": ",
1073
+ error
1074
+ ]
1075
+ })
1076
+ }),
1077
+ notice !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1078
+ className: panel_module_css_default.card,
1079
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1080
+ style: { color: "var(--nf-success)" },
1081
+ children: notice
1082
+ })
1083
+ }),
1084
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1085
+ className: panel_module_css_default.tabBar,
1086
+ role: "tablist",
1087
+ style: {
1088
+ padding: "0 0 8px",
1089
+ borderBottom: "1px solid var(--nf-border)"
1090
+ },
1091
+ children: SUB_TABS.map((tab) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1092
+ type: "button",
1093
+ role: "tab",
1094
+ "aria-selected": assetTab === tab.id,
1095
+ "data-active": assetTab === tab.id ? "" : void 0,
1096
+ className: panel_module_css_default.tab,
1097
+ onClick: () => {
1098
+ setAssetTab(tab.id);
1099
+ },
1100
+ children: tab.label
1101
+ }, tab.id))
1102
+ }),
1103
+ assetTab === "genre" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1104
+ className: panel_module_css_default.card,
1105
+ children: [
1106
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1107
+ className: panel_module_css_default.cardTitle,
1108
+ children: "题材基底库"
1109
+ }),
1110
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1111
+ className: panel_module_css_default.meta,
1112
+ children: "这本书属于哪个阅读市场?题材定位会注入章节生成与审稿提示词。"
1113
+ }),
1114
+ assets.genre !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1115
+ style: {
1116
+ border: "1px solid var(--nf-border)",
1117
+ borderRadius: 6,
1118
+ padding: "6px 10px",
1119
+ fontSize: 12
1120
+ },
1121
+ children: [
1122
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("b", { children: ["当前题材:", assets.genre.name] }),
1123
+ " — ",
1124
+ assets.genre.description
1125
+ ]
1126
+ }),
1127
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1128
+ style: {
1129
+ display: "flex",
1130
+ flexDirection: "column",
1131
+ gap: 8,
1132
+ maxHeight: 340,
1133
+ overflowY: "auto"
1134
+ },
1135
+ children: genreLibrary.map((root) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GenreTree, {
1136
+ node: root,
1137
+ selected: assets.genre?.name ?? "",
1138
+ onSelect: handleSelectGenre
1139
+ }, root.name))
1140
+ })
1141
+ ]
1142
+ }),
1143
+ assetTab === "progression" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1144
+ className: panel_module_css_default.card,
1145
+ children: [
1146
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1147
+ className: panel_module_css_default.cardTitle,
1148
+ children: "推进模式库"
1149
+ }),
1150
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1151
+ className: panel_module_css_default.meta,
1152
+ children: "读者为什么继续看下一章?主模式 + 辅助模式注入卷规划与章节生成。"
1153
+ }),
1154
+ assets.primaryProgression !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1155
+ style: {
1156
+ border: "1px solid var(--nf-accent)",
1157
+ borderRadius: 6,
1158
+ padding: "6px 10px",
1159
+ fontSize: 12,
1160
+ color: "var(--nf-accent)"
1161
+ },
1162
+ children: [
1163
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("b", { children: ["主推进:", assets.primaryProgression.name] }),
1164
+ " — ",
1165
+ assets.primaryProgression.driver
1166
+ ]
1167
+ }),
1168
+ assets.auxiliaryProgressions.map((mode) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1169
+ style: {
1170
+ border: "1px solid var(--nf-border)",
1171
+ borderRadius: 6,
1172
+ padding: "6px 10px",
1173
+ fontSize: 12
1174
+ },
1175
+ children: [
1176
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("b", { children: mode.name }),
1177
+ " — ",
1178
+ mode.driver
1179
+ ]
1180
+ }, mode.name)),
1181
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1182
+ className: panel_module_css_default.meta,
1183
+ children: "从内置推进模式库选择添加(第一个设为主推进):"
1184
+ }),
1185
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1186
+ style: {
1187
+ display: "flex",
1188
+ flexDirection: "column",
1189
+ gap: 6,
1190
+ maxHeight: 260,
1191
+ overflowY: "auto"
1192
+ },
1193
+ children: data.progressionLibrary.map((mode) => {
1194
+ const alreadyPrimary = assets.primaryProgression?.name === mode.name;
1195
+ const alreadyAux = assets.auxiliaryProgressions.some((m) => m.name === mode.name);
1196
+ if (alreadyPrimary || alreadyAux) return null;
1197
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1198
+ className: panel_module_css_default.button,
1199
+ disabled: busy,
1200
+ onClick: () => {
1201
+ handleAddProgression(mode);
1202
+ },
1203
+ children: [
1204
+ "+ ",
1205
+ assets.primaryProgression === void 0 ? `主推进:` : "辅助:",
1206
+ mode.name,
1207
+ " — ",
1208
+ mode.driver.slice(0, 40),
1209
+ "…"
1210
+ ]
1211
+ }, mode.name);
1212
+ })
1213
+ })
1214
+ ]
1215
+ }),
1216
+ assetTab === "templates" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1217
+ className: panel_module_css_default.card,
1218
+ children: [
1219
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1220
+ className: panel_module_css_default.cardTitle,
1221
+ children: "预置写法模板"
1222
+ }),
1223
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1224
+ className: panel_module_css_default.meta,
1225
+ children: "从内置 8 套叙事风格模板中一键选用(来自 AI-Novel-Writing-Assistant 写法引擎),无需样本文本;绑定后生成与润色都遵循该风格。"
1226
+ }),
1227
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1228
+ style: {
1229
+ display: "flex",
1230
+ flexDirection: "column",
1231
+ gap: 8,
1232
+ maxHeight: 420,
1233
+ overflowY: "auto"
1234
+ },
1235
+ children: data.styleTemplates.map((template) => {
1236
+ const bound = assets.styleAssets.some((s) => s.name === template.name);
1237
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1238
+ style: {
1239
+ border: `1px solid ${bound ? "var(--nf-accent)" : "var(--nf-border)"}`,
1240
+ borderRadius: 6,
1241
+ padding: "8px 10px",
1242
+ fontSize: 12
1243
+ },
1244
+ children: [
1245
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1246
+ style: {
1247
+ display: "flex",
1248
+ alignItems: "center",
1249
+ justifyContent: "space-between",
1250
+ gap: 8
1251
+ },
1252
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
1253
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("b", { children: template.name }),
1254
+ " ",
1255
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1256
+ className: panel_module_css_default.badge,
1257
+ style: {
1258
+ borderColor: "var(--nf-text-3)",
1259
+ color: "var(--nf-text-3)"
1260
+ },
1261
+ children: template.category
1262
+ })
1263
+ ] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1264
+ className: `${panel_module_css_default.button} ${panel_module_css_default.buttonSmall} ${bound ? "" : panel_module_css_default.buttonPrimary}`,
1265
+ disabled: busy || bound,
1266
+ onClick: () => {
1267
+ const styleAsset = {
1268
+ name: template.name,
1269
+ proseRules: [...template.proseRules, ...template.rhythmRules.map((r) => `节奏:${r}`)],
1270
+ dialogueRules: template.dialogueRules,
1271
+ descriptionRules: template.languageRules,
1272
+ boundaries: [`模板「${template.name}」适用题材:${template.applicableGenres.join("、")}`, "不要违背模板的叙事单元结构与节奏约束"],
1273
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
1274
+ };
1275
+ patch({ styleAssets: [...data.projectAssets.styleAssets ?? [], styleAsset] });
1276
+ },
1277
+ children: bound ? "✓ 已绑定" : "+ 绑定"
1278
+ })]
1279
+ }),
1280
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1281
+ className: panel_module_css_default.meta,
1282
+ children: template.description
1283
+ }),
1284
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1285
+ className: panel_module_css_default.meta,
1286
+ children: ["叙述:", template.proseRules.slice(0, 2).join(";")]
1287
+ }),
1288
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1289
+ className: panel_module_css_default.meta,
1290
+ children: ["台词:", template.dialogueRules.slice(0, 1).join(";")]
1291
+ })
1292
+ ]
1293
+ }, template.key);
1294
+ })
1295
+ })
1296
+ ]
1297
+ }),
1298
+ assetTab === "rules" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1299
+ className: panel_module_css_default.card,
1300
+ children: [
1301
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1302
+ className: panel_module_css_default.cardTitle,
1303
+ children: "反 AI 规则"
1304
+ }),
1305
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1306
+ className: panel_module_css_default.meta,
1307
+ children: "写作时必须遵守的表达边界(内置全局 + 项目自定义),生成与审稿都会检查。"
1308
+ }),
1309
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1310
+ style: {
1311
+ display: "flex",
1312
+ flexDirection: "column",
1313
+ gap: 6,
1314
+ maxHeight: 280,
1315
+ overflowY: "auto"
1316
+ },
1317
+ children: [builtinRules.map((rule) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1318
+ style: {
1319
+ border: "1px solid var(--nf-border)",
1320
+ borderRadius: 6,
1321
+ padding: "6px 10px",
1322
+ fontSize: 12
1323
+ },
1324
+ children: [
1325
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("b", { children: rule.name }),
1326
+ " ",
1327
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1328
+ className: panel_module_css_default.badge,
1329
+ style: {
1330
+ borderColor: "var(--nf-text-3)",
1331
+ color: "var(--nf-text-3)"
1332
+ },
1333
+ children: "内置"
1334
+ }),
1335
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1336
+ className: panel_module_css_default.meta,
1337
+ children: ["避免:", rule.avoid]
1338
+ }),
1339
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1340
+ className: panel_module_css_default.meta,
1341
+ children: ["修正:", rule.fix]
1342
+ })
1343
+ ]
1344
+ }, rule.name)), customRules.map((rule) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1345
+ style: {
1346
+ border: "1px solid var(--nf-accent)",
1347
+ borderRadius: 6,
1348
+ padding: "6px 10px",
1349
+ fontSize: 12
1350
+ },
1351
+ children: [
1352
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("b", { children: rule.name }),
1353
+ " ",
1354
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1355
+ className: panel_module_css_default.badge,
1356
+ style: {
1357
+ borderColor: "var(--nf-accent)",
1358
+ color: "var(--nf-accent)"
1359
+ },
1360
+ children: "自定义"
1361
+ }),
1362
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1363
+ className: panel_module_css_default.meta,
1364
+ children: ["避免:", rule.avoid]
1365
+ }),
1366
+ rule.fix !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1367
+ className: panel_module_css_default.meta,
1368
+ children: ["修正:", rule.fix]
1369
+ })
1370
+ ]
1371
+ }, rule.name))]
1372
+ }),
1373
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1374
+ className: panel_module_css_default.row,
1375
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1376
+ className: panel_module_css_default.input,
1377
+ style: { flex: 1 },
1378
+ placeholder: "新增规则(格式:{\"name\":\"规则名\",\"avoid\":\"要避免的\",\"fix\":\"修正方向\"};或直接填要避免的问题)",
1379
+ value: newRule,
1380
+ onChange: (e) => {
1381
+ setNewRule(e.target.value);
1382
+ }
1383
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1384
+ className: `${panel_module_css_default.button} ${panel_module_css_default.buttonPrimary}`,
1385
+ disabled: busy || newRule.trim() === "",
1386
+ onClick: () => {
1387
+ handleAddRule();
1388
+ },
1389
+ children: "+ 添加"
1390
+ })]
1391
+ })
1392
+ ]
1393
+ }),
1394
+ assetTab === "style" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1395
+ className: panel_module_css_default.card,
1396
+ children: [
1397
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1398
+ className: panel_module_css_default.cardTitle,
1399
+ children: "自定义写法引擎"
1400
+ }),
1401
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1402
+ className: panel_module_css_default.meta,
1403
+ children: "粘贴一段你喜欢的样本文本,AI 提取叙事风格规则并绑定到本书,后续章节保持同一味道。"
1404
+ }),
1405
+ assets.styleAssets.map((style) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1406
+ style: {
1407
+ border: "1px solid var(--nf-border)",
1408
+ borderRadius: 6,
1409
+ padding: "6px 10px",
1410
+ fontSize: 12
1411
+ },
1412
+ children: [
1413
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("b", { children: style.name }),
1414
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1415
+ className: panel_module_css_default.meta,
1416
+ children: ["叙述:", style.proseRules.slice(0, 3).join(";")]
1417
+ }),
1418
+ style.dialogueRules.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1419
+ className: panel_module_css_default.meta,
1420
+ children: ["台词:", style.dialogueRules.slice(0, 2).join(";")]
1421
+ })
1422
+ ]
1423
+ }, style.name)),
1424
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
1425
+ className: panel_module_css_default.textarea,
1426
+ style: { minHeight: 90 },
1427
+ placeholder: "粘贴样本文本(一段能代表目标风格的文字,50 字以上)…",
1428
+ value: sampleText,
1429
+ onChange: (e) => {
1430
+ setSampleText(e.target.value);
1431
+ }
1432
+ }),
1433
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1434
+ className: panel_module_css_default.row,
1435
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1436
+ className: panel_module_css_default.input,
1437
+ style: { flex: 1 },
1438
+ placeholder: "写法资产名(可选)",
1439
+ value: styleName,
1440
+ onChange: (e) => {
1441
+ setStyleName(e.target.value);
1442
+ }
1443
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1444
+ className: `${panel_module_css_default.button} ${panel_module_css_default.buttonPrimary}`,
1445
+ disabled: busy || sampleText.trim().length < 50,
1446
+ onClick: () => {
1447
+ handleExtractStyle();
1448
+ },
1449
+ children: "提取并绑定"
1450
+ })]
1451
+ })
1452
+ ]
1453
+ })
1454
+ ]
1455
+ });
1456
+ }
1457
+ //#endregion
1458
+ //#region src/client/panel/BookshelfBar.tsx
1459
+ /**
1460
+ * 书架条:显示所有书,点击切换当前书(继续编译),+ 新建。
1461
+ */
1462
+ /** 书架条。 */
1463
+ function BookshelfBar({ api, shelf, onSwitch }) {
1464
+ const [creating, setCreating] = (0, react.useState)(false);
1465
+ const [name, setName] = (0, react.useState)("");
1466
+ const [busy, setBusy] = (0, react.useState)(false);
1467
+ const [error, setError] = (0, react.useState)("");
1468
+ const handleCreate = async () => {
1469
+ const bookName = name.trim();
1470
+ if (bookName === "") return;
1471
+ setBusy(true);
1472
+ setError("");
1473
+ try {
1474
+ await api.bookCreate(bookName);
1475
+ setCreating(false);
1476
+ setName("");
1477
+ onSwitch();
1478
+ } catch (err) {
1479
+ setError(err.message);
1480
+ } finally {
1481
+ setBusy(false);
1482
+ }
1483
+ };
1484
+ const handleActivate = async (id) => {
1485
+ if (id === shelf.activeBookId) return;
1486
+ setBusy(true);
1487
+ try {
1488
+ await api.bookActivate(id);
1489
+ onSwitch();
1490
+ } catch (err) {
1491
+ setError(err.message);
1492
+ } finally {
1493
+ setBusy(false);
1494
+ }
1495
+ };
1496
+ const handleRemove = async (id) => {
1497
+ setBusy(true);
1498
+ try {
1499
+ await api.bookRemove(id);
1500
+ onSwitch();
1501
+ } catch (err) {
1502
+ setError(err.message);
1503
+ } finally {
1504
+ setBusy(false);
1505
+ }
1506
+ };
1507
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1508
+ className: panel_module_css_default.bookshelf,
1509
+ children: [
1510
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1511
+ className: panel_module_css_default.bookshelfLabel,
1512
+ children: "书架"
1513
+ }),
1514
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1515
+ className: panel_module_css_default.bookshelfList,
1516
+ children: [shelf.books.map((book) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1517
+ className: `${panel_module_css_default.bookChip} ${book.id === shelf.activeBookId ? panel_module_css_default.bookChipActive : ""}`,
1518
+ onClick: () => {
1519
+ handleActivate(book.id);
1520
+ },
1521
+ title: book.outputDir,
1522
+ children: [
1523
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1524
+ className: panel_module_css_default.bookChipName,
1525
+ children: book.bookName
1526
+ }),
1527
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1528
+ className: panel_module_css_default.bookChipMeta,
1529
+ children: book.hasProject ? `${book.done}/${book.total} 章` : "未开书"
1530
+ }),
1531
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1532
+ type: "button",
1533
+ className: panel_module_css_default.bookChipRemove,
1534
+ title: "从书架移除",
1535
+ onClick: (e) => {
1536
+ e.stopPropagation();
1537
+ handleRemove(book.id);
1538
+ },
1539
+ children: "×"
1540
+ })
1541
+ ]
1542
+ }, book.id)), !creating ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1543
+ type: "button",
1544
+ className: panel_module_css_default.bookAdd,
1545
+ onClick: () => {
1546
+ setCreating(true);
1547
+ },
1548
+ children: "+ 新书"
1549
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1550
+ className: panel_module_css_default.bookCreateForm,
1551
+ children: [
1552
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1553
+ className: panel_module_css_default.input,
1554
+ style: { width: 140 },
1555
+ placeholder: "书名",
1556
+ value: name,
1557
+ onChange: (e) => {
1558
+ setName(e.target.value);
1559
+ },
1560
+ onKeyDown: (e) => {
1561
+ if (e.key === "Enter") handleCreate();
1562
+ },
1563
+ autoFocus: true
1564
+ }),
1565
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1566
+ type: "button",
1567
+ className: `${panel_module_css_default.button} ${panel_module_css_default.buttonSmall} ${panel_module_css_default.buttonPrimary}`,
1568
+ disabled: busy || name.trim() === "",
1569
+ onClick: () => {
1570
+ handleCreate();
1571
+ },
1572
+ children: "创建"
1573
+ }),
1574
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1575
+ type: "button",
1576
+ className: `${panel_module_css_default.button} ${panel_module_css_default.buttonSmall}`,
1577
+ onClick: () => {
1578
+ setCreating(false);
1579
+ },
1580
+ children: "取消"
1581
+ })
1582
+ ]
1583
+ })]
1584
+ }),
1585
+ error !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1586
+ style: {
1587
+ color: "var(--nf-error)",
1588
+ fontSize: 12
1589
+ },
1590
+ children: error
1591
+ })
1592
+ ]
1593
+ });
1594
+ }
1595
+ //#endregion
1596
+ //#region node_modules/.pnpm/fflate@0.8.3/node_modules/fflate/esm/browser.js
1597
+ var u8 = Uint8Array;
1598
+ var u16 = Uint16Array;
1599
+ var i32 = Int32Array;
1600
+ var fleb = new u8([
1601
+ 0,
1602
+ 0,
1603
+ 0,
1604
+ 0,
1605
+ 0,
1606
+ 0,
1607
+ 0,
1608
+ 0,
1609
+ 1,
1610
+ 1,
1611
+ 1,
1612
+ 1,
1613
+ 2,
1614
+ 2,
1615
+ 2,
1616
+ 2,
1617
+ 3,
1618
+ 3,
1619
+ 3,
1620
+ 3,
1621
+ 4,
1622
+ 4,
1623
+ 4,
1624
+ 4,
1625
+ 5,
1626
+ 5,
1627
+ 5,
1628
+ 5,
1629
+ 0,
1630
+ 0,
1631
+ 0,
1632
+ 0
1633
+ ]);
1634
+ var fdeb = new u8([
1635
+ 0,
1636
+ 0,
1637
+ 0,
1638
+ 0,
1639
+ 1,
1640
+ 1,
1641
+ 2,
1642
+ 2,
1643
+ 3,
1644
+ 3,
1645
+ 4,
1646
+ 4,
1647
+ 5,
1648
+ 5,
1649
+ 6,
1650
+ 6,
1651
+ 7,
1652
+ 7,
1653
+ 8,
1654
+ 8,
1655
+ 9,
1656
+ 9,
1657
+ 10,
1658
+ 10,
1659
+ 11,
1660
+ 11,
1661
+ 12,
1662
+ 12,
1663
+ 13,
1664
+ 13,
1665
+ 0,
1666
+ 0
1667
+ ]);
1668
+ var clim = new u8([
1669
+ 16,
1670
+ 17,
1671
+ 18,
1672
+ 0,
1673
+ 8,
1674
+ 7,
1675
+ 9,
1676
+ 6,
1677
+ 10,
1678
+ 5,
1679
+ 11,
1680
+ 4,
1681
+ 12,
1682
+ 3,
1683
+ 13,
1684
+ 2,
1685
+ 14,
1686
+ 1,
1687
+ 15
1688
+ ]);
1689
+ var freb = function(eb, start) {
1690
+ var b = new u16(31);
1691
+ for (var i = 0; i < 31; ++i) b[i] = start += 1 << eb[i - 1];
1692
+ var r = new i32(b[30]);
1693
+ for (var i = 1; i < 30; ++i) for (var j = b[i]; j < b[i + 1]; ++j) r[j] = j - b[i] << 5 | i;
1694
+ return {
1695
+ b,
1696
+ r
1697
+ };
1698
+ };
1699
+ var _a = freb(fleb, 2);
1700
+ var fl = _a.b;
1701
+ var revfl = _a.r;
1702
+ fl[28] = 258, revfl[258] = 28;
1703
+ var _b = freb(fdeb, 0);
1704
+ var fd = _b.b;
1705
+ _b.r;
1706
+ var rev = new u16(32768);
1707
+ for (var i = 0; i < 32768; ++i) {
1708
+ var x = (i & 43690) >> 1 | (i & 21845) << 1;
1709
+ x = (x & 52428) >> 2 | (x & 13107) << 2;
1710
+ x = (x & 61680) >> 4 | (x & 3855) << 4;
1711
+ rev[i] = ((x & 65280) >> 8 | (x & 255) << 8) >> 1;
1712
+ }
1713
+ var hMap = (function(cd, mb, r) {
1714
+ var s = cd.length;
1715
+ var i = 0;
1716
+ var l = new u16(mb);
1717
+ for (; i < s; ++i) if (cd[i]) ++l[cd[i] - 1];
1718
+ var le = new u16(mb);
1719
+ for (i = 1; i < mb; ++i) le[i] = le[i - 1] + l[i - 1] << 1;
1720
+ var co;
1721
+ if (r) {
1722
+ co = new u16(1 << mb);
1723
+ var rvb = 15 - mb;
1724
+ for (i = 0; i < s; ++i) if (cd[i]) {
1725
+ var sv = i << 4 | cd[i];
1726
+ var r_1 = mb - cd[i];
1727
+ var v = le[cd[i] - 1]++ << r_1;
1728
+ for (var m = v | (1 << r_1) - 1; v <= m; ++v) co[rev[v] >> rvb] = sv;
1729
+ }
1730
+ } else {
1731
+ co = new u16(s);
1732
+ for (i = 0; i < s; ++i) if (cd[i]) co[i] = rev[le[cd[i] - 1]++] >> 15 - cd[i];
1733
+ }
1734
+ return co;
1735
+ });
1736
+ var flt = new u8(288);
1737
+ for (var i = 0; i < 144; ++i) flt[i] = 8;
1738
+ for (var i = 144; i < 256; ++i) flt[i] = 9;
1739
+ for (var i = 256; i < 280; ++i) flt[i] = 7;
1740
+ for (var i = 280; i < 288; ++i) flt[i] = 8;
1741
+ var fdt = new u8(32);
1742
+ for (var i = 0; i < 32; ++i) fdt[i] = 5;
1743
+ var flrm = /*#__PURE__*/ hMap(flt, 9, 1);
1744
+ var fdrm = /*#__PURE__*/ hMap(fdt, 5, 1);
1745
+ var max = function(a) {
1746
+ var m = a[0];
1747
+ for (var i = 1; i < a.length; ++i) if (a[i] > m) m = a[i];
1748
+ return m;
1749
+ };
1750
+ var bits = function(d, p, m) {
1751
+ var o = p / 8 | 0;
1752
+ return (d[o] | d[o + 1] << 8) >> (p & 7) & m;
1753
+ };
1754
+ var bits16 = function(d, p) {
1755
+ var o = p / 8 | 0;
1756
+ return (d[o] | d[o + 1] << 8 | d[o + 2] << 16) >> (p & 7);
1757
+ };
1758
+ var shft = function(p) {
1759
+ return (p + 7) / 8 | 0;
1760
+ };
1761
+ var slc = function(v, s, e) {
1762
+ if (s == null || s < 0) s = 0;
1763
+ if (e == null || e > v.length) e = v.length;
1764
+ return new u8(v.subarray(s, e));
1765
+ };
1766
+ var ec = [
1767
+ "unexpected EOF",
1768
+ "invalid block type",
1769
+ "invalid length/literal",
1770
+ "invalid distance",
1771
+ "stream finished",
1772
+ "no stream handler",
1773
+ ,
1774
+ "no callback",
1775
+ "invalid UTF-8 data",
1776
+ "extra field too long",
1777
+ "date not in range 1980-2099",
1778
+ "filename too long",
1779
+ "stream finishing",
1780
+ "invalid zip data"
1781
+ ];
1782
+ var err = function(ind, msg, nt) {
1783
+ var e = new Error(msg || ec[ind]);
1784
+ e.code = ind;
1785
+ if (Error.captureStackTrace) Error.captureStackTrace(e, err);
1786
+ if (!nt) throw e;
1787
+ return e;
1788
+ };
1789
+ var inflt = function(dat, st, buf, dict) {
1790
+ var sl = dat.length, dl = dict ? dict.length : 0;
1791
+ if (!sl || st.f && !st.l) return buf || new u8(0);
1792
+ var noBuf = !buf;
1793
+ var resize = noBuf || st.i != 2;
1794
+ var noSt = st.i;
1795
+ if (noBuf) buf = new u8(sl * 3);
1796
+ var cbuf = function(l) {
1797
+ var bl = buf.length;
1798
+ if (l > bl) {
1799
+ var nbuf = new u8(Math.max(bl * 2, l));
1800
+ nbuf.set(buf);
1801
+ buf = nbuf;
1802
+ }
1803
+ };
1804
+ var final = st.f || 0, pos = st.p || 0, bt = st.b || 0, lm = st.l, dm = st.d, lbt = st.m, dbt = st.n;
1805
+ var tbts = sl * 8;
1806
+ do {
1807
+ if (!lm) {
1808
+ final = bits(dat, pos, 1);
1809
+ var type = bits(dat, pos + 1, 3);
1810
+ pos += 3;
1811
+ if (!type) {
1812
+ var s = shft(pos) + 4, l = dat[s - 4] | dat[s - 3] << 8, t = s + l;
1813
+ if (t > sl) {
1814
+ if (noSt) err(0);
1815
+ break;
1816
+ }
1817
+ if (resize) cbuf(bt + l);
1818
+ buf.set(dat.subarray(s, t), bt);
1819
+ st.b = bt += l, st.p = pos = t * 8, st.f = final;
1820
+ continue;
1821
+ } else if (type == 1) lm = flrm, dm = fdrm, lbt = 9, dbt = 5;
1822
+ else if (type == 2) {
1823
+ var hLit = bits(dat, pos, 31) + 257, hcLen = bits(dat, pos + 10, 15) + 4;
1824
+ var tl = hLit + bits(dat, pos + 5, 31) + 1;
1825
+ pos += 14;
1826
+ var ldt = new u8(tl);
1827
+ var clt = new u8(19);
1828
+ for (var i = 0; i < hcLen; ++i) clt[clim[i]] = bits(dat, pos + i * 3, 7);
1829
+ pos += hcLen * 3;
1830
+ var clb = max(clt), clbmsk = (1 << clb) - 1;
1831
+ var clm = hMap(clt, clb, 1);
1832
+ for (var i = 0; i < tl;) {
1833
+ var r = clm[bits(dat, pos, clbmsk)];
1834
+ pos += r & 15;
1835
+ var s = r >> 4;
1836
+ if (s < 16) ldt[i++] = s;
1837
+ else {
1838
+ var c = 0, n = 0;
1839
+ if (s == 16) n = 3 + bits(dat, pos, 3), pos += 2, c = ldt[i - 1];
1840
+ else if (s == 17) n = 3 + bits(dat, pos, 7), pos += 3;
1841
+ else if (s == 18) n = 11 + bits(dat, pos, 127), pos += 7;
1842
+ while (n--) ldt[i++] = c;
1843
+ }
1844
+ }
1845
+ var lt = ldt.subarray(0, hLit), dt = ldt.subarray(hLit);
1846
+ lbt = max(lt);
1847
+ dbt = max(dt);
1848
+ lm = hMap(lt, lbt, 1);
1849
+ dm = hMap(dt, dbt, 1);
1850
+ } else err(1);
1851
+ if (pos > tbts) {
1852
+ if (noSt) err(0);
1853
+ break;
1854
+ }
1855
+ }
1856
+ if (resize) cbuf(bt + 131072);
1857
+ var lms = (1 << lbt) - 1, dms = (1 << dbt) - 1;
1858
+ var lpos = pos;
1859
+ for (;; lpos = pos) {
1860
+ var c = lm[bits16(dat, pos) & lms], sym = c >> 4;
1861
+ pos += c & 15;
1862
+ if (pos > tbts) {
1863
+ if (noSt) err(0);
1864
+ break;
1865
+ }
1866
+ if (!c) err(2);
1867
+ if (sym < 256) buf[bt++] = sym;
1868
+ else if (sym == 256) {
1869
+ lpos = pos, lm = null;
1870
+ break;
1871
+ } else {
1872
+ var add = sym - 254;
1873
+ if (sym > 264) {
1874
+ var i = sym - 257, b = fleb[i];
1875
+ add = bits(dat, pos, (1 << b) - 1) + fl[i];
1876
+ pos += b;
1877
+ }
1878
+ var d = dm[bits16(dat, pos) & dms], dsym = d >> 4;
1879
+ if (!d) err(3);
1880
+ pos += d & 15;
1881
+ var dt = fd[dsym];
1882
+ if (dsym > 3) {
1883
+ var b = fdeb[dsym];
1884
+ dt += bits16(dat, pos) & (1 << b) - 1, pos += b;
1885
+ }
1886
+ if (pos > tbts) {
1887
+ if (noSt) err(0);
1888
+ break;
1889
+ }
1890
+ if (resize) cbuf(bt + 131072);
1891
+ var end = bt + add;
1892
+ if (bt < dt) {
1893
+ var shift = dl - dt, dend = Math.min(dt, end);
1894
+ if (shift + bt < 0) err(3);
1895
+ for (; bt < dend; ++bt) buf[bt] = dict[shift + bt];
1896
+ }
1897
+ for (; bt < end; ++bt) buf[bt] = buf[bt - dt];
1898
+ }
1899
+ }
1900
+ st.l = lm, st.p = lpos, st.b = bt, st.f = final;
1901
+ if (lm) final = 1, st.m = lbt, st.d = dm, st.n = dbt;
1902
+ } while (!final);
1903
+ return bt != buf.length && noBuf ? slc(buf, 0, bt) : buf.subarray(0, bt);
1904
+ };
1905
+ var et = /*#__PURE__*/ new u8(0);
1906
+ var b2 = function(d, b) {
1907
+ return d[b] | d[b + 1] << 8;
1908
+ };
1909
+ var b4 = function(d, b) {
1910
+ return (d[b] | d[b + 1] << 8 | d[b + 2] << 16 | d[b + 3] << 24) >>> 0;
1911
+ };
1912
+ var b8 = function(d, b) {
1913
+ return b4(d, b) + b4(d, b + 4) * 4294967296;
1914
+ };
1915
+ function inflateSync(data, opts) {
1916
+ return inflt(data, { i: 2 }, opts && opts.out, opts && opts.dictionary);
1917
+ }
1918
+ var td = typeof TextDecoder != "undefined" && /*#__PURE__*/ new TextDecoder();
1919
+ try {
1920
+ td.decode(et, { stream: true });
1921
+ } catch (e) {}
1922
+ var dutf8 = function(d) {
1923
+ for (var r = "", i = 0;;) {
1924
+ var c = d[i++];
1925
+ var eb = (c > 127) + (c > 223) + (c > 239);
1926
+ if (i + eb > d.length) return {
1927
+ s: r,
1928
+ r: slc(d, i - 1)
1929
+ };
1930
+ if (!eb) r += String.fromCharCode(c);
1931
+ else if (eb == 3) c = ((c & 15) << 18 | (d[i++] & 63) << 12 | (d[i++] & 63) << 6 | d[i++] & 63) - 65536, r += String.fromCharCode(55296 | c >> 10, 56320 | c & 1023);
1932
+ else if (eb & 1) r += String.fromCharCode((c & 31) << 6 | d[i++] & 63);
1933
+ else r += String.fromCharCode((c & 15) << 12 | (d[i++] & 63) << 6 | d[i++] & 63);
1934
+ }
1935
+ };
1936
+ /**
1937
+ * Converts a Uint8Array to a string
1938
+ * @param dat The data to decode to string
1939
+ * @param latin1 Whether or not to interpret the data as Latin-1. This should
1940
+ * not need to be true unless encoding to binary string.
1941
+ * @returns The original UTF-8/Latin-1 string
1942
+ */
1943
+ function strFromU8(dat, latin1) {
1944
+ if (latin1) {
1945
+ var r = "";
1946
+ for (var i = 0; i < dat.length; i += 16384) r += String.fromCharCode.apply(null, dat.subarray(i, i + 16384));
1947
+ return r;
1948
+ } else if (td) return td.decode(dat);
1949
+ else {
1950
+ var _a = dutf8(dat), s = _a.s, r = _a.r;
1951
+ if (r.length) err(8);
1952
+ return s;
1953
+ }
1954
+ }
1955
+ var slzh = function(d, b) {
1956
+ return b + 30 + b2(d, b + 26) + b2(d, b + 28);
1957
+ };
1958
+ var zh = function(d, b, z) {
1959
+ var fnl = b2(d, b + 28), efl = b2(d, b + 30), fn = strFromU8(d.subarray(b + 46, b + 46 + fnl), !(b2(d, b + 8) & 2048)), es = b + 46 + fnl;
1960
+ var _a = z64hs(d, es, efl, z, b4(d, b + 20), b4(d, b + 24), b4(d, b + 42)), sc = _a[0], su = _a[1], off = _a[2];
1961
+ return [
1962
+ b2(d, b + 10),
1963
+ sc,
1964
+ su,
1965
+ fn,
1966
+ es + efl + b2(d, b + 32),
1967
+ off
1968
+ ];
1969
+ };
1970
+ var z64hs = function(d, b, l, z, sc, su, off) {
1971
+ var nsc = sc == 4294967295, nsu = su == 4294967295, noff = off == 4294967295, e = b + l;
1972
+ var nf = nsc + nsu + noff;
1973
+ if (z && nf) {
1974
+ for (; b + 4 < e; b += 4 + b2(d, b + 2)) if (b2(d, b) == 1) return [
1975
+ nsc ? b8(d, b + 4 + 8 * nsu) : sc,
1976
+ nsu ? b8(d, b + 4) : su,
1977
+ noff ? b8(d, b + 4 + 8 * (nsu + nsc)) : off,
1978
+ 1
1979
+ ];
1980
+ if (z < 2) err(13);
1981
+ }
1982
+ return [
1983
+ sc,
1984
+ su,
1985
+ off,
1986
+ 0
1987
+ ];
1988
+ };
1989
+ /**
1990
+ * Synchronously decompresses a ZIP archive. Prefer using `unzip` for better
1991
+ * performance with more than one file.
1992
+ * @param data The raw compressed ZIP file
1993
+ * @param opts The ZIP extraction options
1994
+ * @returns The decompressed files
1995
+ */
1996
+ function unzipSync(data, opts) {
1997
+ var files = {};
1998
+ var e = data.length - 22;
1999
+ for (; b4(data, e) != 101010256; --e) if (!e || data.length - e > 65558) err(13);
2000
+ var c = b2(data, e + 8);
2001
+ if (!c) return {};
2002
+ var o = b4(data, e + 16);
2003
+ var z = b4(data, e - 20) == 117853008;
2004
+ if (z) {
2005
+ var ze = b4(data, e - 12);
2006
+ z = b4(data, ze) == 101075792;
2007
+ if (z) {
2008
+ c = b4(data, ze + 32);
2009
+ o = b4(data, ze + 48);
2010
+ }
2011
+ }
2012
+ var fltr = opts && opts.filter;
2013
+ for (var i = 0; i < c; ++i) {
2014
+ var _a = zh(data, o, z), c_2 = _a[0], sc = _a[1], su = _a[2], fn = _a[3], no = _a[4], off = _a[5], b = slzh(data, off);
2015
+ o = no;
2016
+ if (!fltr || fltr({
2017
+ name: fn,
2018
+ size: sc,
2019
+ originalSize: su,
2020
+ compression: c_2
2021
+ })) if (!c_2) files[fn] = slc(data, b, b + sc);
2022
+ else if (c_2 == 8) files[fn] = inflateSync(data.subarray(b, b + sc), { out: new u8(su) });
2023
+ else err(14, "unknown compression type " + c_2);
2024
+ }
2025
+ return files;
2026
+ }
2027
+ //#endregion
2028
+ //#region src/client/docx.ts
2029
+ /**
2030
+ * Browser-side docx outline extraction: a .docx is a zip whose
2031
+ * word/document.xml holds the body text in <w:t> runs inside <w:p> paragraphs.
2032
+ * Uses fflate (inlined into the client bundle) so the user can pick or drag a
2033
+ * docx without any server upload.
2034
+ *
2035
+ * Import from 'fflate/browser' (not 'fflate'): the default entry resolves to
2036
+ * the Node build (esm/index.mjs), which calls module.createRequire() for the
2037
+ * optional worker_threads path — inlining that into the browser bundle leaves
2038
+ * a bare require("module") the client-modules table cannot answer.
2039
+ */
2040
+ /** Decode the handful of XML entities docx bodies actually use. */
2041
+ function decodeEntities(text) {
2042
+ return text.replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, "\"").replace(/&apos;/g, "'").replace(/&amp;/g, "&").replace(/&nbsp;/g, " ");
2043
+ }
2044
+ /** Extract plain text from a docx buffer: one line per <w:p> paragraph. */
2045
+ function extractDocxTextFromBuffer(buffer) {
2046
+ const bytes = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer);
2047
+ let files;
2048
+ try {
2049
+ files = unzipSync(bytes);
2050
+ } catch (error) {
2051
+ throw new Error(`不是有效的 docx(zip 解压失败):${error.message}`);
2052
+ }
2053
+ const document = files["word/document.xml"];
2054
+ if (document === void 0) throw new Error("不是有效的 docx(缺少 word/document.xml)");
2055
+ const xml = strFromU8(document);
2056
+ const paragraphs = [];
2057
+ const parts = xml.split(/<w:p\b[^>]*>/);
2058
+ for (let i = 1; i < parts.length; i++) {
2059
+ const segment = parts[i];
2060
+ const runs = [];
2061
+ for (const match of segment.matchAll(/<w:t(?:\s[^>]*)?>([\s\S]*?)<\/w:t>|<w:tab\b[^>]*\/>|<w:br\b[^>]*\/>/g)) if (match[0].startsWith("<w:tab")) runs.push(" ");
2062
+ else if (match[0].startsWith("<w:br")) runs.push("\n");
2063
+ else runs.push(decodeEntities(match[1] ?? ""));
2064
+ paragraphs.push(runs.join("").replace(/\u00a0/g, " ").trimEnd());
2065
+ }
2066
+ const text = paragraphs.join("\n").replace(/\n{3,}/g, "\n\n").trim();
2067
+ if (text.length === 0) throw new Error("docx 中没有可提取的文本");
2068
+ return text;
2069
+ }
2070
+ //#endregion
2071
+ //#region src/client/panel/NovelPanel.tsx
2072
+ /**
2073
+ * The novel-forge workbench panel: tabs — 工作流 (guided pipeline), 大纲
2074
+ * (outline), 章节 (chapter plan + per-chapter write/review/rewrite/polish),
2075
+ * 设定库 (story bible), 伏笔 (foreshadows), 设置 (config). Generation and
2076
+ * review streams land in the progress console.
2077
+ */
2078
+ /** The tab bar definition. */
2079
+ const TABS = [
2080
+ {
2081
+ id: "workflow",
2082
+ label: tt("tab.workflow")
2083
+ },
2084
+ {
2085
+ id: "overview",
2086
+ label: tt("tab.overview")
2087
+ },
2088
+ {
2089
+ id: "plan",
2090
+ label: tt("tab.plan")
2091
+ },
2092
+ {
2093
+ id: "bible",
2094
+ label: tt("tab.bible")
2095
+ },
2096
+ {
2097
+ id: "assets",
2098
+ label: "写作资产"
2099
+ },
2100
+ {
2101
+ id: "foreshadow",
2102
+ label: tt("tab.foreshadow")
2103
+ },
2104
+ {
2105
+ id: "assistant",
2106
+ label: tt("tab.assistant")
2107
+ },
2108
+ {
2109
+ id: "settings",
2110
+ label: tt("tab.settings")
2111
+ }
2112
+ ];
2113
+ /** Whether any chapter is being generated right now. */
2114
+ function anyGenerating(chapters) {
2115
+ return (chapters ?? []).some((c) => c.status === "generating" || c.status === "reviewing");
2116
+ }
2117
+ /** Status badge class + label. */
2118
+ function statusBadge(chapter) {
2119
+ switch (chapter.status) {
2120
+ case "pending": return {
2121
+ cls: panel_module_css_default.badgePending,
2122
+ label: tt("plan.pending")
2123
+ };
2124
+ case "generating": return {
2125
+ cls: panel_module_css_default.badgeGenerating,
2126
+ label: tt("plan.generating")
2127
+ };
2128
+ case "written": return {
2129
+ cls: panel_module_css_default.badgeWritten,
2130
+ label: tt("plan.written")
2131
+ };
2132
+ case "reviewing": return {
2133
+ cls: panel_module_css_default.badgeGenerating,
2134
+ label: tt("plan.reviewing")
2135
+ };
2136
+ case "approved": return {
2137
+ cls: panel_module_css_default.badgeDone,
2138
+ label: tt("plan.approved")
2139
+ };
2140
+ case "rejected": return {
2141
+ cls: panel_module_css_default.badgeRejected,
2142
+ label: tt("plan.rejected")
2143
+ };
2144
+ case "error": return {
2145
+ cls: panel_module_css_default.badgeError,
2146
+ label: tt("plan.error")
2147
+ };
2148
+ }
2149
+ }
2150
+ /** One review issue line (severity-colored, theme-aware). */
2151
+ function severityColor(severity) {
2152
+ return severity === "high" ? "var(--nf-error)" : severity === "medium" ? "var(--nf-warn)" : "var(--nf-info)";
2153
+ }
2154
+ /** The novel-forge panel. */
2155
+ function NovelPanel({ controller, api }) {
2156
+ const [activeTab, setActiveTab] = (0, react.useState)("workflow");
2157
+ const [config, setConfig] = (0, react.useState)(null);
2158
+ const [project, setProject] = (0, react.useState)(null);
2159
+ const [generatedFiles, setGeneratedFiles] = (0, react.useState)([]);
2160
+ const [outlineText, setOutlineText] = (0, react.useState)("");
2161
+ const [customDocxPath, setCustomDocxPath] = (0, react.useState)("");
2162
+ const [shelf, setShelf] = (0, react.useState)(null);
2163
+ const [dragActive, setDragActive] = (0, react.useState)(false);
2164
+ const fileInputRef = (0, react.useRef)(null);
2165
+ const [planCount, setPlanCount] = (0, react.useState)(30);
2166
+ const [busy, setBusy] = (0, react.useState)(false);
2167
+ const [busyLabel, setBusyLabel] = (0, react.useState)("");
2168
+ const [error, setError] = (0, react.useState)("");
2169
+ const [notice, setNotice] = (0, react.useState)("");
2170
+ const [progress, setProgress] = (0, react.useState)([]);
2171
+ const [configDraft, setConfigDraft] = (0, react.useState)(null);
2172
+ const [expandedChapter, setExpandedChapter] = (0, react.useState)(null);
2173
+ const [chapterText, setChapterText] = (0, react.useState)("");
2174
+ const [rewriteInstruction, setRewriteInstruction] = (0, react.useState)("");
2175
+ const [localTarget, setLocalTarget] = (0, react.useState)("");
2176
+ const progressId = (0, react.useRef)(0);
2177
+ /** Refresh bookshelf. */
2178
+ const refreshShelf = (0, react.useCallback)(async () => {
2179
+ try {
2180
+ const snapshot = await api.bookshelf();
2181
+ setShelf(snapshot);
2182
+ } catch {}
2183
+ }, [api]);
2184
+ /** Append a progress console line. */
2185
+ const pushProgress = (0, react.useCallback)((text, kind = "info") => {
2186
+ setProgress((prev) => [...prev.slice(-300), {
2187
+ id: progressId.current++,
2188
+ text,
2189
+ kind
2190
+ }]);
2191
+ }, []);
2192
+ /** Refresh status (config + project + files). */
2193
+ const refresh = (0, react.useCallback)(async (showError = true) => {
2194
+ try {
2195
+ const status = await api.status();
2196
+ setConfig(status.config);
2197
+ setConfigDraft(status.config);
2198
+ setProject(status.project ?? null);
2199
+ setGeneratedFiles(status.generatedFiles);
2200
+ const nextOutline = status.project?.outline;
2201
+ if (nextOutline !== void 0 && outlineText === "") setOutlineText(nextOutline);
2202
+ } catch (err) {
2203
+ if (showError) setError(err.message);
2204
+ }
2205
+ }, [api, outlineText]);
2206
+ /** Handle a docx file (pick or drag): parse locally, save outline. */
2207
+ const handleDocxFile = (0, react.useCallback)(async (file) => {
2208
+ setBusy(true);
2209
+ setBusyLabel(tt("overview.loadingOutline"));
2210
+ setError("");
2211
+ try {
2212
+ const outline = extractDocxTextFromBuffer(await file.arrayBuffer());
2213
+ if (outline.length < 50) throw new Error("大纲内容过短(<50 字符),请检查文件");
2214
+ setOutlineText(outline);
2215
+ await api.saveOutline(outline);
2216
+ await refresh(false);
2217
+ pushProgress(`已从「${file.name}」读取大纲(${outline.length} 字)`, "done");
2218
+ } catch (err) {
2219
+ setError(err.message);
2220
+ pushProgress(`读取大纲失败:${err.message}`, "error");
2221
+ } finally {
2222
+ setBusy(false);
2223
+ setBusyLabel("");
2224
+ }
2225
+ }, [
2226
+ api,
2227
+ pushProgress,
2228
+ refresh
2229
+ ]);
2230
+ (0, react.useEffect)(() => {
2231
+ refresh();
2232
+ refreshShelf();
2233
+ }, []);
2234
+ /** Load the outline from docx (default path or custom). */
2235
+ const handleLoadDocx = async (useCustom) => {
2236
+ setBusy(true);
2237
+ setBusyLabel(tt("overview.loadingOutline"));
2238
+ setError("");
2239
+ try {
2240
+ const result = await api.loadOutline(useCustom ? customDocxPath || void 0 : void 0);
2241
+ setOutlineText(result.outline);
2242
+ await api.saveOutline(result.outline);
2243
+ await refresh(false);
2244
+ pushProgress(`大纲已读取(${result.chars} 字):${result.bookName}${result.path !== void 0 ? ` ← ${result.path}` : ""}`, "done");
2245
+ } catch (err) {
2246
+ setError(err.message);
2247
+ pushProgress(`读取大纲失败:${err.message}`, "error");
2248
+ } finally {
2249
+ setBusy(false);
2250
+ setBusyLabel("");
2251
+ }
2252
+ };
2253
+ /** Save the edited outline. */
2254
+ const handleSaveOutline = async () => {
2255
+ setBusy(true);
2256
+ setError("");
2257
+ try {
2258
+ await api.saveOutline(outlineText);
2259
+ setNotice(tt("overview.saved"));
2260
+ pushProgress(tt("overview.saved"), "done");
2261
+ await refresh(false);
2262
+ } catch (err) {
2263
+ setError(err.message);
2264
+ } finally {
2265
+ setBusy(false);
2266
+ }
2267
+ };
2268
+ /** Extract the story bible. */
2269
+ const handleBible = async () => {
2270
+ setBusy(true);
2271
+ setBusyLabel(tt("bible.gen"));
2272
+ setError("");
2273
+ try {
2274
+ const result = await api.bible(outlineText || void 0);
2275
+ setProject((prev) => prev === null ? prev : {
2276
+ ...prev,
2277
+ bible: result.bible,
2278
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
2279
+ });
2280
+ const bible = result.bible;
2281
+ pushProgress(tt("workflow.bibleDone", {
2282
+ n: bible.worldRules.length,
2283
+ c: bible.characters.length,
2284
+ r: bible.redLines.length
2285
+ }), "done");
2286
+ } catch (err) {
2287
+ setError(err.message);
2288
+ pushProgress(`提炼设定圣经失败:${err.message}`, "error");
2289
+ } finally {
2290
+ setBusy(false);
2291
+ setBusyLabel("");
2292
+ }
2293
+ };
2294
+ /** Plan volumes. */
2295
+ const handleVolumes = async () => {
2296
+ setBusy(true);
2297
+ setBusyLabel(tt("workflow.genVolumes"));
2298
+ setError("");
2299
+ try {
2300
+ const result = await api.volumes(outlineText || void 0);
2301
+ setProject((prev) => prev === null ? prev : {
2302
+ ...prev,
2303
+ volumes: result.volumes,
2304
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
2305
+ });
2306
+ pushProgress(tt("workflow.volumesDone", { n: result.volumes.length }), "done");
2307
+ } catch (err) {
2308
+ setError(err.message);
2309
+ pushProgress(`生成卷计划失败:${err.message}`, "error");
2310
+ } finally {
2311
+ setBusy(false);
2312
+ setBusyLabel("");
2313
+ }
2314
+ };
2315
+ /** Generate the chapter plan via LLM. */
2316
+ const handlePlan = async () => {
2317
+ setBusy(true);
2318
+ setBusyLabel(tt("plan.generate"));
2319
+ setError("");
2320
+ try {
2321
+ const result = await api.plan(outlineText || void 0, planCount);
2322
+ setProject((prev) => {
2323
+ const base = prev ?? {
2324
+ bookName: "",
2325
+ outline: outlineText,
2326
+ chapters: [],
2327
+ foreshadows: [],
2328
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
2329
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
2330
+ };
2331
+ return {
2332
+ ...base,
2333
+ chapters: [...base.chapters, ...result.chapters],
2334
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
2335
+ };
2336
+ });
2337
+ pushProgress(tt("workflow.planDone", { n: result.chapters.length }), "done");
2338
+ } catch (err) {
2339
+ setError(err.message);
2340
+ pushProgress(`生成章节计划失败:${err.message}`, "error");
2341
+ } finally {
2342
+ setBusy(false);
2343
+ setBusyLabel("");
2344
+ }
2345
+ };
2346
+ /** Shared frame handler for generate/rewrite/polish streams. */
2347
+ const applyJobFrame = (0, react.useCallback)((frame, label) => {
2348
+ if (frame.type === "start") {
2349
+ setProject((prev) => prev === null ? prev : {
2350
+ ...prev,
2351
+ chapters: prev.chapters.map((c) => c.no === frame.no ? {
2352
+ ...c,
2353
+ status: "generating",
2354
+ error: void 0
2355
+ } : c)
2356
+ });
2357
+ pushProgress(label(frame.no));
2358
+ } else if (frame.type === "delta") {
2359
+ if (frame.text.length % 3e3 < 600) pushProgress(`…已生成 ${frame.text.length} 字`);
2360
+ } else if (frame.type === "done" || frame.type === "rewritten") {
2361
+ setProject((prev) => prev === null ? prev : {
2362
+ ...prev,
2363
+ chapters: prev.chapters.map((c) => c.no === frame.no ? {
2364
+ ...c,
2365
+ status: "written",
2366
+ chars: frame.chars,
2367
+ file: frame.file,
2368
+ review: void 0
2369
+ } : c)
2370
+ });
2371
+ pushProgress(tt("progress.done", {
2372
+ no: frame.no,
2373
+ chars: frame.chars,
2374
+ file: frame.file
2375
+ }), "done");
2376
+ setGeneratedFiles((prev) => prev.includes(frame.file) ? prev : [...prev, frame.file]);
2377
+ } else if (frame.type === "review") {
2378
+ setProject((prev) => prev === null ? prev : {
2379
+ ...prev,
2380
+ chapters: prev.chapters.map((c) => c.no === frame.no ? {
2381
+ ...c,
2382
+ status: frame.report.passed ? "approved" : "rejected",
2383
+ review: frame.report
2384
+ } : c)
2385
+ });
2386
+ pushProgress(tt("progress.reviewed", {
2387
+ no: frame.no,
2388
+ score: frame.report.score,
2389
+ verdict: frame.report.verdict
2390
+ }), frame.report.passed ? "done" : "error");
2391
+ } else if (frame.type === "error") {
2392
+ setProject((prev) => prev === null ? prev : {
2393
+ ...prev,
2394
+ chapters: prev.chapters.map((c) => c.no === frame.no ? {
2395
+ ...c,
2396
+ status: "error",
2397
+ error: frame.message
2398
+ } : c)
2399
+ });
2400
+ pushProgress(tt("progress.error", {
2401
+ no: frame.no,
2402
+ message: frame.message
2403
+ }), "error");
2404
+ }
2405
+ }, [pushProgress]);
2406
+ /** Generate one chapter, streaming frames into the console. */
2407
+ const handleWriteChapter = async (no, skipReview) => {
2408
+ setBusy(true);
2409
+ setBusyLabel(`${tt("plan.write")} 第${no}章`);
2410
+ setError("");
2411
+ try {
2412
+ await api.generate(no, skipReview, (frame) => {
2413
+ applyJobFrame(frame, (n) => tt("progress.generating", {
2414
+ no: n,
2415
+ title: project?.chapters.find((c) => c.no === n)?.title ?? ""
2416
+ }));
2417
+ });
2418
+ } catch (err) {
2419
+ setError(err.message);
2420
+ pushProgress(`第 ${no} 章失败:${err.message}`, "error");
2421
+ } finally {
2422
+ setBusy(false);
2423
+ setBusyLabel("");
2424
+ await refresh(false);
2425
+ }
2426
+ };
2427
+ /** Batch-write all remaining chapters in sequence. */
2428
+ const handleWriteAll = async () => {
2429
+ const remaining = chapters.filter((c) => c.status === "pending" || c.status === "error");
2430
+ if (remaining.length === 0) return;
2431
+ setBusy(true);
2432
+ setBusyLabel(`${tt("plan.writeAllPending")}(共 ${remaining.length} 章)`);
2433
+ setError("");
2434
+ let failed = 0;
2435
+ for (const chapter of remaining) {
2436
+ pushProgress(`▶ 开始生成第 ${chapter.no} 章《${chapter.title}》`);
2437
+ try {
2438
+ await api.generate(chapter.no, true, (frame) => {
2439
+ applyJobFrame(frame, (n) => tt("progress.generating", {
2440
+ no: n,
2441
+ title: project?.chapters.find((c) => c.no === n)?.title ?? ""
2442
+ }));
2443
+ });
2444
+ } catch (err) {
2445
+ failed++;
2446
+ pushProgress(`第 ${chapter.no} 章失败:${err.message}`, "error");
2447
+ }
2448
+ }
2449
+ setBusy(false);
2450
+ setBusyLabel("");
2451
+ await refresh(false);
2452
+ pushProgress(failed === 0 ? `批量生成完成:${remaining.length} 章全部完成` : `批量生成结束:${remaining.length - failed} 章完成,${failed} 章失败`, failed === 0 ? "done" : "error");
2453
+ };
2454
+ /** Review one chapter. */
2455
+ const handleReview = async (no) => {
2456
+ setBusy(true);
2457
+ setBusyLabel(`${tt("plan.review")} 第${no}章`);
2458
+ setError("");
2459
+ try {
2460
+ const report = (await api.review(no)).report;
2461
+ setProject((prev) => prev === null ? prev : {
2462
+ ...prev,
2463
+ chapters: prev.chapters.map((c) => c.no === no ? {
2464
+ ...c,
2465
+ status: report.passed ? "approved" : "rejected",
2466
+ review: report
2467
+ } : c)
2468
+ });
2469
+ pushProgress(tt("progress.reviewed", {
2470
+ no,
2471
+ score: report.score,
2472
+ verdict: report.verdict
2473
+ }), report.passed ? "done" : "error");
2474
+ } catch (err) {
2475
+ setError(err.message);
2476
+ } finally {
2477
+ setBusy(false);
2478
+ setBusyLabel("");
2479
+ }
2480
+ };
2481
+ /** Rewrite one chapter (whole-chapter or local target). */
2482
+ const handleRewrite = async (no) => {
2483
+ setBusy(true);
2484
+ setBusyLabel(`${tt("plan.rewrite")} 第${no}章`);
2485
+ setError("");
2486
+ try {
2487
+ await api.rewrite(no, rewriteInstruction, localTarget, (frame) => {
2488
+ applyJobFrame(frame, (n) => tt("progress.rewriting", { no: n }));
2489
+ });
2490
+ setRewriteInstruction("");
2491
+ setLocalTarget("");
2492
+ } catch (err) {
2493
+ setError(err.message);
2494
+ pushProgress(`第 ${no} 章修订失败:${err.message}`, "error");
2495
+ } finally {
2496
+ setBusy(false);
2497
+ setBusyLabel("");
2498
+ await refresh(false);
2499
+ }
2500
+ };
2501
+ /** Polish one chapter. */
2502
+ const handlePolish = async (no) => {
2503
+ setBusy(true);
2504
+ setBusyLabel(`${tt("plan.polish")} 第${no}章`);
2505
+ setError("");
2506
+ try {
2507
+ await api.polish(no, (frame) => {
2508
+ applyJobFrame(frame, (n) => tt("progress.polishing", { no: n }));
2509
+ });
2510
+ } catch (err) {
2511
+ setError(err.message);
2512
+ pushProgress(`第 ${no} 章润色失败:${err.message}`, "error");
2513
+ } finally {
2514
+ setBusy(false);
2515
+ setBusyLabel("");
2516
+ await refresh(false);
2517
+ }
2518
+ };
2519
+ /** Approve a chapter manually. */
2520
+ const handleApprove = (no) => {
2521
+ setProject((prev) => prev === null ? prev : {
2522
+ ...prev,
2523
+ chapters: prev.chapters.map((c) => c.no === no ? {
2524
+ ...c,
2525
+ status: "approved"
2526
+ } : c),
2527
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
2528
+ });
2529
+ };
2530
+ /** Toggle chapter preview. */
2531
+ const handleToggleChapter = async (no) => {
2532
+ if (expandedChapter === no) {
2533
+ setExpandedChapter(null);
2534
+ setChapterText("");
2535
+ return;
2536
+ }
2537
+ setExpandedChapter(no);
2538
+ setChapterText("");
2539
+ try {
2540
+ const result = await api.chapter(no);
2541
+ setChapterText(result.markdown);
2542
+ } catch (err) {
2543
+ setChapterText(`(${err.message})`);
2544
+ }
2545
+ };
2546
+ /** Suggest foreshadows via LLM. */
2547
+ const handleSuggestForeshadows = async () => {
2548
+ setBusy(true);
2549
+ setBusyLabel(tt("foreshadow.suggest"));
2550
+ setError("");
2551
+ try {
2552
+ const result = await api.foreshadow({ suggest: true });
2553
+ setProject((prev) => prev === null ? prev : {
2554
+ ...prev,
2555
+ foreshadows: [...prev?.foreshadows ?? [], ...result.foreshadows],
2556
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
2557
+ });
2558
+ pushProgress(`AI 已建议 ${result.foreshadows.length} 条伏笔`, "done");
2559
+ } catch (err) {
2560
+ setError(err.message);
2561
+ pushProgress(`伏笔建议失败:${err.message}`, "error");
2562
+ } finally {
2563
+ setBusy(false);
2564
+ setBusyLabel("");
2565
+ }
2566
+ };
2567
+ /** Save the settings draft. */
2568
+ const handleSaveConfig = async () => {
2569
+ if (configDraft === null) return;
2570
+ setBusy(true);
2571
+ setError("");
2572
+ try {
2573
+ const result = await api.patchConfig({
2574
+ outlinePath: configDraft.outlinePath,
2575
+ outputDir: configDraft.outputDir,
2576
+ provider: configDraft.provider,
2577
+ model: configDraft.model,
2578
+ chapterChars: configDraft.chapterChars,
2579
+ maxTokens: configDraft.maxTokens,
2580
+ reviewPassScore: configDraft.reviewPassScore,
2581
+ autoReview: configDraft.autoReview
2582
+ });
2583
+ setConfig(result.config);
2584
+ setConfigDraft(result.config);
2585
+ setNotice(tt("settings.saved"));
2586
+ pushProgress(tt("settings.saved"), "done");
2587
+ await refresh(false);
2588
+ } catch (err) {
2589
+ setError(err.message);
2590
+ } finally {
2591
+ setBusy(false);
2592
+ }
2593
+ };
2594
+ /** Export the book. */
2595
+ const handleExport = async (format) => {
2596
+ setBusy(true);
2597
+ setError("");
2598
+ try {
2599
+ const result = await api.exportBook(format);
2600
+ setNotice(tt("settings.exported", {
2601
+ file: result.file,
2602
+ chars: result.chars,
2603
+ chapters: result.chapters
2604
+ }));
2605
+ pushProgress(tt("settings.exported", {
2606
+ file: result.file,
2607
+ chars: result.chars,
2608
+ chapters: result.chapters
2609
+ }), "done");
2610
+ } catch (err) {
2611
+ setError(err.message);
2612
+ } finally {
2613
+ setBusy(false);
2614
+ }
2615
+ };
2616
+ const busyAny = anyGenerating(project?.chapters);
2617
+ const chapters = project?.chapters ?? [];
2618
+ const doneCount = chapters.filter((c) => c.status === "approved" || c.status === "written" || c.status === "rejected").length;
2619
+ const pendingCount = chapters.filter((c) => c.status === "pending" || c.status === "error").length;
2620
+ const bible = project?.bible;
2621
+ const volumes = project?.volumes;
2622
+ const foreshadows = project?.foreshadows ?? [];
2623
+ /** Workflow timeline row: step dot + connector + label + optional action. */
2624
+ const workflowRow = (stepNo, done, label, hint, buttonLabel, onClick, disabled) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2625
+ className: panel_module_css_default.workflowRow,
2626
+ children: [
2627
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2628
+ className: `${panel_module_css_default.workflowDot} ${done ? panel_module_css_default.workflowDotDone : panel_module_css_default.workflowDotActive}`,
2629
+ children: done ? "✓" : stepNo
2630
+ }),
2631
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2632
+ className: panel_module_css_default.workflowBody,
2633
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2634
+ className: panel_module_css_default.workflowLabel,
2635
+ children: label
2636
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2637
+ className: panel_module_css_default.workflowHint,
2638
+ children: hint
2639
+ })]
2640
+ }),
2641
+ !done && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2642
+ type: "button",
2643
+ className: `${panel_module_css_default.button} ${panel_module_css_default.buttonSmall} ${panel_module_css_default.buttonPrimary}`,
2644
+ disabled: disabled || busy,
2645
+ onClick,
2646
+ children: buttonLabel
2647
+ })
2648
+ ]
2649
+ });
2650
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2651
+ className: panel_module_css_default.panel,
2652
+ children: [
2653
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2654
+ className: panel_module_css_default.panelHeader,
2655
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("h2", {
2656
+ className: panel_module_css_default.panelTitle,
2657
+ children: [tt("panel.title"), project?.bookName !== "" && project?.bookName !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2658
+ className: panel_module_css_default.badge,
2659
+ style: {
2660
+ borderColor: "var(--nf-accent)",
2661
+ color: "var(--nf-accent)",
2662
+ fontSize: 11
2663
+ },
2664
+ children: project.bookName
2665
+ })]
2666
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2667
+ type: "button",
2668
+ className: panel_module_css_default.iconButton,
2669
+ title: tt("common.close"),
2670
+ "aria-label": tt("common.close"),
2671
+ onClick: () => {
2672
+ controller.close();
2673
+ },
2674
+ children: "×"
2675
+ })]
2676
+ }),
2677
+ shelf !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BookshelfBar, {
2678
+ api,
2679
+ shelf,
2680
+ onSwitch: () => {
2681
+ refreshShelf();
2682
+ setOutlineText("");
2683
+ setProject(null);
2684
+ setGeneratedFiles([]);
2685
+ setChapterText("");
2686
+ setExpandedChapter(null);
2687
+ setProgress([]);
2688
+ refresh(false);
2689
+ }
2690
+ }),
2691
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2692
+ className: panel_module_css_default.tabBar,
2693
+ role: "tablist",
2694
+ children: TABS.map((tab) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2695
+ type: "button",
2696
+ role: "tab",
2697
+ "aria-selected": activeTab === tab.id,
2698
+ "data-active": activeTab === tab.id ? "" : void 0,
2699
+ className: panel_module_css_default.tab,
2700
+ onClick: () => {
2701
+ setActiveTab(tab.id);
2702
+ },
2703
+ children: tab.label
2704
+ }, tab.id))
2705
+ }),
2706
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2707
+ className: panel_module_css_default.panelContent,
2708
+ children: [
2709
+ error !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2710
+ className: panel_module_css_default.card,
2711
+ style: { borderColor: "var(--nf-error)" },
2712
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2713
+ style: { color: "var(--nf-error)" },
2714
+ children: [
2715
+ tt("common.error"),
2716
+ ": ",
2717
+ error
2718
+ ]
2719
+ })
2720
+ }),
2721
+ notice !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2722
+ className: panel_module_css_default.card,
2723
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2724
+ style: { color: "var(--nf-success)" },
2725
+ children: notice
2726
+ })
2727
+ }),
2728
+ busy && busyLabel !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2729
+ className: panel_module_css_default.card,
2730
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2731
+ style: { color: "var(--nf-accent)" },
2732
+ children: [busyLabel, "…"]
2733
+ })
2734
+ }),
2735
+ activeTab === "workflow" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2736
+ className: panel_module_css_default.card,
2737
+ children: [
2738
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2739
+ className: panel_module_css_default.cardTitle,
2740
+ children: tt("workflow.title")
2741
+ }),
2742
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2743
+ className: panel_module_css_default.meta,
2744
+ children: tt("workflow.progress", {
2745
+ bible: bible !== void 0 ? "✓" : "—",
2746
+ volumes: volumes !== void 0 ? "✓" : "—",
2747
+ plan: chapters.length > 0 ? "✓" : "—",
2748
+ done: doneCount,
2749
+ total: chapters.length
2750
+ })
2751
+ }),
2752
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2753
+ className: panel_module_css_default.workflowList,
2754
+ children: [
2755
+ workflowRow(1, project !== null, tt("workflow.step1"), "从 docx 或粘贴文本导入全书大纲", tt("workflow.loadOutline"), () => {
2756
+ handleLoadDocx(false);
2757
+ }, false),
2758
+ workflowRow(2, bible !== void 0, tt("workflow.step2"), "提炼人设 / 世界观 / 金手指规则 / 写作红线", tt("workflow.genBible"), () => {
2759
+ handleBible();
2760
+ }, project === null),
2761
+ workflowRow(3, volumes !== void 0, tt("workflow.step3"), "按剧情弧线划分全书卷结构", tt("workflow.genVolumes"), () => {
2762
+ handleVolumes();
2763
+ }, project === null),
2764
+ workflowRow(4, chapters.length > 0, tt("workflow.step4"), "每章标题 + 剧情要点 + 字数目标", tt("workflow.genPlan"), () => {
2765
+ handlePlan();
2766
+ }, project === null),
2767
+ workflowRow(5, doneCount > 0, tt("workflow.step5"), "逐章生成,自动摘要 + AI 审稿", tt("plan.write"), () => {
2768
+ setActiveTab("plan");
2769
+ }, false),
2770
+ workflowRow(6, doneCount > 0, tt("workflow.step6"), "去 AI 味润色 / 导出全本", tt("settings.exportTxt"), () => {
2771
+ handleExport("txt");
2772
+ }, false)
2773
+ ]
2774
+ })
2775
+ ]
2776
+ }),
2777
+ activeTab === "overview" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2778
+ className: panel_module_css_default.card,
2779
+ children: [
2780
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2781
+ className: panel_module_css_default.row,
2782
+ style: { justifyContent: "space-between" },
2783
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2784
+ className: panel_module_css_default.cardTitle,
2785
+ children: tt("tab.overview")
2786
+ }), project !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2787
+ className: panel_module_css_default.meta,
2788
+ children: [
2789
+ tt("overview.bookName"),
2790
+ ": ",
2791
+ project.bookName
2792
+ ]
2793
+ })]
2794
+ }),
2795
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2796
+ className: `${panel_module_css_default.dropzone} ${dragActive ? panel_module_css_default.dropzoneActive : ""}`,
2797
+ onClick: () => {
2798
+ fileInputRef.current?.click();
2799
+ },
2800
+ onDragOver: (e) => {
2801
+ e.preventDefault();
2802
+ setDragActive(true);
2803
+ },
2804
+ onDragLeave: () => {
2805
+ setDragActive(false);
2806
+ },
2807
+ onDrop: (e) => {
2808
+ e.preventDefault();
2809
+ setDragActive(false);
2810
+ const file = e.dataTransfer.files?.[0];
2811
+ if (file !== void 0) handleDocxFile(file);
2812
+ },
2813
+ children: [
2814
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2815
+ className: panel_module_css_default.dropzoneIcon,
2816
+ children: "📄"
2817
+ }),
2818
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: "点击选择本机 docx 大纲,或将文件拖到这里" }),
2819
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2820
+ className: panel_module_css_default.meta,
2821
+ children: "也支持粘贴文本到下方编辑区"
2822
+ }),
2823
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2824
+ ref: fileInputRef,
2825
+ type: "file",
2826
+ accept: ".docx,application/vnd.openxmlformats-officedocument.wordprocessingml.document",
2827
+ style: { display: "none" },
2828
+ onChange: (e) => {
2829
+ const file = e.target.files?.[0];
2830
+ if (file !== void 0) handleDocxFile(file);
2831
+ e.target.value = "";
2832
+ }
2833
+ })
2834
+ ]
2835
+ }),
2836
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2837
+ className: panel_module_css_default.row,
2838
+ style: { justifyContent: "space-between" },
2839
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2840
+ className: panel_module_css_default.meta,
2841
+ children: [
2842
+ tt("overview.outlineChars"),
2843
+ ": ",
2844
+ outlineText.length
2845
+ ]
2846
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2847
+ type: "button",
2848
+ className: panel_module_css_default.button,
2849
+ disabled: busy || outlineText.length < 50,
2850
+ onClick: () => {
2851
+ handleSaveOutline();
2852
+ },
2853
+ children: tt("overview.saveOutline")
2854
+ })]
2855
+ }),
2856
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
2857
+ className: panel_module_css_default.textarea,
2858
+ value: outlineText,
2859
+ placeholder: tt("overview.outlineHint"),
2860
+ onChange: (e) => {
2861
+ setOutlineText(e.target.value);
2862
+ },
2863
+ spellCheck: false
2864
+ })
2865
+ ]
2866
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2867
+ className: panel_module_css_default.card,
2868
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2869
+ className: panel_module_css_default.cardTitle,
2870
+ children: [
2871
+ tt("status.files"),
2872
+ "(",
2873
+ generatedFiles.length,
2874
+ ")"
2875
+ ]
2876
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2877
+ className: panel_module_css_default.fileList,
2878
+ children: [generatedFiles.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: tt("status.projectNone") }), generatedFiles.map((file) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: file }, file))]
2879
+ })]
2880
+ })] }),
2881
+ activeTab === "plan" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
2882
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2883
+ className: panel_module_css_default.card,
2884
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2885
+ className: panel_module_css_default.row,
2886
+ style: { justifyContent: "space-between" },
2887
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2888
+ className: panel_module_css_default.cardTitle,
2889
+ children: tt("tab.plan")
2890
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2891
+ className: panel_module_css_default.row,
2892
+ children: [
2893
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2894
+ className: panel_module_css_default.meta,
2895
+ children: tt("plan.generateHint")
2896
+ }),
2897
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2898
+ className: panel_module_css_default.input,
2899
+ style: { width: 72 },
2900
+ type: "number",
2901
+ min: 1,
2902
+ max: 200,
2903
+ value: planCount,
2904
+ onChange: (e) => {
2905
+ const v = Number(e.target.value);
2906
+ if (Number.isInteger(v)) setPlanCount(v);
2907
+ }
2908
+ }),
2909
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2910
+ className: panel_module_css_default.meta,
2911
+ children: tt("plan.count")
2912
+ }),
2913
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2914
+ type: "button",
2915
+ className: `${panel_module_css_default.button} ${panel_module_css_default.buttonPrimary}`,
2916
+ disabled: busy || outlineText.length < 50,
2917
+ onClick: () => {
2918
+ handlePlan();
2919
+ },
2920
+ children: tt("plan.generate")
2921
+ })
2922
+ ]
2923
+ })]
2924
+ }), volumes !== void 0 && volumes.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2925
+ className: panel_module_css_default.row,
2926
+ children: volumes.map((v) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2927
+ className: panel_module_css_default.badge,
2928
+ style: {
2929
+ borderColor: "var(--nf-accent)",
2930
+ color: "var(--nf-accent)"
2931
+ },
2932
+ children: [
2933
+ v.no,
2934
+ ". ",
2935
+ v.title,
2936
+ "(",
2937
+ v.chapterStart,
2938
+ "-",
2939
+ v.chapterEnd,
2940
+ ")"
2941
+ ]
2942
+ }, v.no))
2943
+ })]
2944
+ }),
2945
+ chapters.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2946
+ className: panel_module_css_default.card,
2947
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2948
+ className: panel_module_css_default.row,
2949
+ style: { justifyContent: "space-between" },
2950
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2951
+ className: panel_module_css_default.meta,
2952
+ children: [
2953
+ "共 ",
2954
+ chapters.length,
2955
+ " 章 · 已完成 ",
2956
+ doneCount,
2957
+ " · 待生成 ",
2958
+ pendingCount
2959
+ ]
2960
+ }), pendingCount > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2961
+ type: "button",
2962
+ className: `${panel_module_css_default.button} ${panel_module_css_default.buttonPrimary}`,
2963
+ disabled: busy,
2964
+ onClick: () => {
2965
+ handleWriteAll();
2966
+ },
2967
+ children: [
2968
+ tt("plan.writeAllPending"),
2969
+ "(",
2970
+ pendingCount,
2971
+ ")"
2972
+ ]
2973
+ })]
2974
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2975
+ className: panel_module_css_default.chapterList,
2976
+ children: chapters.map((chapter) => {
2977
+ const badge = statusBadge(chapter);
2978
+ const expanded = expandedChapter === chapter.no;
2979
+ const review = chapter.review;
2980
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2981
+ className: panel_module_css_default.chapter,
2982
+ children: [
2983
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2984
+ className: panel_module_css_default.chapterNum,
2985
+ children: chapter.no
2986
+ }),
2987
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2988
+ className: panel_module_css_default.chapterMain,
2989
+ children: [
2990
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2991
+ className: panel_module_css_default.chapterTitle,
2992
+ children: [
2993
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2994
+ type: "button",
2995
+ className: `${panel_module_css_default.button} ${panel_module_css_default.buttonSmall}`,
2996
+ style: { padding: "1px 6px" },
2997
+ onClick: () => {
2998
+ handleToggleChapter(chapter.no);
2999
+ },
3000
+ children: expanded ? "−" : "+"
3001
+ }),
3002
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: chapter.title }),
3003
+ chapter.status === "approved" && chapter.chars !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
3004
+ className: panel_module_css_default.meta,
3005
+ children: [chapter.chars, tt("common.chars")]
3006
+ }),
3007
+ chapter.volume > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
3008
+ className: panel_module_css_default.meta,
3009
+ children: [tt("plan.volumes"), chapter.volume]
3010
+ })
3011
+ ]
3012
+ }),
3013
+ !expanded && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3014
+ className: panel_module_css_default.chapterBeats,
3015
+ title: chapter.beats,
3016
+ children: chapter.beats
3017
+ }),
3018
+ expanded && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3019
+ style: {
3020
+ display: "flex",
3021
+ flexDirection: "column",
3022
+ gap: 6
3023
+ },
3024
+ children: [
3025
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3026
+ className: panel_module_css_default.meta,
3027
+ children: [
3028
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("b", { children: [tt("plan.beats"), ":"] }),
3029
+ " ",
3030
+ chapter.beats
3031
+ ]
3032
+ }),
3033
+ chapter.summary !== void 0 && chapter.summary !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3034
+ className: panel_module_css_default.meta,
3035
+ children: [
3036
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("b", { children: [tt("plan.summary"), ":"] }),
3037
+ " ",
3038
+ chapter.summary
3039
+ ]
3040
+ }),
3041
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
3042
+ className: panel_module_css_default.chapterPreview,
3043
+ children: chapterText || `(${tt("common.loading")})`
3044
+ }),
3045
+ review !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3046
+ className: panel_module_css_default.reviewBox,
3047
+ children: [
3048
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3049
+ className: panel_module_css_default.row,
3050
+ style: { justifyContent: "space-between" },
3051
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("b", { children: tt("plan.reviewReport") }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
3052
+ style: { color: review.passed ? "var(--nf-success)" : "var(--nf-error)" },
3053
+ children: [
3054
+ tt("plan.reviewScore"),
3055
+ ": ",
3056
+ review.score,
3057
+ " — ",
3058
+ review.passed ? tt("plan.reviewPass") : tt("plan.reviewFail")
3059
+ ]
3060
+ })]
3061
+ }),
3062
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3063
+ className: panel_module_css_default.meta,
3064
+ children: [
3065
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("b", { children: [tt("plan.reviewVerdict"), ":"] }),
3066
+ " ",
3067
+ review.verdict
3068
+ ]
3069
+ }),
3070
+ review.issues.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
3071
+ style: {
3072
+ margin: 0,
3073
+ paddingLeft: 18,
3074
+ fontSize: 12
3075
+ },
3076
+ children: review.issues.map((issue, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
3077
+ style: { color: severityColor(issue.severity) },
3078
+ children: [
3079
+ "[",
3080
+ issue.severity,
3081
+ "] ",
3082
+ issue.item,
3083
+ " → ",
3084
+ issue.suggestion
3085
+ ]
3086
+ }, i))
3087
+ })
3088
+ ]
3089
+ }),
3090
+ (chapter.status === "rejected" || chapter.status === "written" || chapter.status === "approved") && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3091
+ style: {
3092
+ display: "flex",
3093
+ flexDirection: "column",
3094
+ gap: 6
3095
+ },
3096
+ children: [
3097
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3098
+ className: panel_module_css_default.meta,
3099
+ style: { fontWeight: 600 },
3100
+ children: "修订(可整章或局部)"
3101
+ }),
3102
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3103
+ className: panel_module_css_default.field,
3104
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
3105
+ className: panel_module_css_default.fieldLabel,
3106
+ children: "要修改的原文片段(从上面正文复制一段;留空 = 整章修订)"
3107
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
3108
+ className: panel_module_css_default.textarea,
3109
+ style: { minHeight: 56 },
3110
+ placeholder: "例如:林越咬紧牙关:…(复制正文中的原句)",
3111
+ value: localTarget,
3112
+ onChange: (e) => {
3113
+ setLocalTarget(e.target.value);
3114
+ }
3115
+ })]
3116
+ }),
3117
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3118
+ className: panel_module_css_default.row,
3119
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
3120
+ className: panel_module_css_default.input,
3121
+ style: { flex: 1 },
3122
+ placeholder: "修订指令(如:这段对话太生硬,改得更口语化)",
3123
+ value: rewriteInstruction,
3124
+ onChange: (e) => {
3125
+ setRewriteInstruction(e.target.value);
3126
+ }
3127
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3128
+ type: "button",
3129
+ className: `${panel_module_css_default.button} ${panel_module_css_default.buttonPrimary}`,
3130
+ disabled: busy || busyAny,
3131
+ onClick: () => {
3132
+ handleRewrite(chapter.no);
3133
+ },
3134
+ children: tt("plan.rewrite")
3135
+ })]
3136
+ })
3137
+ ]
3138
+ })
3139
+ ]
3140
+ })
3141
+ ]
3142
+ }),
3143
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3144
+ className: `${panel_module_css_default.badge} ${badge.cls}`,
3145
+ children: badge.label
3146
+ }),
3147
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3148
+ className: panel_module_css_default.chapterActions,
3149
+ children: [
3150
+ (chapter.status === "pending" || chapter.status === "error") && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3151
+ type: "button",
3152
+ className: `${panel_module_css_default.button} ${panel_module_css_default.buttonSmall} ${panel_module_css_default.buttonPrimary}`,
3153
+ disabled: busy || busyAny,
3154
+ onClick: () => {
3155
+ handleWriteChapter(chapter.no, true);
3156
+ },
3157
+ children: tt("plan.write")
3158
+ }),
3159
+ (chapter.status === "written" || chapter.status === "rejected") && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3160
+ type: "button",
3161
+ className: `${panel_module_css_default.button} ${panel_module_css_default.buttonSmall}`,
3162
+ disabled: busy || busyAny,
3163
+ onClick: () => {
3164
+ handleReview(chapter.no);
3165
+ },
3166
+ children: tt("plan.review")
3167
+ }),
3168
+ chapter.status === "written" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3169
+ type: "button",
3170
+ className: `${panel_module_css_default.button} ${panel_module_css_default.buttonSmall}`,
3171
+ disabled: busy || busyAny,
3172
+ onClick: () => {
3173
+ handleApprove(chapter.no);
3174
+ },
3175
+ children: tt("plan.approve")
3176
+ }),
3177
+ (chapter.status === "written" || chapter.status === "rejected" || chapter.status === "approved") && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3178
+ type: "button",
3179
+ className: `${panel_module_css_default.button} ${panel_module_css_default.buttonSmall}`,
3180
+ disabled: busy || busyAny,
3181
+ onClick: () => {
3182
+ handlePolish(chapter.no);
3183
+ },
3184
+ children: tt("plan.polish")
3185
+ }),
3186
+ chapter.status === "rejected" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3187
+ type: "button",
3188
+ className: `${panel_module_css_default.button} ${panel_module_css_default.buttonSmall}`,
3189
+ disabled: busy || busyAny,
3190
+ onClick: () => {
3191
+ handleWriteChapter(chapter.no, true);
3192
+ },
3193
+ children: tt("plan.rewrite")
3194
+ })
3195
+ ]
3196
+ })
3197
+ ]
3198
+ }, chapter.no);
3199
+ })
3200
+ })]
3201
+ }),
3202
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3203
+ className: panel_module_css_default.card,
3204
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3205
+ className: panel_module_css_default.cardTitle,
3206
+ children: tt("plan.progress")
3207
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3208
+ className: panel_module_css_default.progress,
3209
+ children: [progress.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3210
+ className: panel_module_css_default.meta,
3211
+ children: tt("progress.empty")
3212
+ }), progress.map((line) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3213
+ className: line.kind === "done" ? panel_module_css_default.progressLineDone : line.kind === "error" ? panel_module_css_default.progressLineError : panel_module_css_default.progressLine,
3214
+ children: line.text
3215
+ }, line.id))]
3216
+ })]
3217
+ })
3218
+ ] }),
3219
+ activeTab === "bible" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3220
+ className: panel_module_css_default.card,
3221
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3222
+ className: panel_module_css_default.row,
3223
+ style: { justifyContent: "space-between" },
3224
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3225
+ className: panel_module_css_default.cardTitle,
3226
+ children: tt("bible.title")
3227
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3228
+ type: "button",
3229
+ className: `${panel_module_css_default.button} ${panel_module_css_default.buttonPrimary}`,
3230
+ disabled: busy,
3231
+ onClick: () => {
3232
+ handleBible();
3233
+ },
3234
+ children: tt("bible.gen")
3235
+ })]
3236
+ }), bible === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3237
+ className: panel_module_css_default.meta,
3238
+ children: tt("bible.none")
3239
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3240
+ style: {
3241
+ display: "flex",
3242
+ flexDirection: "column",
3243
+ gap: 10
3244
+ },
3245
+ children: [
3246
+ bible.genre !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [
3247
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("b", { children: [tt("bible.genre"), ":"] }),
3248
+ " ",
3249
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3250
+ className: panel_module_css_default.meta,
3251
+ children: bible.genre
3252
+ })
3253
+ ] }),
3254
+ bible.worldRules.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("b", { children: [
3255
+ tt("bible.worldRules"),
3256
+ "(",
3257
+ bible.worldRules.length,
3258
+ ")"
3259
+ ] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
3260
+ style: {
3261
+ margin: 0,
3262
+ paddingLeft: 18,
3263
+ fontSize: 12
3264
+ },
3265
+ children: bible.worldRules.map((r, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: r }, i))
3266
+ })] }),
3267
+ bible.characters.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("b", { children: [
3268
+ tt("bible.characters"),
3269
+ "(",
3270
+ bible.characters.length,
3271
+ ")"
3272
+ ] }), bible.characters.map((card) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3273
+ style: {
3274
+ marginTop: 4,
3275
+ fontSize: 12
3276
+ },
3277
+ children: [
3278
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("b", { children: card.name }),
3279
+ " ",
3280
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
3281
+ className: panel_module_css_default.meta,
3282
+ children: [
3283
+ "[",
3284
+ card.role,
3285
+ "] ",
3286
+ card.traits.join("、")
3287
+ ]
3288
+ }),
3289
+ card.goals !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3290
+ className: panel_module_css_default.meta,
3291
+ children: ["目标:", card.goals]
3292
+ }),
3293
+ card.relations !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3294
+ className: panel_module_css_default.meta,
3295
+ children: ["关系:", card.relations]
3296
+ })
3297
+ ]
3298
+ }, card.name))] }),
3299
+ bible.redLines.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("b", { children: [
3300
+ tt("bible.redLines"),
3301
+ "(",
3302
+ bible.redLines.length,
3303
+ ")"
3304
+ ] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
3305
+ style: {
3306
+ margin: 0,
3307
+ paddingLeft: 18,
3308
+ fontSize: 12,
3309
+ color: "var(--nf-error)"
3310
+ },
3311
+ children: bible.redLines.map((r, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: r }, i))
3312
+ })] }),
3313
+ bible.style.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("b", { children: [
3314
+ tt("bible.style"),
3315
+ "(",
3316
+ bible.style.length,
3317
+ ")"
3318
+ ] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
3319
+ style: {
3320
+ margin: 0,
3321
+ paddingLeft: 18,
3322
+ fontSize: 12
3323
+ },
3324
+ children: bible.style.map((r, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: r }, i))
3325
+ })] })
3326
+ ]
3327
+ })]
3328
+ }),
3329
+ activeTab === "assets" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AssetsTab, { api }),
3330
+ activeTab === "foreshadow" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3331
+ className: panel_module_css_default.card,
3332
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3333
+ className: panel_module_css_default.row,
3334
+ style: { justifyContent: "space-between" },
3335
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
3336
+ className: panel_module_css_default.cardTitle,
3337
+ children: [
3338
+ tt("foreshadow.title"),
3339
+ "(",
3340
+ foreshadows.length,
3341
+ ")"
3342
+ ]
3343
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3344
+ type: "button",
3345
+ className: `${panel_module_css_default.button} ${panel_module_css_default.buttonPrimary}`,
3346
+ disabled: busy,
3347
+ onClick: () => {
3348
+ handleSuggestForeshadows();
3349
+ },
3350
+ children: tt("foreshadow.suggest")
3351
+ })]
3352
+ }), foreshadows.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3353
+ className: panel_module_css_default.meta,
3354
+ children: tt("foreshadow.none")
3355
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3356
+ className: panel_module_css_default.chapterList,
3357
+ children: foreshadows.map((f) => {
3358
+ const statusLabel = {
3359
+ planned: tt("foreshadow.planned"),
3360
+ planted: tt("foreshadow.planted"),
3361
+ progressing: tt("foreshadow.progressing"),
3362
+ resolved: tt("foreshadow.resolved"),
3363
+ abandoned: tt("foreshadow.abandoned")
3364
+ }[f.status];
3365
+ const statusColor = f.status === "resolved" ? "var(--nf-success)" : f.status === "planted" || f.status === "progressing" ? "var(--nf-accent)" : f.status === "abandoned" ? "var(--nf-text-3)" : "var(--nf-info)";
3366
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3367
+ className: panel_module_css_default.chapter,
3368
+ children: [
3369
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3370
+ className: panel_module_css_default.chapterMain,
3371
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3372
+ className: panel_module_css_default.chapterTitle,
3373
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: f.description })
3374
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3375
+ className: panel_module_css_default.meta,
3376
+ children: [
3377
+ f.plantedChapter !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
3378
+ tt("foreshadow.plantedAt"),
3379
+ " 第",
3380
+ f.plantedChapter,
3381
+ "章 · "
3382
+ ] }),
3383
+ f.targetChapter !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
3384
+ tt("foreshadow.target"),
3385
+ " 第",
3386
+ f.targetChapter,
3387
+ "章 · "
3388
+ ] }),
3389
+ f.resolvedNote !== void 0 && f.resolvedNote !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
3390
+ "回收:",
3391
+ f.resolvedNote,
3392
+ " · "
3393
+ ] })
3394
+ ]
3395
+ })]
3396
+ }),
3397
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3398
+ className: panel_module_css_default.badge,
3399
+ style: {
3400
+ borderColor: statusColor,
3401
+ color: statusColor
3402
+ },
3403
+ children: statusLabel
3404
+ }),
3405
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3406
+ className: panel_module_css_default.row,
3407
+ style: { gap: 4 },
3408
+ children: [f.status === "planned" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3409
+ type: "button",
3410
+ className: `${panel_module_css_default.button} ${panel_module_css_default.buttonSmall}`,
3411
+ disabled: busy,
3412
+ onClick: () => {
3413
+ api.foreshadow({
3414
+ id: f.id,
3415
+ status: "planted",
3416
+ plantedChapter: doneCount + 1
3417
+ }).then((r) => setProject((prev) => prev === null ? prev : {
3418
+ ...prev,
3419
+ foreshadows: r.foreshadows
3420
+ }));
3421
+ },
3422
+ children: tt("foreshadow.setPlanted")
3423
+ }), (f.status === "planted" || f.status === "progressing") && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3424
+ type: "button",
3425
+ className: `${panel_module_css_default.button} ${panel_module_css_default.buttonSmall}`,
3426
+ disabled: busy,
3427
+ onClick: () => {
3428
+ api.foreshadow({
3429
+ id: f.id,
3430
+ status: "resolved",
3431
+ resolvedNote: `第${doneCount}章回收`
3432
+ }).then((r) => setProject((prev) => prev === null ? prev : {
3433
+ ...prev,
3434
+ foreshadows: r.foreshadows
3435
+ }));
3436
+ },
3437
+ children: tt("foreshadow.setResolved")
3438
+ })]
3439
+ })
3440
+ ]
3441
+ }, f.id);
3442
+ })
3443
+ })]
3444
+ }),
3445
+ activeTab === "assistant" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AssistantTab, { api }),
3446
+ activeTab === "settings" && configDraft !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3447
+ className: panel_module_css_default.card,
3448
+ children: [
3449
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3450
+ className: panel_module_css_default.cardTitle,
3451
+ children: tt("settings.title")
3452
+ }),
3453
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3454
+ className: panel_module_css_default.field,
3455
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
3456
+ className: panel_module_css_default.fieldLabel,
3457
+ children: tt("settings.outlinePath")
3458
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
3459
+ className: panel_module_css_default.input,
3460
+ value: configDraft.outlinePath,
3461
+ onChange: (e) => {
3462
+ setConfigDraft({
3463
+ ...configDraft,
3464
+ outlinePath: e.target.value
3465
+ });
3466
+ }
3467
+ })]
3468
+ }),
3469
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3470
+ className: panel_module_css_default.field,
3471
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
3472
+ className: panel_module_css_default.fieldLabel,
3473
+ children: tt("settings.outputDir")
3474
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
3475
+ className: panel_module_css_default.input,
3476
+ value: configDraft.outputDir,
3477
+ onChange: (e) => {
3478
+ setConfigDraft({
3479
+ ...configDraft,
3480
+ outputDir: e.target.value
3481
+ });
3482
+ }
3483
+ })]
3484
+ }),
3485
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3486
+ className: panel_module_css_default.row,
3487
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3488
+ className: panel_module_css_default.field,
3489
+ style: { flex: 1 },
3490
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
3491
+ className: panel_module_css_default.fieldLabel,
3492
+ children: tt("settings.provider")
3493
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
3494
+ className: panel_module_css_default.input,
3495
+ value: configDraft.provider,
3496
+ onChange: (e) => {
3497
+ setConfigDraft({
3498
+ ...configDraft,
3499
+ provider: e.target.value
3500
+ });
3501
+ }
3502
+ })]
3503
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3504
+ className: panel_module_css_default.field,
3505
+ style: { flex: 1 },
3506
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
3507
+ className: panel_module_css_default.fieldLabel,
3508
+ children: tt("settings.model")
3509
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
3510
+ className: panel_module_css_default.input,
3511
+ value: configDraft.model,
3512
+ onChange: (e) => {
3513
+ setConfigDraft({
3514
+ ...configDraft,
3515
+ model: e.target.value
3516
+ });
3517
+ }
3518
+ })]
3519
+ })]
3520
+ }),
3521
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3522
+ className: panel_module_css_default.row,
3523
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3524
+ className: panel_module_css_default.field,
3525
+ style: { flex: 1 },
3526
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
3527
+ className: panel_module_css_default.fieldLabel,
3528
+ children: tt("settings.chapterChars")
3529
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
3530
+ className: panel_module_css_default.input,
3531
+ type: "number",
3532
+ min: 1e3,
3533
+ max: 2e4,
3534
+ value: configDraft.chapterChars,
3535
+ onChange: (e) => {
3536
+ setConfigDraft({
3537
+ ...configDraft,
3538
+ chapterChars: Number(e.target.value)
3539
+ });
3540
+ }
3541
+ })]
3542
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3543
+ className: panel_module_css_default.field,
3544
+ style: { flex: 1 },
3545
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
3546
+ className: panel_module_css_default.fieldLabel,
3547
+ children: tt("settings.maxTokens")
3548
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
3549
+ className: panel_module_css_default.input,
3550
+ type: "number",
3551
+ min: 2e3,
3552
+ max: 64e3,
3553
+ value: configDraft.maxTokens,
3554
+ onChange: (e) => {
3555
+ setConfigDraft({
3556
+ ...configDraft,
3557
+ maxTokens: Number(e.target.value)
3558
+ });
3559
+ }
3560
+ })]
3561
+ })]
3562
+ }),
3563
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3564
+ className: panel_module_css_default.row,
3565
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3566
+ className: panel_module_css_default.field,
3567
+ style: { flex: 1 },
3568
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
3569
+ className: panel_module_css_default.fieldLabel,
3570
+ children: tt("settings.reviewPassScore")
3571
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
3572
+ className: panel_module_css_default.input,
3573
+ type: "number",
3574
+ min: 0,
3575
+ max: 100,
3576
+ value: configDraft.reviewPassScore,
3577
+ onChange: (e) => {
3578
+ setConfigDraft({
3579
+ ...configDraft,
3580
+ reviewPassScore: Number(e.target.value)
3581
+ });
3582
+ }
3583
+ })]
3584
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3585
+ className: panel_module_css_default.field,
3586
+ style: { flex: 1 },
3587
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
3588
+ className: panel_module_css_default.fieldLabel,
3589
+ children: tt("settings.autoReview")
3590
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
3591
+ className: panel_module_css_default.input,
3592
+ value: configDraft.autoReview ? "1" : "0",
3593
+ onChange: (e) => {
3594
+ setConfigDraft({
3595
+ ...configDraft,
3596
+ autoReview: e.target.value === "1"
3597
+ });
3598
+ },
3599
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
3600
+ value: "1",
3601
+ children: "✓ 是"
3602
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
3603
+ value: "0",
3604
+ children: "✗ 否"
3605
+ })]
3606
+ })]
3607
+ })]
3608
+ }),
3609
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3610
+ className: panel_module_css_default.row,
3611
+ children: [
3612
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3613
+ type: "button",
3614
+ className: `${panel_module_css_default.button} ${panel_module_css_default.buttonPrimary}`,
3615
+ disabled: busy,
3616
+ onClick: () => {
3617
+ handleSaveConfig();
3618
+ },
3619
+ children: tt("settings.save")
3620
+ }),
3621
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3622
+ type: "button",
3623
+ className: panel_module_css_default.button,
3624
+ onClick: () => {
3625
+ api.openFolder();
3626
+ },
3627
+ children: tt("settings.openFolder")
3628
+ }),
3629
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
3630
+ className: panel_module_css_default.meta,
3631
+ children: [
3632
+ "当前:",
3633
+ config?.provider,
3634
+ " / ",
3635
+ config?.model,
3636
+ " · ",
3637
+ config?.outputDir
3638
+ ]
3639
+ })
3640
+ ]
3641
+ }),
3642
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3643
+ className: panel_module_css_default.row,
3644
+ children: [
3645
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3646
+ className: panel_module_css_default.cardTitle,
3647
+ children: tt("settings.export")
3648
+ }),
3649
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3650
+ type: "button",
3651
+ className: panel_module_css_default.button,
3652
+ disabled: busy || chapters.length === 0,
3653
+ onClick: () => {
3654
+ handleExport("txt");
3655
+ },
3656
+ children: tt("settings.exportTxt")
3657
+ }),
3658
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3659
+ type: "button",
3660
+ className: panel_module_css_default.button,
3661
+ disabled: busy || chapters.length === 0,
3662
+ onClick: () => {
3663
+ handleExport("md");
3664
+ },
3665
+ children: tt("settings.exportMd")
3666
+ })
3667
+ ]
3668
+ })
3669
+ ]
3670
+ })
3671
+ ]
3672
+ })
3673
+ ]
3674
+ });
3675
+ }
3676
+ //#endregion
3677
+ //#region src/client/mount.tsx
3678
+ /**
3679
+ * Panel view mounting — mirrors the family plugins: a container appended
3680
+ * inside the conversation grid item, hidden while inactive; toggling is a
3681
+ * data attribute on <html>, with cross-plugin activation events.
3682
+ */
3683
+ const CONVERSATION_COLUMN_SELECTOR = "[data-pane=\"conversation\"]";
3684
+ const ACTIVE_ATTR = "data-dsh-novelforge-active";
3685
+ /** Sibling panels' activation attributes (evicted when this panel opens). */
3686
+ const OTHER_ACTIVE_ATTRS = ["data-dsh-taskboard-active", "data-dsh-ssh-active"];
3687
+ /** Cross-plugin activation event; detail is the activating panel name. */
3688
+ const ACTIVATE_EVENT = "dsh-panel-activate";
3689
+ const PANEL_NAME = "novelforge";
3690
+ /** Find the center column, or undefined while the frame is not mounted. */
3691
+ function conversationColumn() {
3692
+ return document.querySelector(CONVERSATION_COLUMN_SELECTOR) ?? void 0;
3693
+ }
3694
+ /**
3695
+ * Mount the panel React tree into the center column and bind visibility to
3696
+ * the controller.
3697
+ */
3698
+ function mountPanel(controller, api) {
3699
+ let root;
3700
+ let container;
3701
+ const ensure = () => {
3702
+ if (container !== void 0) {
3703
+ if (container.isConnected) return;
3704
+ root?.unmount();
3705
+ root = void 0;
3706
+ container.remove();
3707
+ container = void 0;
3708
+ }
3709
+ const column = conversationColumn();
3710
+ if (column === void 0) return;
3711
+ container = document.createElement("div");
3712
+ container.dataset.dshNovelforgeView = "true";
3713
+ container.className = panel_module_css_default.view;
3714
+ column.appendChild(container);
3715
+ root = (0, react_dom_client.createRoot)(container);
3716
+ root.render(/* @__PURE__ */ (0, react_jsx_runtime.jsx)(NovelPanel, {
3717
+ controller,
3718
+ api
3719
+ }));
3720
+ };
3721
+ const waitObserver = new MutationObserver(() => {
3722
+ ensure();
3723
+ });
3724
+ waitObserver.observe(document.body, {
3725
+ childList: true,
3726
+ subtree: true
3727
+ });
3728
+ const applyActive = () => {
3729
+ if (controller.getSnapshot().panelOpen) {
3730
+ for (const attr of OTHER_ACTIVE_ATTRS) document.documentElement.removeAttribute(attr);
3731
+ document.documentElement.setAttribute(ACTIVE_ATTR, "");
3732
+ document.dispatchEvent(new CustomEvent(ACTIVATE_EVENT, { detail: PANEL_NAME }));
3733
+ } else document.documentElement.removeAttribute(ACTIVE_ATTR);
3734
+ };
3735
+ const onOtherActivate = (event) => {
3736
+ const detail = event.detail;
3737
+ if ((detail === "taskboard" || detail === "ssh") && controller.getSnapshot().panelOpen) controller.close();
3738
+ };
3739
+ const SIDEBAR_ROW_SELECTOR = "[class*=\"sessionRow\"], [class*=\"projectRow\"], [class*=\"searchResultRow\"], [class*=\"searchResultWorkspace\"], [class*=\"newSession\"]";
3740
+ const onClickSidebarRow = (event) => {
3741
+ if (!controller.getSnapshot().panelOpen) return;
3742
+ const target = event.target;
3743
+ if (target === null) return;
3744
+ if (target.closest(SIDEBAR_ROW_SELECTOR) !== null) controller.close();
3745
+ };
3746
+ document.addEventListener("click", onClickSidebarRow, true);
3747
+ document.addEventListener(ACTIVATE_EVENT, onOtherActivate);
3748
+ const unsubscribe = controller.subscribe(applyActive);
3749
+ applyActive();
3750
+ ensure();
3751
+ return () => {
3752
+ document.removeEventListener("click", onClickSidebarRow, true);
3753
+ document.removeEventListener(ACTIVATE_EVENT, onOtherActivate);
3754
+ waitObserver.disconnect();
3755
+ unsubscribe();
3756
+ document.documentElement.removeAttribute(ACTIVE_ATTR);
3757
+ root?.unmount();
3758
+ root = void 0;
3759
+ container?.remove();
3760
+ container = void 0;
3761
+ };
3762
+ }
3763
+ //#endregion
3764
+ //#region src/client/panel/controller.ts
3765
+ /** The panel state owner the sidebar entry toggles and the view renders from. */
3766
+ var PanelController = class {
3767
+ panelOpen = false;
3768
+ listeners = /* @__PURE__ */ new Set();
3769
+ getSnapshot() {
3770
+ return { panelOpen: this.panelOpen };
3771
+ }
3772
+ subscribe(fn) {
3773
+ this.listeners.add(fn);
3774
+ return () => {
3775
+ this.listeners.delete(fn);
3776
+ };
3777
+ }
3778
+ open() {
3779
+ if (this.panelOpen) return;
3780
+ this.panelOpen = true;
3781
+ this.notify();
3782
+ }
3783
+ close() {
3784
+ if (!this.panelOpen) return;
3785
+ this.panelOpen = false;
3786
+ this.notify();
3787
+ }
3788
+ toggle() {
3789
+ if (this.panelOpen) this.close();
3790
+ else this.open();
3791
+ }
3792
+ notify() {
3793
+ for (const fn of [...this.listeners]) fn();
3794
+ }
3795
+ };
3796
+ //#endregion
3797
+ //#region src/client/sidebar-entry.ts
3798
+ /** Find the sidebar shell root element. */
3799
+ function sidebarRoot() {
3800
+ const column = document.querySelector("[data-pane=\"sidebar\"], [class*=\"sidebarCol\"]");
3801
+ if (column === null) return void 0;
3802
+ return column.querySelector("[class*=\"logoRow\"]")?.parentElement ?? column.firstElementChild;
3803
+ }
3804
+ /** The New Session button (nested in the logo row on current shells). */
3805
+ function newSessionButton(root) {
3806
+ const nested = root.querySelector("button[class*=\"newSession\"]");
3807
+ if (nested !== null) return nested;
3808
+ for (const child of root.children) if (child.tagName === "BUTTON") return child;
3809
+ }
3810
+ /** Build the entry row (a detached button; insert once the shell is up). */
3811
+ function createEntry(controller) {
3812
+ const entry = document.createElement("button");
3813
+ entry.type = "button";
3814
+ entry.dataset.dshNovelforgeEntry = "true";
3815
+ entry.className = panel_module_css_default.entry;
3816
+ entry.setAttribute("aria-label", tt("entry.label"));
3817
+ entry.setAttribute("title", tt("entry.tooltip"));
3818
+ entry.innerHTML = "<span class=\"" + panel_module_css_default.entryIcon + "\"><svg viewBox=\"0 0 16 16\" width=\"14\" height=\"14\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.3\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M2.5 3.5h4a2 2 0 0 1 2 2v7a2 2 0 0 0-2-2h-4z\"/><path d=\"M13.5 3.5h-4a2 2 0 0 0-2 2v7a2 2 0 0 1 2-2h4z\"/><path d=\"M8 5.5v7\"/></svg></span><span class=\"" + panel_module_css_default.entryLabel + "\">" + tt("entry.label") + "</span>";
3819
+ entry.addEventListener("click", () => {
3820
+ controller.toggle();
3821
+ });
3822
+ return entry;
3823
+ }
3824
+ /** Re-insert the entry after the sibling plugin entry block. */
3825
+ function placeEntry(root, entry) {
3826
+ const button = newSessionButton(root);
3827
+ if (button === void 0) return false;
3828
+ if (entry.parentElement !== root) {
3829
+ const row = button.closest("[class*=\"logoRow\"]");
3830
+ const base = row !== null && row.parentElement === root ? row : button;
3831
+ const family = Array.from(root.children).filter((el) => el instanceof HTMLElement && el.matches("[data-dsh-taskboard-entry], [data-dsh-ssh-entry], [data-dsh-novelforge-entry]"));
3832
+ const last = family.length > 0 ? family[family.length - 1] : void 0;
3833
+ const anchor = last !== void 0 ? last.nextElementSibling : base.nextElementSibling;
3834
+ root.insertBefore(entry, anchor);
3835
+ }
3836
+ return true;
3837
+ }
3838
+ /**
3839
+ * Mount the sidebar entry, waiting for the shell and self-healing on
3840
+ * re-renders.
3841
+ */
3842
+ function mountSidebarEntry(controller) {
3843
+ const entry = createEntry(controller);
3844
+ let root;
3845
+ let placed = false;
3846
+ const tryPlace = () => {
3847
+ if (root !== void 0 && !root.isConnected) {
3848
+ rootObserver.disconnect();
3849
+ root = void 0;
3850
+ placed = false;
3851
+ }
3852
+ if (placed) {
3853
+ if (document.body.contains(entry)) return;
3854
+ rootObserver.disconnect();
3855
+ root = void 0;
3856
+ placed = false;
3857
+ }
3858
+ root ??= sidebarRoot();
3859
+ if (root === void 0) return;
3860
+ placed = placeEntry(root, entry);
3861
+ if (placed) rootObserver.observe(root, {
3862
+ childList: true,
3863
+ subtree: true
3864
+ });
3865
+ };
3866
+ const waitObserver = new MutationObserver(() => {
3867
+ tryPlace();
3868
+ });
3869
+ waitObserver.observe(document.body, {
3870
+ childList: true,
3871
+ subtree: true
3872
+ });
3873
+ const rootObserver = new MutationObserver(() => {
3874
+ if (root === void 0 || !root.isConnected) {
3875
+ placed = false;
3876
+ tryPlace();
3877
+ return;
3878
+ }
3879
+ if (!root.contains(entry)) placed = placeEntry(root, entry);
3880
+ });
3881
+ const syncActive = () => {
3882
+ if (controller.getSnapshot().panelOpen) entry.dataset.active = "true";
3883
+ else delete entry.dataset.active;
3884
+ };
3885
+ const unsubscribe = controller.subscribe(syncActive);
3886
+ syncActive();
3887
+ tryPlace();
3888
+ return () => {
3889
+ waitObserver.disconnect();
3890
+ rootObserver.disconnect();
3891
+ unsubscribe();
3892
+ entry.remove();
3893
+ };
3894
+ }
3895
+ //#endregion
3896
+ //#region src/client/index.ts
3897
+ /** Required services (fiber inject waiting). */
3898
+ const inject = ["slots", "locale"];
3899
+ /**
3900
+ * Mount the novel-forge workbench.
3901
+ * @param ctx - client root context.
3902
+ */
3903
+ function apply(ctx) {
3904
+ const controller = new PanelController();
3905
+ const api = new NovelApi();
3906
+ const disposers = [];
3907
+ try {
3908
+ disposers.push(mountSidebarEntry(controller));
3909
+ disposers.push(mountPanel(controller, api));
3910
+ } catch (error) {
3911
+ console.warn("[dsh-novel-forge] mount failed:", error);
3912
+ }
3913
+ ctx.effect(() => () => {
3914
+ for (const dispose of disposers.splice(0)) dispose();
3915
+ }, "dsh-novel-forge: ui mounts");
3916
+ }
3917
+ //#endregion
3918
+ exports.apply = apply;
3919
+ exports.inject = inject;
3920
+ return module.exports;
3921
+ }
3922
+ });
3923
+
3924
+ //# sourceMappingURL=client.js.map