dsh-novel-write-engine 0.2.8 → 0.2.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/README.md +20 -18
- package/briefs/P3.md +1 -0
- package/briefs/P4.md +1 -1
- package/briefs/P8.md +1 -1
- package/lib/client.js +915 -117
- package/lib/index.d.ts +83 -8
- package/lib/index.js +1246 -225
- package/lib/types/client/dto.d.ts +12 -9
- package/lib/types/client/panel.d.ts +19 -1
- package/lib/types/client/viewmodel.d.ts +11 -4
- package/lib/types/constants.d.ts +2 -0
- package/lib/types/panel-contract.d.ts +2 -2
- package/lib/types/types.d.ts +18 -1
- package/package.json +8 -3
- package/presets/novel-write-engine.patch.yml +4 -0
- package/rulepacks/default.yaml +13 -5
- package/skills/entry.md +2 -1
package/lib/client.js
CHANGED
|
@@ -5,9 +5,12 @@ window.__ModuleLoader__.load({
|
|
|
5
5
|
var exports = module.exports;
|
|
6
6
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
7
|
let react = require("react");
|
|
8
|
+
let react_dom = require("react-dom");
|
|
8
9
|
// src/constants.ts
|
|
9
10
|
const PANEL_ID = "novel-workshop";
|
|
10
11
|
const PANEL_ROUTE = "/api/novel-write-engine/panel";
|
|
12
|
+
/** User-visible panel/tutorial version; keep in sync with package.json. */
|
|
13
|
+
const PANEL_VERSION = "0.2.9";
|
|
11
14
|
// src/next-reasons.ts
|
|
12
15
|
/**
|
|
13
16
|
* Chinese one-line explanation for the tool each engine state recommends.
|
|
@@ -37,6 +40,9 @@ window.__ModuleLoader__.load({
|
|
|
37
40
|
novel_rollback: "回退到更早阶段重建当前章。",
|
|
38
41
|
novel_amend: "按明确的修复理由修正一份状态文档。",
|
|
39
42
|
novel_audit: "先查看事务日志,确认并恢复损坏的历史。",
|
|
43
|
+
novel_restore: "按备份清单恢复损坏的状态文档或当前章。",
|
|
44
|
+
novel_state: "查看状态文档、大纲、世界观或历史原文。",
|
|
45
|
+
novel_pipeline: "按当前阶段领取下一步工具与参数。",
|
|
40
46
|
novel_export: "导出成书或书级提案。",
|
|
41
47
|
novel_diff: "对比当前正文与上一版或给定文本。",
|
|
42
48
|
novel_word_count: "按引擎字数口径核对正文长度。"
|
|
@@ -115,6 +121,7 @@ window.__ModuleLoader__.load({
|
|
|
115
121
|
damaged: false,
|
|
116
122
|
stageLabel: "读取中",
|
|
117
123
|
finished: false,
|
|
124
|
+
chapter: 0,
|
|
118
125
|
revisionLabel: "",
|
|
119
126
|
phases: [],
|
|
120
127
|
rows: [],
|
|
@@ -152,7 +159,7 @@ window.__ModuleLoader__.load({
|
|
|
152
159
|
},
|
|
153
160
|
quality: {
|
|
154
161
|
averageScore: 0,
|
|
155
|
-
lastTenAverage:
|
|
162
|
+
lastTenAverage: null,
|
|
156
163
|
bookMedian: 0,
|
|
157
164
|
bookMin: 0,
|
|
158
165
|
bookMax: 0,
|
|
@@ -304,15 +311,15 @@ window.__ModuleLoader__.load({
|
|
|
304
311
|
hint: item.hint ?? ""
|
|
305
312
|
}));
|
|
306
313
|
const rows = chapterRows.map((row) => {
|
|
307
|
-
const scoreValue =
|
|
314
|
+
const scoreValue = row.score === null ? null : Math.max(0, Math.min(150, Number.isFinite(row.score) ? row.score : 0));
|
|
308
315
|
return {
|
|
309
316
|
num: String(row.num),
|
|
310
317
|
title: row.title,
|
|
311
318
|
words: row.wordCount.toLocaleString("zh-CN"),
|
|
312
319
|
wordCount: row.wordCount,
|
|
313
|
-
score: `${scoreValue}/150`,
|
|
320
|
+
score: scoreValue === null ? "—" : `${scoreValue}/150`,
|
|
314
321
|
scoreValue,
|
|
315
|
-
scorePercent: Math.round(scoreValue / 150 * 100),
|
|
322
|
+
scorePercent: scoreValue === null ? null : Math.round(scoreValue / 150 * 100),
|
|
316
323
|
badge: row.status === "passed" ? "已归档" : row.status === "blocked" ? "阻塞" : "草稿",
|
|
317
324
|
tone: row.status === "passed" ? "ok" : row.status === "blocked" ? "bad" : "muted",
|
|
318
325
|
details: row.details ?? "",
|
|
@@ -328,8 +335,8 @@ window.__ModuleLoader__.load({
|
|
|
328
335
|
archivedAt: row.archivedAt ?? null
|
|
329
336
|
};
|
|
330
337
|
});
|
|
331
|
-
const scored = rows.filter((row) => row.tone === "ok");
|
|
332
|
-
const rowAverage = scored.length === 0 ? 0 : scored.reduce((sum, row) => sum + row.scoreValue, 0) / scored.length;
|
|
338
|
+
const scored = rows.filter((row) => row.tone === "ok" && row.scoreValue !== null);
|
|
339
|
+
const rowAverage = scored.length === 0 ? 0 : scored.reduce((sum, row) => sum + (row.scoreValue ?? 0), 0) / scored.length;
|
|
333
340
|
const average = summary.averageScore !== void 0 && Number.isFinite(summary.averageScore) ? summary.averageScore : rowAverage;
|
|
334
341
|
const next = summary.next;
|
|
335
342
|
const progress = summary.progress;
|
|
@@ -348,6 +355,7 @@ window.__ModuleLoader__.load({
|
|
|
348
355
|
subtitle: `${summary.genre} · ${platformLabel(summary.platform)} · ${progressLine}${volumeLabel(summary)}`,
|
|
349
356
|
stageLabel: doneLabel,
|
|
350
357
|
finished,
|
|
358
|
+
chapter: summary.chapter,
|
|
351
359
|
revisionLabel: `第 ${summary.bookData?.current.rounds.revise ?? summary.revision} 次修订`,
|
|
352
360
|
phases: summary.phases.map((phase) => ({
|
|
353
361
|
...phase,
|
|
@@ -396,7 +404,7 @@ window.__ModuleLoader__.load({
|
|
|
396
404
|
},
|
|
397
405
|
quality: {
|
|
398
406
|
averageScore: summary.bookData?.quality.average ?? summary.averageScore ?? average,
|
|
399
|
-
lastTenAverage: summary.bookData?.quality.last_ten_average ?? summary.quality?.
|
|
407
|
+
lastTenAverage: summary.bookData?.quality.last_ten_average ?? summary.quality?.last_ten_average ?? null,
|
|
400
408
|
bookMedian: summary.bookData?.quality.median ?? 0,
|
|
401
409
|
bookMin: summary.bookData?.quality.min ?? 0,
|
|
402
410
|
bookMax: summary.bookData?.quality.max ?? 0,
|
|
@@ -405,7 +413,7 @@ window.__ModuleLoader__.load({
|
|
|
405
413
|
trend: summary.bookData?.quality.trend ?? summary.quality?.trend ?? null,
|
|
406
414
|
trendText: (summary.bookData?.quality.trend ?? summary.quality?.trend) === null || (summary.bookData?.quality.trend ?? summary.quality?.trend) === void 0 ? summary.quality?.trend_basis === "insufficient_samples" || summary.bookData?.quality.trend_basis === "insufficient_samples" ? "样本不足" : summary.quality?.trend_basis === "insufficient_history" || summary.bookData?.quality.trend_basis === "insufficient_history" ? "历史不足" : "—" : `${(summary.bookData?.quality.trend ?? summary.quality.trend) >= 0 ? "+" : ""}${summary.bookData?.quality.trend ?? summary.quality.trend}`,
|
|
407
415
|
samples: summary.bookData?.quality.samples ?? summary.quality?.samples ?? 0,
|
|
408
|
-
attempts:
|
|
416
|
+
attempts: summary.bookData?.quality.attempts ?? summary.quality?.attempts ?? 0
|
|
409
417
|
},
|
|
410
418
|
health: {
|
|
411
419
|
ok: summary.health?.ok ?? blocking.length === 0,
|
|
@@ -432,12 +440,20 @@ window.__ModuleLoader__.load({
|
|
|
432
440
|
};
|
|
433
441
|
}
|
|
434
442
|
/** Fetch the panel payload; failures become a plain message, never a throw. */
|
|
435
|
-
async function fetchPanel(fetcher, path, signal) {
|
|
443
|
+
async function fetchPanel(fetcher, path, signal, etag) {
|
|
436
444
|
try {
|
|
437
445
|
const response = await fetcher(path, {
|
|
438
|
-
headers: {
|
|
446
|
+
headers: {
|
|
447
|
+
accept: "application/json",
|
|
448
|
+
...etag === void 0 ? {} : { "if-none-match": etag }
|
|
449
|
+
},
|
|
439
450
|
...signal === void 0 ? {} : { signal }
|
|
440
451
|
});
|
|
452
|
+
if (response.status === 304) return {
|
|
453
|
+
payload: null,
|
|
454
|
+
error: null,
|
|
455
|
+
notModified: true
|
|
456
|
+
};
|
|
441
457
|
if (!response.ok) {
|
|
442
458
|
let detail = `宿主返回 ${response.status}`;
|
|
443
459
|
try {
|
|
@@ -468,7 +484,9 @@ window.__ModuleLoader__.load({
|
|
|
468
484
|
--nwe-surface: var(--dsw-alias-bg-layer-1, #171a20);
|
|
469
485
|
--nwe-raised: var(--dsw-alias-bg-layer-2, #1e222a);
|
|
470
486
|
--nwe-border: var(--dsw-alias-border-l1, #ffffff14);
|
|
471
|
-
--nwe-gold: color-mix(in srgb,
|
|
487
|
+
--nwe-gold: var(--dsw-alias-label-accent, color-mix(in srgb, var(--nwe-ink) 58%, var(--nwe-raised)));
|
|
488
|
+
--nwe-accent: var(--nwe-gold);
|
|
489
|
+
--nwe-font: var(--dsw-font-family, "Noto Sans SC", "PingFang SC", "Microsoft YaHei", system-ui, sans-serif);
|
|
472
490
|
--nwe-jade: color-mix(in srgb, #4f9f82 68%, var(--nwe-ink));
|
|
473
491
|
--nwe-rose: color-mix(in srgb, #c96a6a 68%, var(--nwe-ink));
|
|
474
492
|
position: relative;
|
|
@@ -485,7 +503,7 @@ window.__ModuleLoader__.load({
|
|
|
485
503
|
margin: 0 auto;
|
|
486
504
|
padding: clamp(18px, 3.2vw, 38px);
|
|
487
505
|
color: var(--nwe-ink);
|
|
488
|
-
font-family:
|
|
506
|
+
font-family: var(--nwe-font);
|
|
489
507
|
line-height: 1.55;
|
|
490
508
|
}
|
|
491
509
|
.nwe-root * { box-sizing: border-box; }
|
|
@@ -512,7 +530,7 @@ window.__ModuleLoader__.load({
|
|
|
512
530
|
}
|
|
513
531
|
.nwe-eyebrow {
|
|
514
532
|
color: var(--nwe-gold);
|
|
515
|
-
font-size:
|
|
533
|
+
font-size: 12px;
|
|
516
534
|
font-weight: 600;
|
|
517
535
|
letter-spacing: .22em;
|
|
518
536
|
text-transform: uppercase;
|
|
@@ -541,7 +559,7 @@ window.__ModuleLoader__.load({
|
|
|
541
559
|
border: 1px solid color-mix(in srgb, var(--nwe-gold) 50%, transparent);
|
|
542
560
|
border-radius: 999px;
|
|
543
561
|
color: var(--nwe-gold);
|
|
544
|
-
font-size:
|
|
562
|
+
font-size: 12px;
|
|
545
563
|
font-weight: 600;
|
|
546
564
|
}
|
|
547
565
|
.nwe-stage::before { content: ""; width: 5px; height: 5px; border-radius: 50%; background: currentColor; }
|
|
@@ -565,7 +583,7 @@ window.__ModuleLoader__.load({
|
|
|
565
583
|
.nwe-refresh:disabled { cursor: wait; opacity: .55; }
|
|
566
584
|
.nwe-actions { display: flex; align-items: center; gap: 8px; flex: 0 0 auto; flex-wrap: wrap; }
|
|
567
585
|
.nwe-project { display: flex; align-items: center; gap: 6px; min-width: 0; }
|
|
568
|
-
.nwe-project-label { color: var(--nwe-muted); font-size:
|
|
586
|
+
.nwe-project-label { color: var(--nwe-muted); font-size: 12px; white-space: nowrap; }
|
|
569
587
|
.nwe-project-select {
|
|
570
588
|
max-width: 260px;
|
|
571
589
|
min-height: 36px;
|
|
@@ -577,7 +595,7 @@ window.__ModuleLoader__.load({
|
|
|
577
595
|
font: inherit;
|
|
578
596
|
font-size: 12px;
|
|
579
597
|
}
|
|
580
|
-
.nwe-rootline { margin-top: 5px; color: var(--nwe-muted); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size:
|
|
598
|
+
.nwe-rootline { margin-top: 5px; color: var(--nwe-muted); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; overflow-wrap: anywhere; }
|
|
581
599
|
.nwe-damaged { margin-top: 10px; padding: 9px 12px; border: 1px solid color-mix(in srgb, var(--nwe-rose) 36%, var(--nwe-border)); border-radius: 10px; color: var(--nwe-rose); background: color-mix(in srgb, var(--nwe-rose) 7%, transparent); font-size: 12px; }
|
|
582
600
|
.nwe-help {
|
|
583
601
|
display: inline-flex;
|
|
@@ -606,7 +624,7 @@ window.__ModuleLoader__.load({
|
|
|
606
624
|
align-items: center;
|
|
607
625
|
justify-content: center;
|
|
608
626
|
padding: clamp(12px, 3.5vw, 42px);
|
|
609
|
-
background: color-mix(in srgb, #
|
|
627
|
+
background: color-mix(in srgb, #000 76%, transparent);
|
|
610
628
|
backdrop-filter: blur(7px);
|
|
611
629
|
}
|
|
612
630
|
.nwe-modal {
|
|
@@ -670,27 +688,49 @@ window.__ModuleLoader__.load({
|
|
|
670
688
|
.nwe-tutorial-cards { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; }
|
|
671
689
|
.nwe-tutorial-card { padding: 11px 12px; border: 1px solid var(--nwe-border); border-radius: 11px; background: color-mix(in srgb, var(--nwe-raised) 52%, transparent); }
|
|
672
690
|
.nwe-tutorial-card strong { display: block; color: var(--nwe-gold); font-size: 12px; }
|
|
673
|
-
.nwe-tutorial-card span { display: block; margin-top: 4px; color: var(--nwe-muted); font-size:
|
|
691
|
+
.nwe-tutorial-card span { display: block; margin-top: 4px; color: var(--nwe-muted); font-size: 12px; line-height: 1.65; }
|
|
674
692
|
.nwe-steps { display: flex; flex-direction: column; gap: 8px; margin: 0; padding: 0; list-style: none; }
|
|
675
693
|
.nwe-step { padding: 12px 14px; border: 1px solid var(--nwe-border); border-left: 2px solid color-mix(in srgb, var(--nwe-gold) 58%, var(--nwe-border)); border-radius: 11px; background: color-mix(in srgb, var(--nwe-surface) 86%, var(--nwe-raised) 14%); }
|
|
676
694
|
.nwe-step-head { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; }
|
|
677
|
-
.nwe-step-no { color: var(--nwe-gold); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size:
|
|
695
|
+
.nwe-step-no { color: var(--nwe-gold); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; letter-spacing: .06em; }
|
|
678
696
|
.nwe-step h4 { margin: 0; font-size: 13px; font-weight: 600; }
|
|
679
|
-
.nwe-step-tool { margin-left: auto; padding: 2px 6px; border: 1px solid var(--nwe-border); border-radius: 5px; color: var(--nwe-gold); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size:
|
|
697
|
+
.nwe-step-tool { margin-left: auto; padding: 2px 6px; border: 1px solid var(--nwe-border); border-radius: 5px; color: var(--nwe-gold); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; white-space: nowrap; }
|
|
680
698
|
.nwe-step p { margin: 6px 0 0; color: var(--nwe-muted); font-size: 12px; line-height: 1.7; }
|
|
681
699
|
.nwe-step p strong { color: var(--nwe-ink); font-weight: 600; }
|
|
682
|
-
.nwe-tutorial-note { margin: 12px 0 0; padding: 10px 12px; border-radius: 9px; background: color-mix(in srgb, var(--nwe-gold) 8%, transparent); color: var(--nwe-muted); font-size:
|
|
700
|
+
.nwe-tutorial-note { margin: 12px 0 0; padding: 10px 12px; border-radius: 9px; background: color-mix(in srgb, var(--nwe-gold) 8%, transparent); color: var(--nwe-muted); font-size: 12px; line-height: 1.7; }
|
|
683
701
|
.nwe-tutorial-note strong { color: var(--nwe-gold); }
|
|
684
702
|
.nwe-faq { display: flex; flex-direction: column; gap: 8px; margin: 0; padding: 0; list-style: none; }
|
|
685
703
|
.nwe-faq li { padding: 10px 12px; border: 1px solid var(--nwe-border); border-radius: 10px; }
|
|
686
704
|
.nwe-faq strong { display: block; color: var(--nwe-ink); font-size: 12px; }
|
|
687
705
|
.nwe-faq p { margin: 4px 0 0; color: var(--nwe-muted); font-size: 12px; line-height: 1.65; }
|
|
688
|
-
.nwe-
|
|
706
|
+
.nwe-copy-block { margin-top: 0; }
|
|
707
|
+
.nwe-copy-row { display: flex; align-items: center; gap: 10px; margin-top: 7px; }
|
|
708
|
+
.nwe-copy-status { color: var(--nwe-muted); font-size: 12px; }
|
|
709
|
+
.nwe-prompt { overflow-x: auto; margin: 0; padding: 12px 14px; border: 1px solid var(--nwe-border); border-radius: 10px; background: color-mix(in srgb, #000 24%, var(--nwe-surface)); color: var(--nwe-ink); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; line-height: 1.75; white-space: pre-wrap; }
|
|
689
710
|
.nwe-checklist { display: grid; gap: 6px; margin: 0; padding: 0; list-style: none; }
|
|
690
711
|
.nwe-checklist li { display: flex; gap: 7px; color: var(--nwe-muted); font-size: 12px; line-height: 1.6; }
|
|
691
712
|
.nwe-checklist li::before { content: "✓"; color: var(--nwe-jade); font-weight: 700; }
|
|
692
|
-
.nwe-modal-foot { flex: 0 0 auto; padding: 10px 24px; border-top: 1px solid var(--nwe-border); color: var(--nwe-muted); font-size:
|
|
713
|
+
.nwe-modal-foot { flex: 0 0 auto; padding: 10px 24px; border-top: 1px solid var(--nwe-border); color: var(--nwe-muted); font-size: 12px; }
|
|
693
714
|
.nwe-help-label { white-space: nowrap; }
|
|
715
|
+
.nwe-tabs { display: flex; flex-wrap: wrap; gap: 4px; margin: 18px 0 0; padding: 4px; border: 1px solid var(--nwe-border); border-radius: 12px; background: color-mix(in srgb, var(--nwe-raised) 65%, transparent); }
|
|
716
|
+
.nwe-tab { padding: 7px 13px; border: 0; border-radius: 8px; background: transparent; color: var(--nwe-muted); font: inherit; font-size: 12px; cursor: pointer; }
|
|
717
|
+
.nwe-tab.is-active { background: color-mix(in srgb, var(--nwe-gold) 14%, transparent); color: var(--nwe-ink); }
|
|
718
|
+
.nwe-tab:focus-visible { outline: 2px solid var(--nwe-accent); outline-offset: 2px; }
|
|
719
|
+
.nwe-section-list { display: grid; gap: 7px; }
|
|
720
|
+
.nwe-data-row { border: 1px solid var(--nwe-border); border-radius: 9px; background: color-mix(in srgb, var(--nwe-raised) 55%, transparent); }
|
|
721
|
+
.nwe-data-row > summary { cursor: pointer; padding: 9px 11px; font-size: 12px; overflow-wrap: anywhere; }
|
|
722
|
+
.nwe-data-json { margin: 0; padding: 0 11px 11px; color: var(--nwe-muted); font-size: 12px; white-space: pre-wrap; overflow-wrap: anywhere; }
|
|
723
|
+
.nwe-subsection { margin: 14px 0 0; }
|
|
724
|
+
.nwe-subsection > h3 { margin: 0 0 6px; color: var(--nwe-muted); font-size: 12px; font-weight: 600; letter-spacing: .04em; }
|
|
725
|
+
.nwe-kv { display: grid; grid-template-columns: minmax(72px, max-content) minmax(0, 1fr); gap: 4px 12px; margin: 0; padding: 0 11px 11px; font-size: 12px; }
|
|
726
|
+
.nwe-kv dt { color: var(--nwe-muted); }
|
|
727
|
+
.nwe-kv dd { margin: 0; overflow-wrap: anywhere; }
|
|
728
|
+
.nwe-quality-summary { display: flex; flex-wrap: wrap; gap: 8px 18px; margin-bottom: 6px; color: var(--nwe-muted); font-size: 12px; }
|
|
729
|
+
.nwe-dim { display: grid; grid-template-columns: 72px minmax(0, 1fr) 76px; align-items: center; gap: 9px; font-size: 12px; }
|
|
730
|
+
.nwe-dim > span:last-child { color: var(--nwe-muted); text-align: right; font-variant-numeric: tabular-nums; }
|
|
731
|
+
.nwe-dim .nwe-v6-bar { margin: 0; }
|
|
732
|
+
.nwe-health-flags { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 6px; }
|
|
733
|
+
.nwe-empty { padding: 18px; border: 1px dashed var(--nwe-border); border-radius: 10px; color: var(--nwe-muted); font-size: 12px; }
|
|
694
734
|
.nwe-stats {
|
|
695
735
|
display: grid;
|
|
696
736
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
@@ -703,7 +743,7 @@ window.__ModuleLoader__.load({
|
|
|
703
743
|
border-radius: 14px;
|
|
704
744
|
background: color-mix(in srgb, var(--nwe-surface) 88%, var(--nwe-gold) 4%);
|
|
705
745
|
}
|
|
706
|
-
.nwe-stat-label { color: var(--nwe-muted); font-size:
|
|
746
|
+
.nwe-stat-label { color: var(--nwe-muted); font-size: 12px; letter-spacing: .04em; }
|
|
707
747
|
.nwe-stat-value {
|
|
708
748
|
margin-top: 3px;
|
|
709
749
|
font-size: 22px;
|
|
@@ -711,7 +751,7 @@ window.__ModuleLoader__.load({
|
|
|
711
751
|
font-weight: 600;
|
|
712
752
|
letter-spacing: -.02em;
|
|
713
753
|
}
|
|
714
|
-
.nwe-stat-unit { margin-left: 2px; color: var(--nwe-muted); font-size:
|
|
754
|
+
.nwe-stat-unit { margin-left: 2px; color: var(--nwe-muted); font-size: 12px; font-weight: 400; }
|
|
715
755
|
.nwe-section { margin-top: 26px; }
|
|
716
756
|
.nwe-section-head { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; }
|
|
717
757
|
.nwe-section-title {
|
|
@@ -720,7 +760,7 @@ window.__ModuleLoader__.load({
|
|
|
720
760
|
font-size: 17px;
|
|
721
761
|
font-weight: 600;
|
|
722
762
|
}
|
|
723
|
-
.nwe-section-note { color: var(--nwe-muted); font-size:
|
|
763
|
+
.nwe-section-note { color: var(--nwe-muted); font-size: 12px; }
|
|
724
764
|
.nwe-phases {
|
|
725
765
|
display: grid;
|
|
726
766
|
grid-template-columns: repeat(9, minmax(0, 1fr));
|
|
@@ -754,37 +794,37 @@ window.__ModuleLoader__.load({
|
|
|
754
794
|
border-radius: 50%;
|
|
755
795
|
background: var(--nwe-surface);
|
|
756
796
|
color: transparent;
|
|
757
|
-
font-size:
|
|
797
|
+
font-size: 12px;
|
|
758
798
|
font-weight: 700;
|
|
759
799
|
}
|
|
760
800
|
.nwe-phase.is-done .nwe-dot { border-color: color-mix(in srgb, var(--nwe-jade) 70%, transparent); background: color-mix(in srgb, var(--nwe-jade) 18%, var(--nwe-surface)); color: var(--nwe-jade); }
|
|
761
801
|
.nwe-phase.is-active .nwe-dot { border-color: var(--nwe-gold); background: var(--nwe-gold); color: #19150d; box-shadow: 0 0 0 4px color-mix(in srgb, var(--nwe-gold) 16%, transparent); }
|
|
762
|
-
.nwe-phase-label { overflow: hidden; max-width: 100%; color: var(--nwe-muted); font-size:
|
|
802
|
+
.nwe-phase-label { overflow: hidden; max-width: 100%; color: var(--nwe-muted); font-size: 12px; text-overflow: ellipsis; white-space: nowrap; }
|
|
763
803
|
.nwe-phase.is-active .nwe-phase-label { color: var(--nwe-ink); font-weight: 600; }
|
|
764
804
|
.nwe-book-grid { display: grid; grid-template-columns: minmax(0, 1.35fr) minmax(0, 1fr); gap: 10px; margin-top: 12px; }
|
|
765
805
|
.nwe-book-card { padding: 14px 16px; border: 1px solid var(--nwe-border); border-radius: 14px; background: color-mix(in srgb, var(--nwe-surface) 92%, transparent); }
|
|
766
806
|
.nwe-book-head { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; }
|
|
767
807
|
.nwe-book-title { font-family: "Noto Serif SC", "Songti SC", Georgia, serif; font-size: 14px; font-weight: 600; }
|
|
768
|
-
.nwe-book-note { color: var(--nwe-muted); font-size:
|
|
808
|
+
.nwe-book-note { color: var(--nwe-muted); font-size: 12px; }
|
|
769
809
|
.nwe-progress { overflow: hidden; height: 7px; margin-top: 10px; border-radius: 99px; background: var(--nwe-border); }
|
|
770
810
|
.nwe-progress > span { display: block; height: 100%; border-radius: inherit; background: linear-gradient(90deg, var(--nwe-jade), var(--nwe-gold)); }
|
|
771
|
-
.nwe-progress-legend { display: flex; justify-content: space-between; margin-top: 5px; color: var(--nwe-muted); font-size:
|
|
811
|
+
.nwe-progress-legend { display: flex; justify-content: space-between; margin-top: 5px; color: var(--nwe-muted); font-size: 12px; font-variant-numeric: tabular-nums; }
|
|
772
812
|
.nwe-volumes { max-height: 190px; overflow-y: auto; overscroll-behavior: contain; padding-right: 3px; }
|
|
773
|
-
.nwe-volume { display: flex; justify-content: space-between; gap: 8px; margin-top: 7px; color: var(--nwe-muted); font-size:
|
|
813
|
+
.nwe-volume { display: flex; justify-content: space-between; gap: 8px; margin-top: 7px; color: var(--nwe-muted); font-size: 12px; }
|
|
774
814
|
.nwe-volume strong { overflow: hidden; color: var(--nwe-ink); font-weight: 500; text-overflow: ellipsis; white-space: nowrap; }
|
|
775
815
|
.nwe-volume.is-current strong { color: var(--nwe-gold); }
|
|
776
|
-
.nwe-copy { margin-top: 8px; padding: 6px 10px; border: 1px solid var(--nwe-border); border-radius: 8px; background: var(--nwe-raised); color: var(--nwe-muted); font: inherit; font-size:
|
|
816
|
+
.nwe-copy { margin-top: 8px; padding: 6px 10px; border: 1px solid var(--nwe-border); border-radius: 8px; background: var(--nwe-raised); color: var(--nwe-muted); font: inherit; font-size: 12px; cursor: pointer; }
|
|
777
817
|
.nwe-copy:hover { border-color: color-mix(in srgb, var(--nwe-gold) 65%, var(--nwe-border)); color: var(--nwe-ink); }
|
|
778
|
-
.nwe-resume-fallback { margin-top: 6px; padding: 8px 10px; border: 1px solid var(--nwe-border); border-radius: 8px; background: color-mix(in srgb, #
|
|
818
|
+
.nwe-resume-fallback { margin-top: 6px; padding: 8px 10px; border: 1px solid var(--nwe-border); border-radius: 8px; background: color-mix(in srgb, #000 24%, var(--nwe-surface)); color: var(--nwe-muted); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; line-height: 1.6; white-space: pre-wrap; }
|
|
779
819
|
.nwe-chips { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 10px; }
|
|
780
|
-
.nwe-chip { padding: 3px 8px; border: 1px solid var(--nwe-border); border-radius: 999px; color: var(--nwe-muted); font-size:
|
|
820
|
+
.nwe-chip { padding: 3px 8px; border: 1px solid var(--nwe-border); border-radius: 999px; color: var(--nwe-muted); font-size: 12px; font-variant-numeric: tabular-nums; }
|
|
781
821
|
.nwe-bars { display: flex; align-items: flex-end; gap: 5px; height: 54px; margin-top: 10px; }
|
|
782
822
|
.nwe-bar { flex: 1 1 0; min-width: 4px; border-radius: 4px 4px 2px 2px; background: linear-gradient(180deg, var(--nwe-gold), color-mix(in srgb, var(--nwe-gold) 35%, transparent)); }
|
|
783
|
-
.nwe-health { display: flex; align-items: center; gap: 8px; margin-top: 10px; color: var(--nwe-muted); font-size:
|
|
823
|
+
.nwe-health { display: flex; align-items: center; gap: 8px; margin-top: 10px; color: var(--nwe-muted); font-size: 12px; }
|
|
784
824
|
.nwe-health.is-bad { color: var(--nwe-rose); }
|
|
785
825
|
.nwe-activity { display: flex; flex-direction: column; gap: 6px; margin: 10px 0 0; padding: 0; list-style: none; }
|
|
786
|
-
.nwe-activity li { display: grid; grid-template-columns: 96px minmax(0, 1fr) auto; gap: 8px; color: var(--nwe-muted); font-size:
|
|
787
|
-
.nwe-activity code { color: var(--nwe-ink); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size:
|
|
826
|
+
.nwe-activity li { display: grid; grid-template-columns: 96px minmax(0, 1fr) auto; gap: 8px; color: var(--nwe-muted); font-size: 12px; }
|
|
827
|
+
.nwe-activity code { color: var(--nwe-ink); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; }
|
|
788
828
|
.nwe-next {
|
|
789
829
|
display: flex;
|
|
790
830
|
align-items: flex-start;
|
|
@@ -797,11 +837,27 @@ window.__ModuleLoader__.load({
|
|
|
797
837
|
background: linear-gradient(90deg, color-mix(in srgb, var(--nwe-gold) 9%, transparent), transparent 72%), var(--nwe-surface);
|
|
798
838
|
}
|
|
799
839
|
.nwe-next-icon { display: grid; place-items: center; flex: 0 0 auto; width: 28px; height: 28px; border-radius: 9px; background: color-mix(in srgb, var(--nwe-gold) 15%, transparent); color: var(--nwe-gold); }
|
|
800
|
-
.nwe-next-kicker { color: var(--nwe-gold); font-size:
|
|
840
|
+
.nwe-next-kicker { color: var(--nwe-gold); font-size: 12px; font-weight: 600; letter-spacing: .16em; text-transform: uppercase; }
|
|
801
841
|
.nwe-next-title { margin-top: 2px; font-size: 15px; font-weight: 600; }
|
|
802
842
|
.nwe-next-detail { margin-top: 2px; color: var(--nwe-muted); font-size: 12px; }
|
|
803
|
-
.nwe-next-tool { margin-left: auto; align-self: center; flex: 0 0 auto; color: var(--nwe-muted); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size:
|
|
843
|
+
.nwe-next-tool { margin-left: auto; align-self: center; flex: 0 0 auto; color: var(--nwe-muted); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; }
|
|
804
844
|
.nwe-chapters { display: flex; flex-direction: column; gap: 9px; margin: 12px 0 0; padding: 0; list-style: none; }
|
|
845
|
+
.nwe-chapter-toolbar { display: flex; flex-wrap: wrap; align-items: end; gap: 8px; margin: 12px 0 0; padding: 10px 12px; border: 1px solid var(--nwe-border); border-radius: 10px; background: color-mix(in srgb, var(--nwe-raised) 55%, transparent); }
|
|
846
|
+
.nwe-chapter-toolbar label { display: grid; gap: 3px; color: var(--nwe-muted); font-size: 12px; }
|
|
847
|
+
.nwe-chapter-toolbar input, .nwe-chapter-toolbar select { min-height: 30px; padding: 4px 7px; border: 1px solid var(--nwe-border); border-radius: 7px; color: var(--nwe-ink); background: var(--nwe-surface); font: inherit; font-size: 12px; }
|
|
848
|
+
.nwe-chapter-toolbar input { min-width: 150px; }
|
|
849
|
+
.nwe-section-search { display: flex; align-items: center; gap: 8px; margin: 12px 0 0; color: var(--nwe-muted); font-size: 12px; }
|
|
850
|
+
.nwe-section-search input { min-height: 30px; min-width: 180px; padding: 4px 8px; border: 1px solid var(--nwe-border); border-radius: 7px; color: var(--nwe-ink); background: var(--nwe-surface); font: inherit; font-size: 12px; }
|
|
851
|
+
.nwe-table-wrap { overflow-x: auto; margin-top: 10px; border: 1px solid var(--nwe-border); border-radius: 10px; }
|
|
852
|
+
.nwe-chapter-detail { margin-top: 12px; padding: 12px; border: 1px solid var(--nwe-border); border-radius: 10px; background: color-mix(in srgb, var(--nwe-raised) 45%, transparent); }
|
|
853
|
+
.nwe-detail-text { max-height: 360px; overflow: auto; margin: 8px 0 0; padding: 10px; border-radius: 8px; background: var(--nwe-surface); color: var(--nwe-ink); font: 12px/1.7 ui-monospace, SFMono-Regular, Menlo, monospace; white-space: pre-wrap; overflow-wrap: anywhere; }
|
|
854
|
+
.nwe-chapter-table { width: 100%; min-width: 640px; border-collapse: collapse; font-size: 12px; }
|
|
855
|
+
.nwe-chapter-table th, .nwe-chapter-table td { padding: 8px 10px; border-bottom: 1px solid var(--nwe-border); text-align: left; vertical-align: top; }
|
|
856
|
+
.nwe-chapter-table th { color: var(--nwe-muted); font-weight: 600; background: color-mix(in srgb, var(--nwe-raised) 55%, transparent); }
|
|
857
|
+
.nwe-chapter-table tr:last-child td { border-bottom: 0; }
|
|
858
|
+
.nwe-chapter-table td.is-num { color: var(--nwe-gold); font-variant-numeric: tabular-nums; white-space: nowrap; }
|
|
859
|
+
.nwe-chapter-table .nwe-table-title { font-weight: 600; overflow-wrap: anywhere; }
|
|
860
|
+
.nwe-pager { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-top: 8px; color: var(--nwe-muted); font-size: 12px; }
|
|
805
861
|
.nwe-chapter {
|
|
806
862
|
overflow: hidden;
|
|
807
863
|
border: 1px solid var(--nwe-border);
|
|
@@ -823,18 +879,18 @@ window.__ModuleLoader__.load({
|
|
|
823
879
|
list-style: none;
|
|
824
880
|
}
|
|
825
881
|
.nwe-chapter summary::-webkit-details-marker { display: none; }
|
|
826
|
-
.nwe-chapter-num { color: var(--nwe-gold); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size:
|
|
882
|
+
.nwe-chapter-num { color: var(--nwe-gold); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; letter-spacing: .04em; }
|
|
827
883
|
.nwe-chapter-title { overflow: hidden; font-size: 14px; font-weight: 600; text-overflow: ellipsis; white-space: nowrap; }
|
|
828
|
-
.nwe-chapter-stage { margin-top: 2px; color: var(--nwe-muted); font-size:
|
|
884
|
+
.nwe-chapter-stage { margin-top: 2px; color: var(--nwe-muted); font-size: 12px; }
|
|
829
885
|
.nwe-chevron { color: var(--nwe-muted); font-size: 16px; line-height: 1; transition: transform .18s ease; }
|
|
830
886
|
.nwe-chapter[open] .nwe-chevron { transform: rotate(90deg); }
|
|
831
|
-
.nwe-chapter-meta { display: flex; align-items: center; gap: 10px; justify-content: flex-end; color: var(--nwe-muted); font-size:
|
|
887
|
+
.nwe-chapter-meta { display: flex; align-items: center; gap: 10px; justify-content: flex-end; color: var(--nwe-muted); font-size: 12px; font-variant-numeric: tabular-nums; white-space: nowrap; }
|
|
832
888
|
.nwe-score { display: flex; flex-direction: column; gap: 4px; width: 92px; }
|
|
833
889
|
.nwe-score-line { display: flex; justify-content: space-between; gap: 6px; }
|
|
834
|
-
.nwe-score-value { color: var(--nwe-ink); font-size:
|
|
890
|
+
.nwe-score-value { color: var(--nwe-ink); font-size: 12px; }
|
|
835
891
|
.nwe-scorebar { overflow: hidden; height: 4px; border-radius: 99px; background: var(--nwe-border); }
|
|
836
892
|
.nwe-scorebar > span { display: block; height: 100%; border-radius: inherit; background: linear-gradient(90deg, color-mix(in srgb, var(--nwe-gold) 65%, var(--nwe-jade)), var(--nwe-gold)); }
|
|
837
|
-
.nwe-badge { display: inline-flex; align-items: center; gap: 4px; padding: 3px 8px; border: 1px solid var(--nwe-border); border-radius: 999px; font-size:
|
|
893
|
+
.nwe-badge { display: inline-flex; align-items: center; gap: 4px; padding: 3px 8px; border: 1px solid var(--nwe-border); border-radius: 999px; font-size: 12px; white-space: nowrap; }
|
|
838
894
|
.nwe-badge.is-ok { border-color: color-mix(in srgb, var(--nwe-jade) 45%, transparent); color: var(--nwe-jade); }
|
|
839
895
|
.nwe-badge.is-bad { border-color: color-mix(in srgb, var(--nwe-rose) 45%, transparent); color: var(--nwe-rose); }
|
|
840
896
|
.nwe-badge.is-muted { color: var(--nwe-muted); }
|
|
@@ -857,19 +913,20 @@ window.__ModuleLoader__.load({
|
|
|
857
913
|
.nwe-project-choices { display: flex; flex-direction: column; gap: 8px; width: min(560px, 100%); margin-top: 16px; }
|
|
858
914
|
.nwe-project-choice { display: flex; flex-direction: column; gap: 3px; padding: 10px 13px; border: 1px solid var(--nwe-border); border-radius: 10px; background: var(--nwe-raised); color: var(--nwe-ink); font: inherit; font-size: 13px; text-align: left; cursor: pointer; }
|
|
859
915
|
.nwe-project-choice:hover { border-color: color-mix(in srgb, var(--nwe-gold) 65%, var(--nwe-border)); }
|
|
860
|
-
.nwe-project-choice span { overflow-wrap: anywhere; color: var(--nwe-muted); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size:
|
|
916
|
+
.nwe-project-choice span { overflow-wrap: anywhere; color: var(--nwe-muted); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; }
|
|
861
917
|
.nwe-skeleton { display: grid; gap: 10px; margin-top: 12px; }
|
|
862
918
|
.nwe-skeleton-line { height: 16px; border-radius: 8px; background: linear-gradient(90deg, var(--nwe-border), color-mix(in srgb, var(--nwe-border) 40%, transparent), var(--nwe-border)); background-size: 200% 100%; animation: nwe-shimmer 1.6s linear infinite; }
|
|
863
919
|
.nwe-skeleton-line.is-wide { height: 72px; }
|
|
864
920
|
.nwe-skeleton-line.is-short { width: 42%; }
|
|
865
921
|
.nwe-blocking { display: flex; flex-direction: column; gap: 8px; margin: 12px 0 0; padding: 0; list-style: none; }
|
|
866
922
|
.nwe-block { display: flex; gap: 11px; align-items: flex-start; padding: 13px 15px; border: 1px solid var(--nwe-border); border-left: 2px solid var(--nwe-rose); border-radius: 12px; background: color-mix(in srgb, var(--nwe-surface) 92%, transparent); }
|
|
867
|
-
.nwe-block-icon { display: grid; place-items: center; flex: 0 0 auto; width: 22px; height: 22px; border-radius: 7px; background: color-mix(in srgb, var(--nwe-rose) 14%, transparent); color: var(--nwe-rose); font-size:
|
|
868
|
-
.nwe-block-code { color: var(--nwe-rose); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size:
|
|
923
|
+
.nwe-block-icon { display: grid; place-items: center; flex: 0 0 auto; width: 22px; height: 22px; border-radius: 7px; background: color-mix(in srgb, var(--nwe-rose) 14%, transparent); color: var(--nwe-rose); font-size: 12px; font-weight: 700; }
|
|
924
|
+
.nwe-block-code { color: var(--nwe-rose); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; }
|
|
869
925
|
.nwe-block-detail { margin-top: 2px; font-size: 12px; }
|
|
870
|
-
.nwe-block-hint { margin-top: 3px; color: var(--nwe-muted); font-size:
|
|
926
|
+
.nwe-block-hint { margin-top: 3px; color: var(--nwe-muted); font-size: 12px; }
|
|
871
927
|
.nwe-pass { display: flex; align-items: center; gap: 9px; margin-top: 12px; padding: 13px 15px; border: 1px solid color-mix(in srgb, var(--nwe-jade) 30%, var(--nwe-border)); border-radius: 12px; color: var(--nwe-jade); font-size: 12px; background: color-mix(in srgb, var(--nwe-jade) 6%, var(--nwe-surface)); }
|
|
872
|
-
.nwe-footer { display: flex; justify-content: space-between; gap: 10px; margin-top: 26px; padding-top: 12px; border-top: 1px solid var(--nwe-border); color: var(--nwe-muted); font-size:
|
|
928
|
+
.nwe-footer { display: flex; justify-content: space-between; gap: 10px; margin-top: 26px; padding-top: 12px; border-top: 1px solid var(--nwe-border); color: var(--nwe-muted); font-size: 12px; }
|
|
929
|
+
.nwe-footer-actions { display: flex; align-items: center; gap: 8px; }
|
|
873
930
|
.nwe-footer code { color: var(--nwe-muted); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
|
|
874
931
|
@keyframes nwe-shimmer { from { background-position: 200% 0; } to { background-position: -200% 0; } }
|
|
875
932
|
@media (max-width: 760px) {
|
|
@@ -916,7 +973,7 @@ window.__ModuleLoader__.load({
|
|
|
916
973
|
box-shadow: none;
|
|
917
974
|
}
|
|
918
975
|
.nwe-hero-title { min-width: 0; }
|
|
919
|
-
.nwe-eyebrow { color: var(--nwe-muted); font-size:
|
|
976
|
+
.nwe-eyebrow { color: var(--nwe-muted); font-size: 12px; letter-spacing: .16em; }
|
|
920
977
|
.nwe-title {
|
|
921
978
|
margin: 4px 0 4px;
|
|
922
979
|
font-family: inherit;
|
|
@@ -924,17 +981,17 @@ window.__ModuleLoader__.load({
|
|
|
924
981
|
font-weight: 650;
|
|
925
982
|
letter-spacing: -.015em;
|
|
926
983
|
}
|
|
927
|
-
.nwe-meta { gap: 7px; font-size:
|
|
984
|
+
.nwe-meta { gap: 7px; font-size: 12px; }
|
|
928
985
|
.nwe-actions { gap: 7px; }
|
|
929
986
|
.nwe-project { flex-wrap: wrap; gap: 5px; }
|
|
930
|
-
.nwe-project-label { font-size:
|
|
987
|
+
.nwe-project-label { font-size: 12px; }
|
|
931
988
|
.nwe-project-select {
|
|
932
989
|
max-width: 310px;
|
|
933
990
|
min-height: 32px;
|
|
934
991
|
padding: 5px 26px 5px 9px;
|
|
935
992
|
border-radius: 8px;
|
|
936
993
|
background: var(--nwe-surface);
|
|
937
|
-
font-size:
|
|
994
|
+
font-size: 12px;
|
|
938
995
|
}
|
|
939
996
|
.nwe-project-search {
|
|
940
997
|
width: 124px;
|
|
@@ -945,19 +1002,19 @@ window.__ModuleLoader__.load({
|
|
|
945
1002
|
background: var(--nwe-surface);
|
|
946
1003
|
color: var(--nwe-ink);
|
|
947
1004
|
font: inherit;
|
|
948
|
-
font-size:
|
|
1005
|
+
font-size: 12px;
|
|
949
1006
|
}
|
|
950
1007
|
.nwe-project-toggle {
|
|
951
1008
|
display: inline-flex;
|
|
952
1009
|
align-items: center;
|
|
953
1010
|
gap: 4px;
|
|
954
1011
|
color: var(--nwe-muted);
|
|
955
|
-
font-size:
|
|
1012
|
+
font-size: 12px;
|
|
956
1013
|
cursor: pointer;
|
|
957
1014
|
white-space: nowrap;
|
|
958
1015
|
}
|
|
959
1016
|
.nwe-refresh, .nwe-help { min-height: 32px; padding: 6px 10px; border-radius: 8px; }
|
|
960
|
-
.nwe-rootline { font-size:
|
|
1017
|
+
.nwe-rootline { font-size: 12px; margin-top: 3px; }
|
|
961
1018
|
.nwe-stats {
|
|
962
1019
|
display: grid;
|
|
963
1020
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
@@ -975,11 +1032,11 @@ window.__ModuleLoader__.load({
|
|
|
975
1032
|
background: transparent;
|
|
976
1033
|
}
|
|
977
1034
|
.nwe-stat:last-child { border-right: 0; }
|
|
978
|
-
.nwe-stat-label { font-size:
|
|
1035
|
+
.nwe-stat-label { font-size: 12px; }
|
|
979
1036
|
.nwe-stat-value { margin-top: 1px; font-size: 18px; font-weight: 600; }
|
|
980
1037
|
.nwe-section { margin-top: 18px; }
|
|
981
1038
|
.nwe-section-title { font-family: inherit; font-size: 14px; font-weight: 650; }
|
|
982
|
-
.nwe-section-note { font-size:
|
|
1039
|
+
.nwe-section-note { font-size: 12px; }
|
|
983
1040
|
.nwe-next {
|
|
984
1041
|
margin-top: 12px;
|
|
985
1042
|
padding: 14px 16px;
|
|
@@ -991,8 +1048,8 @@ window.__ModuleLoader__.load({
|
|
|
991
1048
|
.nwe-next-icon { width: 24px; height: 24px; border-radius: 7px; }
|
|
992
1049
|
.nwe-next-body { min-width: 0; }
|
|
993
1050
|
.nwe-next-title { font-size: 14px; }
|
|
994
|
-
.nwe-next-detail { font-size:
|
|
995
|
-
.nwe-next-tool { font-size:
|
|
1051
|
+
.nwe-next-detail { font-size: 12px; }
|
|
1052
|
+
.nwe-next-tool { font-size: 12px; }
|
|
996
1053
|
.nwe-desk {
|
|
997
1054
|
display: grid;
|
|
998
1055
|
grid-template-columns: minmax(0, 1.2fr) minmax(300px, .8fr);
|
|
@@ -1008,8 +1065,8 @@ window.__ModuleLoader__.load({
|
|
|
1008
1065
|
gap: 8px;
|
|
1009
1066
|
margin-top: 16px;
|
|
1010
1067
|
}
|
|
1011
|
-
.nwe-desk-kicker { color: var(--nwe-muted); font-size:
|
|
1012
|
-
.nwe-desk-note { color: var(--nwe-gold); font-size:
|
|
1068
|
+
.nwe-desk-kicker { color: var(--nwe-muted); font-size: 12px; font-weight: 650; letter-spacing: .14em; text-transform: uppercase; }
|
|
1069
|
+
.nwe-desk-note { color: var(--nwe-gold); font-size: 12px; }
|
|
1013
1070
|
.nwe-desk-main .nwe-phases { margin-top: 8px; }
|
|
1014
1071
|
.nwe-desk-side .nwe-book-grid { grid-template-columns: 1fr; }
|
|
1015
1072
|
.nwe-desk-side .nwe-section { margin-top: 16px; }
|
|
@@ -1020,27 +1077,27 @@ window.__ModuleLoader__.load({
|
|
|
1020
1077
|
gap: 8px;
|
|
1021
1078
|
align-items: baseline;
|
|
1022
1079
|
padding: 7px 9px;
|
|
1023
|
-
border: 1px solid color-mix(in srgb,
|
|
1080
|
+
border: 1px solid color-mix(in srgb, var(--nwe-gold) 24%, var(--nwe-border));
|
|
1024
1081
|
border-radius: 7px;
|
|
1025
|
-
background: color-mix(in srgb,
|
|
1026
|
-
font-size:
|
|
1082
|
+
background: color-mix(in srgb, var(--nwe-gold) 5%, transparent);
|
|
1083
|
+
font-size: 12px;
|
|
1027
1084
|
}
|
|
1028
|
-
.nwe-advisory-code { color:
|
|
1085
|
+
.nwe-advisory-code { color: var(--nwe-gold); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; white-space: nowrap; }
|
|
1029
1086
|
.nwe-advisory-detail { color: var(--nwe-ink); line-height: 1.5; }
|
|
1030
|
-
.nwe-advisory-hint { color: var(--nwe-muted); font-size:
|
|
1087
|
+
.nwe-advisory-hint { color: var(--nwe-muted); font-size: 12px; white-space: nowrap; }
|
|
1031
1088
|
.nwe-advisory-empty {
|
|
1032
1089
|
margin-top: 8px;
|
|
1033
1090
|
padding: 8px 10px;
|
|
1034
|
-
border: 1px solid color-mix(in srgb,
|
|
1091
|
+
border: 1px solid color-mix(in srgb, var(--nwe-gold) 18%, var(--nwe-border));
|
|
1035
1092
|
border-radius: 7px;
|
|
1036
1093
|
color: var(--nwe-muted);
|
|
1037
|
-
font-size:
|
|
1094
|
+
font-size: 12px;
|
|
1038
1095
|
}
|
|
1039
1096
|
|
|
1040
|
-
.nwe-pending-section { border-left: 3px solid
|
|
1097
|
+
.nwe-pending-section { border-left: 3px solid var(--nwe-gold); padding-left: 10px; }
|
|
1041
1098
|
.nwe-pending-error { border-left-color: #b3261e; }
|
|
1042
|
-
.nwe-pending-detail { margin: 8px 0 0; color: var(--nwe-ink); font-size:
|
|
1043
|
-
.nwe-pending-hint { margin: 4px 0 0; color: var(--nwe-muted); font-size:
|
|
1099
|
+
.nwe-pending-detail { margin: 8px 0 0; color: var(--nwe-ink); font-size: 12px; line-height: 1.6; }
|
|
1100
|
+
.nwe-pending-hint { margin: 4px 0 0; color: var(--nwe-muted); font-size: 12px; line-height: 1.5; }
|
|
1044
1101
|
|
|
1045
1102
|
.nwe-v6-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
|
|
1046
1103
|
.nwe-v6-card { border: 1px solid var(--nwe-border); border-radius: 8px; padding: 10px; background: var(--nwe-surface); }
|
|
@@ -1048,16 +1105,17 @@ window.__ModuleLoader__.load({
|
|
|
1048
1105
|
.nwe-v6-card > summary { cursor: pointer; font: 600 12px/1.4 var(--nwe-font); color: var(--nwe-ink); }
|
|
1049
1106
|
.nwe-v6-card > summary:focus-visible { outline: 2px solid var(--nwe-accent); outline-offset: 2px; }
|
|
1050
1107
|
.nwe-v6-list { margin: 8px 0 0; padding: 0; list-style: none; display: grid; gap: 4px; }
|
|
1051
|
-
.nwe-v6-list li
|
|
1108
|
+
.nwe-v6-list li, .nwe-v6-list b, .nwe-v6-list span { min-width: 0; overflow-wrap: anywhere; }
|
|
1109
|
+
.nwe-v6-list li { display: grid; grid-template-columns: 7.5em minmax(0, 1fr); gap: 8px; font-size: 12px; line-height: 1.55; color: var(--nwe-muted); }
|
|
1052
1110
|
.nwe-v6-list b { color: var(--nwe-ink); font-weight: 600; overflow-wrap: anywhere; }
|
|
1053
1111
|
.nwe-v6-bars { display: grid; gap: 4px; margin-top: 8px; }
|
|
1054
|
-
.nwe-v6-bar { display: grid; grid-template-columns: 7em minmax(0, 1fr) 3em; gap: 6px; align-items: center; font-size:
|
|
1112
|
+
.nwe-v6-bar { display: grid; grid-template-columns: 7em minmax(0, 1fr) 3em; gap: 6px; align-items: center; font-size: 12px; color: var(--nwe-muted); }
|
|
1055
1113
|
.nwe-v6-bar span { height: 4px; border-radius: 2px; background: var(--nwe-border); overflow: hidden; }
|
|
1056
1114
|
.nwe-v6-bar span i { display: block; height: 100%; background: var(--nwe-accent); }
|
|
1057
|
-
.nwe-v6-note { margin: 8px 0 0; font-size:
|
|
1115
|
+
.nwe-v6-note { margin: 8px 0 0; font-size: 12px; line-height: 1.5; color: var(--nwe-muted); }
|
|
1058
1116
|
.nwe-v6-tags { display: flex; flex-wrap: wrap; gap: 5px; margin-top: 6px; }
|
|
1059
|
-
.nwe-v6-tags span { border: 1px solid var(--nwe-border); border-radius: 999px; padding: 2px 7px; font-size:
|
|
1060
|
-
.nwe-chapter-extra { margin-top: 6px; font-size:
|
|
1117
|
+
.nwe-v6-tags span { border: 1px solid var(--nwe-border); border-radius: 999px; padding: 2px 7px; font-size: 12px; color: var(--nwe-muted); }
|
|
1118
|
+
.nwe-chapter-extra { margin-top: 6px; font-size: 12px; color: var(--nwe-muted); line-height: 1.5; overflow-wrap: anywhere; }
|
|
1061
1119
|
@media (max-width: 640px) { .nwe-v6-grid { grid-template-columns: 1fr; } .nwe-v6-card[open] { grid-column: auto; } }
|
|
1062
1120
|
.nwe-legacy {
|
|
1063
1121
|
display: flex;
|
|
@@ -1067,10 +1125,10 @@ window.__ModuleLoader__.load({
|
|
|
1067
1125
|
border: 1px dashed var(--nwe-border);
|
|
1068
1126
|
border-radius: 7px;
|
|
1069
1127
|
color: var(--nwe-muted);
|
|
1070
|
-
font-size:
|
|
1128
|
+
font-size: 12px;
|
|
1071
1129
|
line-height: 1.5;
|
|
1072
1130
|
}
|
|
1073
|
-
.nwe-legacy code { color: var(--nwe-muted); font-size:
|
|
1131
|
+
.nwe-legacy code { color: var(--nwe-muted); font-size: 12px; white-space: nowrap; }
|
|
1074
1132
|
.nwe-chapters-section { margin-top: 20px; }
|
|
1075
1133
|
.nwe-desk-main .nwe-chapters {
|
|
1076
1134
|
max-height: 460px;
|
|
@@ -1222,7 +1280,7 @@ brief P6 → check → brief P8 → archive 的顺序推进。
|
|
|
1222
1280
|
正文只通过 write_draft/revise 写;不要直接改 state/、chapters/ 或 outline/。
|
|
1223
1281
|
每次提交前先领取当前阶段任务书,失败时按返回的 code 和 blocking 一次改完。`;
|
|
1224
1282
|
const TUTORIAL_NEXT_PROMPT = `继续写当前项目的下一章。先调用 novel_pipeline(或 novel_status)确认当前阶段、项目根目录和下一步工具,不要猜阶段。
|
|
1225
|
-
|
|
1283
|
+
如果当前会话还没绑定或不确定是哪本书:先让用户执行 /novel current 或 /novel use(Agent 不执行 slash 命令),同时调用 novel_status 看 selection;不要靠 active 猜。
|
|
1226
1284
|
第一次带 project_root 的项目调用会成为当前会话的持久绑定;后续无 root 调用会继续这本书。纯文本的 novel_word_count 和 novel_diff 不读取项目,也不会建立绑定。
|
|
1227
1285
|
|
|
1228
1286
|
如果下一章还没有章卡:用 novel_outline 只提交下一章的卡(按 num upsert),省略的已有卡会保持不动;不要重交全量,也不要用空数组清空。要删卡必须显式传 remove。
|
|
@@ -1235,17 +1293,17 @@ brief P6 → check → brief P8 → archive 的顺序推进。
|
|
|
1235
1293
|
className: "nwe-stat",
|
|
1236
1294
|
key: label
|
|
1237
1295
|
}, (0, react.createElement)("div", { className: "nwe-stat-label" }, label), (0, react.createElement)("div", { className: "nwe-stat-value" }, value, (0, react.createElement)("span", { className: "nwe-stat-unit" }, unit)));
|
|
1238
|
-
const chapterCard = (row) => (0, react.createElement)("li", { key: row.num }, (0, react.createElement)("details", { className: `nwe-chapter is-${row.tone}` }, (0, react.createElement)("summary", { "aria-label": `第 ${row.num} 章 ${row.title},${row.badge},${row.score}` }, (0, react.createElement)("
|
|
1296
|
+
const chapterCard = (row) => (0, react.createElement)("li", { key: row.num }, (0, react.createElement)("details", { className: `nwe-chapter is-${row.tone}` }, (0, react.createElement)("summary", { "aria-label": `第 ${row.num} 章 ${row.title},${row.badge},${row.score}` }, (0, react.createElement)("span", null, (0, react.createElement)("span", { className: "nwe-chapter-num" }, `CH ${row.num.padStart(2, "0")}`)), (0, react.createElement)("span", { style: { minWidth: 0 } }, (0, react.createElement)("span", { className: "nwe-chapter-title" }, row.title), (0, react.createElement)("span", { className: "nwe-chapter-stage" }, row.stageLabel)), (0, react.createElement)("span", { className: "nwe-chapter-meta" }, (0, react.createElement)("span", null, `${row.words} 字`), (0, react.createElement)("span", { className: "nwe-score" }, (0, react.createElement)("span", { className: "nwe-score-line" }, (0, react.createElement)("span", { className: "nwe-score-value" }, row.score), (0, react.createElement)("span", { className: "nwe-chapter-stage" }, row.scorePercent === null ? "未评审" : `${row.scorePercent}%`)), row.scoreValue === null ? null : (0, react.createElement)("span", {
|
|
1239
1297
|
className: "nwe-scorebar",
|
|
1240
1298
|
role: "progressbar",
|
|
1241
1299
|
"aria-valuemin": 0,
|
|
1242
1300
|
"aria-valuemax": 150,
|
|
1243
1301
|
"aria-valuenow": row.scoreValue,
|
|
1244
1302
|
"aria-label": `${row.title} 评分`
|
|
1245
|
-
}, (0, react.createElement)("span", { style: { width: `${row.scorePercent}%` } }))), (0, react.createElement)("span", { className: `nwe-badge is-${row.tone}` }, row.badge)), (0, react.createElement)("span", {
|
|
1303
|
+
}, (0, react.createElement)("span", { style: { width: `${row.scorePercent ?? 0}%` } }))), (0, react.createElement)("span", { className: `nwe-badge is-${row.tone}` }, row.badge)), (0, react.createElement)("span", {
|
|
1246
1304
|
className: "nwe-chevron",
|
|
1247
1305
|
"aria-hidden": "true"
|
|
1248
|
-
}, "›")), row.details === "" ? null : (0, react.createElement)("div", { className: "nwe-chapter-detail" }, row.details), (0, react.createElement)("div", { className: "nwe-chapter-extra" }, `${row.sceneType || "—"} · 对话 ${row.dialogueRatio === null ? "—" : `${Math.round(row.dialogueRatio * 100)}%`} · 钩子 ${row.hookStrength ?? "—"} · 终检 ${row.finalCheck
|
|
1306
|
+
}, "›")), row.details === "" ? null : (0, react.createElement)("div", { className: "nwe-chapter-detail" }, row.details), (0, react.createElement)("div", { className: "nwe-chapter-extra" }, `${row.sceneType || "—"} · 对话 ${row.dialogueRatio === null ? "—" : `${Math.round(row.dialogueRatio * 100)}%`} · 钩子 ${row.hookStrength ?? "—"} · 终检 ${finalCheckLabel(row.finalCheck)} · 评 ${row.reviews} / 修 ${row.revisions} / 检 ${row.checks} / 回滚 ${row.rollbacks} · 归档 ${localTime(row.archivedAt)}`)));
|
|
1249
1307
|
const phases = (view) => (0, react.createElement)("ol", {
|
|
1250
1308
|
className: "nwe-phases",
|
|
1251
1309
|
"aria-label": "创作阶段"
|
|
@@ -1294,7 +1352,7 @@ brief P6 → check → brief P8 → archive 的顺序推进。
|
|
|
1294
1352
|
}, `${label} ${value}`))), (0, react.createElement)("div", {
|
|
1295
1353
|
className: "nwe-book-head",
|
|
1296
1354
|
style: { marginTop: 12 }
|
|
1297
|
-
}, (0, react.createElement)("span", { className: "nwe-book-title" }, "质量趋势"), (0, react.createElement)("span", { className: "nwe-book-note" }, bars.length === 0 ? "—" : `${view.quality.lastTenAverage.toFixed(1)} 近十均分 · 趋势 ${view.quality.trendText} · ${view.quality.samples} 章 / ${view.quality.attempts} 评`)), (0, react.createElement)("div", {
|
|
1355
|
+
}, (0, react.createElement)("span", { className: "nwe-book-title" }, "质量趋势"), (0, react.createElement)("span", { className: "nwe-book-note" }, bars.length === 0 ? "—" : `${view.quality.lastTenAverage === null ? "—" : view.quality.lastTenAverage.toFixed(1)} 近十均分 · 趋势 ${view.quality.samples === 0 ? "见质量页" : view.quality.trendText} · ${view.quality.samples === 0 ? "可读评审 —" : `${view.quality.samples} 章 / ${view.quality.attempts} 评`}`)), (0, react.createElement)("div", {
|
|
1298
1356
|
className: "nwe-bars",
|
|
1299
1357
|
role: "img",
|
|
1300
1358
|
"aria-label": "最近十章评分"
|
|
@@ -1305,9 +1363,19 @@ brief P6 → check → brief P8 → archive 的顺序推进。
|
|
|
1305
1363
|
title: `${score}/150`
|
|
1306
1364
|
}))), (0, react.createElement)("div", { className: `nwe-health${view.health.ok ? "" : " is-bad"}` }, (0, react.createElement)("span", { "aria-hidden": "true" }, view.health.ok ? "✓" : "!"), (0, react.createElement)("span", null, view.health.ok ? "项目健康:没有阻塞或损坏状态" : `需要处理:${view.health.blocking} 条门控 · ${view.health.stateErrors} 个损坏文档${view.health.pending ? " · 有 pending 事务" : ""}`)))));
|
|
1307
1365
|
};
|
|
1366
|
+
const localTime = (value) => {
|
|
1367
|
+
if (value === null || value === void 0 || value === "") return "—";
|
|
1368
|
+
const parsed = new Date(value);
|
|
1369
|
+
return Number.isNaN(parsed.getTime()) ? value : parsed.toLocaleString("zh-CN", {
|
|
1370
|
+
month: "2-digit",
|
|
1371
|
+
day: "2-digit",
|
|
1372
|
+
hour: "2-digit",
|
|
1373
|
+
minute: "2-digit"
|
|
1374
|
+
});
|
|
1375
|
+
};
|
|
1308
1376
|
const activitySection = (view) => {
|
|
1309
1377
|
if (view.activity.length === 0) return null;
|
|
1310
|
-
return (0, react.createElement)("section", { className: "nwe-section" }, (0, react.createElement)("div", { className: "nwe-section-head" }, (0, react.createElement)("h2", { className: "nwe-section-title" }, "最近活动"), (0, react.createElement)("span", { className: "nwe-section-note" }, "来自事务日志")), (0, react.createElement)("ul", { className: "nwe-activity" }, ...view.activity.slice(-5).map((row, index) => (0, react.createElement)("li", { key: `${row.at}-${row.op}-${index}` }, (0, react.createElement)("
|
|
1378
|
+
return (0, react.createElement)("section", { className: "nwe-section" }, (0, react.createElement)("div", { className: "nwe-section-head" }, (0, react.createElement)("h2", { className: "nwe-section-title" }, "最近活动"), (0, react.createElement)("span", { className: "nwe-section-note" }, "来自事务日志")), (0, react.createElement)("ul", { className: "nwe-activity" }, ...view.activity.slice(-5).map((row, index) => (0, react.createElement)("li", { key: `${row.at}-${row.op}-${index}` }, (0, react.createElement)("time", { dateTime: row.at }, localTime(row.at)), (0, react.createElement)("code", null, row.op), (0, react.createElement)("span", null, row.chapter === 0 ? statusLabel(row.status) : `第 ${row.chapter} 章 · ${statusLabel(row.status)}`)))));
|
|
1311
1379
|
};
|
|
1312
1380
|
const advisoriesSection = (view) => {
|
|
1313
1381
|
if (view.advisories.length === 0 && view.acknowledgedAdvisories === 0) return null;
|
|
@@ -1326,7 +1394,7 @@ brief P6 → check → brief P8 → archive 的顺序推进。
|
|
|
1326
1394
|
return (0, react.createElement)("section", {
|
|
1327
1395
|
className: `nwe-section nwe-pending-section${partial ? " nwe-pending-error" : ""}`,
|
|
1328
1396
|
"aria-label": "待恢复事务"
|
|
1329
|
-
}, (0, react.createElement)("div", { className: "nwe-section-head" }, (0, react.createElement)("h2", { className: "nwe-section-title" }, partial ? "事务部分完成" : "待恢复事务"), (0, react.createElement)("span", { className: "nwe-section-note" },
|
|
1397
|
+
}, (0, react.createElement)("div", { className: "nwe-section-head" }, (0, react.createElement)("h2", { className: "nwe-section-title" }, partial ? "事务部分完成" : "待恢复事务"), (0, react.createElement)("span", { className: "nwe-section-note" }, partial ? "部分完成" : "待恢复")), (0, react.createElement)("p", { className: "nwe-pending-detail" }, view.pending.detail), (0, react.createElement)("p", { className: "nwe-pending-hint" }, partial ? `检查 backups 与 .staging/${view.pending.txid}/before/,不要整项目恢复。` : "下一次写操作会自动收尾;只读面不会改动事务。"));
|
|
1330
1398
|
};
|
|
1331
1399
|
const legacySection = (view) => {
|
|
1332
1400
|
if (view.legacy.length === 0) return null;
|
|
@@ -1338,7 +1406,14 @@ brief P6 → check → brief P8 → archive 的顺序推进。
|
|
|
1338
1406
|
key: `${item.code}-${item.detail}`
|
|
1339
1407
|
}, (0, react.createElement)("code", null, item.code), (0, react.createElement)("span", null, item.detail)))));
|
|
1340
1408
|
};
|
|
1341
|
-
const footer = () => (0, react.createElement)("footer", { className: "nwe-footer" }, (0, react.createElement)("span",
|
|
1409
|
+
const footer = (view, onRefresh) => (0, react.createElement)("footer", { className: "nwe-footer" }, (0, react.createElement)("span", {
|
|
1410
|
+
role: "status",
|
|
1411
|
+
"aria-live": "polite"
|
|
1412
|
+
}, view.syncError !== void 0 && view.syncError !== null && view.syncError !== "" ? `同步失败:${view.syncError}` : view.lastSync !== void 0 && view.lastSync !== null ? `只读面板 · 数据来自引擎 · 更新于 ${new Date(view.lastSync).toLocaleTimeString("zh-CN")}` : "只读面板 · 数据来自引擎 · 等待首次同步"), (0, react.createElement)("span", { className: "nwe-footer-actions" }, view.syncError !== void 0 && view.syncError !== null && view.syncError !== "" ? (0, react.createElement)("button", {
|
|
1413
|
+
type: "button",
|
|
1414
|
+
className: "nwe-copy",
|
|
1415
|
+
onClick: onRefresh
|
|
1416
|
+
}, "重试") : null, (0, react.createElement)("code", null, "novel-workshop")));
|
|
1342
1417
|
const loadingState = () => (0, react.createElement)("div", {
|
|
1343
1418
|
className: "nwe-section",
|
|
1344
1419
|
role: "status",
|
|
@@ -1405,7 +1480,7 @@ brief P6 → check → brief P8 → archive 的顺序推进。
|
|
|
1405
1480
|
onChange: (event) => {
|
|
1406
1481
|
onSelect(event.target.value === "" ? null : event.target.value);
|
|
1407
1482
|
}
|
|
1408
|
-
},
|
|
1483
|
+
}, (0, react.createElement)("option", { value: "" }, view.selectedRoot === null ? "跟随当前项目(自动)" : "跟随当前项目"), ...groups.filter((group) => group.items.length > 0).map((group) => (0, react.createElement)("optgroup", {
|
|
1409
1484
|
key: group.key,
|
|
1410
1485
|
label: group.label
|
|
1411
1486
|
}, ...group.items.map((project) => (0, react.createElement)("option", {
|
|
@@ -1420,10 +1495,10 @@ brief P6 → check → brief P8 → archive 的顺序推进。
|
|
|
1420
1495
|
const choosePanel = (view, onSelect) => {
|
|
1421
1496
|
const preferred = view.projects.filter((project) => project.group !== "missing");
|
|
1422
1497
|
const shown = preferred.length > 0 ? preferred : view.projects;
|
|
1423
|
-
return (0, react.createElement)("div", {
|
|
1424
|
-
className: "nwe-state",
|
|
1498
|
+
return (0, react.createElement)("div", { className: "nwe-state" }, (0, react.createElement)("div", { className: "nwe-state-icon" }, stateIcon()), (0, react.createElement)("div", { className: "nwe-state-title" }, "发现多本书,先选一本"), (0, react.createElement)("div", {
|
|
1499
|
+
className: "nwe-state-text",
|
|
1425
1500
|
role: "status"
|
|
1426
|
-
},
|
|
1501
|
+
}, view.subtitle), (0, react.createElement)("div", { className: "nwe-project-choices" }, ...shown.map((project) => (0, react.createElement)("button", {
|
|
1427
1502
|
key: project.root,
|
|
1428
1503
|
type: "button",
|
|
1429
1504
|
className: "nwe-project-choice",
|
|
@@ -1481,6 +1556,28 @@ brief P6 → check → brief P8 → archive 的顺序推进。
|
|
|
1481
1556
|
className: "nwe-step",
|
|
1482
1557
|
key: step.title
|
|
1483
1558
|
}, (0, react.createElement)("div", { className: "nwe-step-head" }, (0, react.createElement)("span", { className: "nwe-step-no" }, String(index + 1).padStart(2, "0")), (0, react.createElement)("h4", null, step.title), (0, react.createElement)("code", { className: "nwe-step-tool" }, step.tool)), (0, react.createElement)("p", null, step.body), (0, react.createElement)("p", null, (0, react.createElement)("strong", null, "注意:"), step.note));
|
|
1559
|
+
const CopyPrompt = ({ text, label }) => {
|
|
1560
|
+
const [status, setStatus] = (0, react.useState)("");
|
|
1561
|
+
const copy = (0, react.useCallback)(async () => {
|
|
1562
|
+
try {
|
|
1563
|
+
const clipboard = globalThis.navigator?.clipboard;
|
|
1564
|
+
if (clipboard?.writeText === void 0) throw new Error("clipboard unavailable");
|
|
1565
|
+
await clipboard.writeText(text);
|
|
1566
|
+
setStatus("已复制到剪贴板");
|
|
1567
|
+
} catch {
|
|
1568
|
+
setStatus("复制失败:请手动选择下方文本,或检查浏览器剪贴板权限");
|
|
1569
|
+
}
|
|
1570
|
+
}, [text]);
|
|
1571
|
+
return (0, react.createElement)("div", { className: "nwe-copy-block" }, (0, react.createElement)("pre", { className: "nwe-prompt" }, text), (0, react.createElement)("div", { className: "nwe-copy-row" }, (0, react.createElement)("button", {
|
|
1572
|
+
type: "button",
|
|
1573
|
+
className: "nwe-copy",
|
|
1574
|
+
onClick: () => void copy()
|
|
1575
|
+
}, label), (0, react.createElement)("span", {
|
|
1576
|
+
className: "nwe-copy-status",
|
|
1577
|
+
role: "status",
|
|
1578
|
+
"aria-live": "polite"
|
|
1579
|
+
}, status)));
|
|
1580
|
+
};
|
|
1484
1581
|
const tutorialDialog = (onClose, closeRef) => (0, react.createElement)("div", {
|
|
1485
1582
|
className: "nwe-modal-backdrop",
|
|
1486
1583
|
onMouseDown: (event) => {
|
|
@@ -1519,7 +1616,13 @@ brief P6 → check → brief P8 → archive 的顺序推进。
|
|
|
1519
1616
|
ref: closeRef,
|
|
1520
1617
|
onClick: onClose,
|
|
1521
1618
|
"aria-label": "关闭使用教程"
|
|
1522
|
-
}, (0, react.createElement)("span", { "aria-hidden": "true" }, "×"))), (0, react.createElement)("div", { className: "nwe-modal-body" }, (0, react.createElement)("div", { className: "nwe-tutorial-intro" }, (0, react.createElement)("p", null, "网文工坊不是一个“帮你想剧情”的按钮,而是一台有状态的写作引擎:你提供内容,引擎负责阶段、字数、门控、评审、归档和文件。"), (0, react.createElement)("p", null, "每一步都先领取当前阶段的任务书,再提交;提交后看引擎返回的 blocking 与下一步。面板只读,真正的写操作都在对话里的工具调用中。")), (0, react.createElement)("section", { className: "nwe-tutorial-block" }, (0, react.createElement)("h3", null, "开始前:记住三件事"), (0, react.createElement)("div", { className: "nwe-tutorial-cards" }, (0, react.createElement)("div", { className: "nwe-tutorial-card" }, (0, react.createElement)("strong", null, "只用引擎入口写"), (0, react.createElement)("span", null, "正文只走 novel_write_draft / novel_revise;状态只走工具。手改 chapters/、state/、outline/ 会破坏事务与校验。")), (0, react.createElement)("div", { className: "nwe-tutorial-card" }, (0, react.createElement)("strong", null, "每一步先领 brief"), (0, react.createElement)("span", null, "任务书携带当前上下文和 digest;跳过它会撞 PG-01。任务书过期就重新领取,不要复用旧上下文。")), (0, react.createElement)("div", { className: "nwe-tutorial-card" }, (0, react.createElement)("strong", null, "失败先读 code"), (0, react.createElement)("span", null, "参数错改参数,门控没过改正文,系统故障查目录/稍后重试。blocking 会一次列全,一次改完再交。"))), (0, react.createElement)("p", { className: "nwe-tutorial-note" }, (0, react.createElement)("strong", null, "用哪个会话:"), " 想获得完整的引擎保证,请新开一个「网文工坊」预设会话;普通会话里模型可能还挂着文件工具,能绕过引擎直接改稿。")), (0, react.createElement)("section", { className: "nwe-tutorial-block" }, (0, react.createElement)("h3", null, "一本书的完整流程"), (0, react.createElement)("p", null, "前四步是开书准备,后面是每章循环。第一次先按顺序走完一章,熟悉后再批量推进整本。"), (0, react.createElement)("ol", { className: "nwe-steps" }, ...TUTORIAL_STEPS.map((step, index) => tutorialStep(step, index)))), (0, react.createElement)("section", { className: "nwe-tutorial-block" }, (0, react.createElement)("h3", null, "面板怎么看"), (0, react.createElement)("div", { className: "nwe-tutorial-cards" }, (0, react.createElement)("div", { className: "nwe-tutorial-card" }, (0, react.createElement)("strong", null, "顶部统计"), (0, react.createElement)("span", null, "已归档章、总字数、平均分、待处理门控。数字来自引擎,不是正文估算。")), (0, react.createElement)("div", { className: "nwe-tutorial-card" }, (0, react.createElement)("strong", null, "九阶段轨与下一步"), (0, react.createElement)("span", null, "亮起的阶段是当前进度,“下一步”卡会直接给出该调用的工具名。")), (0, react.createElement)("div", { className: "nwe-tutorial-card" }, (0, react.createElement)("strong", null, "章节与门控"), (0, react.createElement)("span", null, "章节卡展开可看字数、评分与状态;门控区按 code/detail/hint 列出所有阻塞。"))), (0, react.createElement)("p", { className: "nwe-tutorial-note" }, (0, react.createElement)("strong", null, "刷新:"), " 面板每 15 秒静默同步一次,也可以点右上角刷新。它是只读投影;不会替你改稿、归档或修状态。"), (0, react.createElement)("p", { className: "nwe-tutorial-note" }, (0, react.createElement)("strong", null, "项目状态:"), "选择器里 choose 表示发现多本书、还没选;项目不在了表示目录缺失,绑定仍会保留;不是项目表示目录还在但没有 novel.yaml 或项目状态;项目不可读表示权限或 IO 错误,先修权限或换一本。面板选择只影响查看,不改变任何会话的写作绑定。")), (0, react.createElement)("section", { className: "nwe-tutorial-block" }, (0, react.createElement)("h3", null, "卡住时先看这里"), (0, react.createElement)("ul", { className: "nwe-faq" }, (0, react.createElement)("li", null, (0, react.createElement)("strong", null, "为什么一直让我先领 brief?"), (0, react.createElement)("p", null, "这是正常门控。任务书决定当前阶段可用的字段与上下文 digest;先按返回值调用 novel_brief,再继续。")), (0, react.createElement)("li", null, (0, react.createElement)("strong", null, "门控失败,改一条还是全改?"), (0, react.createElement)("p", null, "一次看完所有 blocking,再一起改。逐条试会反复烧掉阶段轮次;错误里的 code 会告诉你属于参数、门控还是系统故障。")), (0, react.createElement)("li", null, (0, react.createElement)("strong", null, "字数和评分为什么对不上?"), (0, react.createElement)("p", null, "字数以 engine 返回为准,可用 novel_word_count 核对;review 的 total 必须等于 150 减全部扣分,不能另算一套。")), (0, react.createElement)("li", null, (0, react.createElement)("strong", null, "P4/P6 过不去怎么办?"), (0, react.createElement)("p", null, "P4 有 blocker 就领 P5 任务书修订,再回 P4 重评;P6 失败同样回修订。不要 waive blocker,也不要只跑半个 scope 就归档。")), (0, react.createElement)("li", null, (0, react.createElement)("strong", null, "想改已经归档的章节?"), (0, react.createElement)("p", null, "先 novel_reopen,带非空 reason。它会从归档备份恢复,重开后重新走 P3→P8;不要直接编辑归档卡或正文文件。")), (0, react.createElement)("li", null, (0, react.createElement)("strong", null, "状态文档坏了能手工修吗?"), (0, react.createElement)("p", null, "先用 novel_state 看 raw 和 next;需要修复时用 novel_amend,并在 reason 写清原因。手改 YAML 会丢失事务保护。")), (0, react.createElement)("li", null, (0, react.createElement)("strong", null, "有多个项目,面板看的是哪本?"), (0, react.createElement)("p", null, "面板用顶部项目选择器切换查看对象;选择只改 ?root= 和浏览器 localStorage,不调用 activate、不写会话绑定。写作项目请用 /novel use 或显式 project_root。")), (0, react.createElement)("li", null, (0, react.createElement)("strong", null, "选错书、想清掉绑定怎么办?"), (0, react.createElement)("p", null, "先用 /novel current 看当前会话绑定;要换书用 /novel use <路径|书名|.>,要清掉只影响当前会话的绑定用 /novel unbind。不要靠 active 猜,也不要手改 state/session-bindings.yaml。")), (0, react.createElement)("li", null, (0, react.createElement)("strong", null, "扫描到了,为什么还没切过去?"), (0, react.createElement)("p", null, "scan 只读发现;scan --register 只登记项目,不选择、不改 active、不绑会话。真正切换写作项目要用 /novel use,或第一次调用显式传 project_root。")))), (0, react.createElement)("section", { className: "nwe-tutorial-block" }, (0, react.createElement)("h3", null, "可以直接复制给模型的起手话术"), (0, react.createElement)(
|
|
1619
|
+
}, (0, react.createElement)("span", { "aria-hidden": "true" }, "×"))), (0, react.createElement)("div", { className: "nwe-modal-body" }, (0, react.createElement)("div", { className: "nwe-tutorial-intro" }, (0, react.createElement)("p", null, "网文工坊不是一个“帮你想剧情”的按钮,而是一台有状态的写作引擎:你提供内容,引擎负责阶段、字数、门控、评审、归档和文件。"), (0, react.createElement)("p", null, "每一步都先领取当前阶段的任务书,再提交;提交后看引擎返回的 blocking 与下一步。面板只读,真正的写操作都在对话里的工具调用中。")), (0, react.createElement)("section", { className: "nwe-tutorial-block" }, (0, react.createElement)("h3", null, "开始前:记住三件事"), (0, react.createElement)("div", { className: "nwe-tutorial-cards" }, (0, react.createElement)("div", { className: "nwe-tutorial-card" }, (0, react.createElement)("strong", null, "只用引擎入口写"), (0, react.createElement)("span", null, "正文只走 novel_write_draft / novel_revise;状态只走工具。手改 chapters/、state/、outline/ 会破坏事务与校验。")), (0, react.createElement)("div", { className: "nwe-tutorial-card" }, (0, react.createElement)("strong", null, "每一步先领 brief"), (0, react.createElement)("span", null, "任务书携带当前上下文和 digest;跳过它会撞 PG-01。任务书过期就重新领取,不要复用旧上下文。")), (0, react.createElement)("div", { className: "nwe-tutorial-card" }, (0, react.createElement)("strong", null, "失败先读 code"), (0, react.createElement)("span", null, "参数错改参数,门控没过改正文,系统故障查目录/稍后重试。blocking 会一次列全,一次改完再交。"))), (0, react.createElement)("p", { className: "nwe-tutorial-note" }, (0, react.createElement)("strong", null, "用哪个会话:"), " 想获得完整的引擎保证,请新开一个「网文工坊」预设会话;普通会话里模型可能还挂着文件工具,能绕过引擎直接改稿。")), (0, react.createElement)("section", { className: "nwe-tutorial-block" }, (0, react.createElement)("h3", null, "一本书的完整流程"), (0, react.createElement)("p", null, "前四步是开书准备,后面是每章循环。第一次先按顺序走完一章,熟悉后再批量推进整本。"), (0, react.createElement)("ol", { className: "nwe-steps" }, ...TUTORIAL_STEPS.map((step, index) => tutorialStep(step, index)))), (0, react.createElement)("section", { className: "nwe-tutorial-block" }, (0, react.createElement)("h3", null, "面板怎么看"), (0, react.createElement)("div", { className: "nwe-tutorial-cards" }, (0, react.createElement)("div", { className: "nwe-tutorial-card" }, (0, react.createElement)("strong", null, "顶部统计"), (0, react.createElement)("span", null, "已归档章、总字数、平均分、待处理门控。数字来自引擎,不是正文估算。")), (0, react.createElement)("div", { className: "nwe-tutorial-card" }, (0, react.createElement)("strong", null, "九阶段轨与下一步"), (0, react.createElement)("span", null, "亮起的阶段是当前进度,“下一步”卡会直接给出该调用的工具名。")), (0, react.createElement)("div", { className: "nwe-tutorial-card" }, (0, react.createElement)("strong", null, "章节与门控"), (0, react.createElement)("span", null, "章节卡展开可看字数、评分与状态;门控区按 code/detail/hint 列出所有阻塞。"))), (0, react.createElement)("p", { className: "nwe-tutorial-note" }, (0, react.createElement)("strong", null, "刷新:"), " 面板每 15 秒静默同步一次,也可以点右上角刷新。它是只读投影;不会替你改稿、归档或修状态。"), (0, react.createElement)("p", { className: "nwe-tutorial-note" }, (0, react.createElement)("strong", null, "项目状态:"), "选择器里 choose 表示发现多本书、还没选;项目不在了表示目录缺失,绑定仍会保留;不是项目表示目录还在但没有 novel.yaml 或项目状态;项目不可读表示权限或 IO 错误,先修权限或换一本。面板选择只影响查看,不改变任何会话的写作绑定。")), (0, react.createElement)("section", { className: "nwe-tutorial-block" }, (0, react.createElement)("h3", null, "卡住时先看这里"), (0, react.createElement)("ul", { className: "nwe-faq" }, (0, react.createElement)("li", null, (0, react.createElement)("strong", null, "为什么一直让我先领 brief?"), (0, react.createElement)("p", null, "这是正常门控。任务书决定当前阶段可用的字段与上下文 digest;先按返回值调用 novel_brief,再继续。")), (0, react.createElement)("li", null, (0, react.createElement)("strong", null, "门控失败,改一条还是全改?"), (0, react.createElement)("p", null, "一次看完所有 blocking,再一起改。逐条试会反复烧掉阶段轮次;错误里的 code 会告诉你属于参数、门控还是系统故障。")), (0, react.createElement)("li", null, (0, react.createElement)("strong", null, "字数和评分为什么对不上?"), (0, react.createElement)("p", null, "字数以 engine 返回为准,可用 novel_word_count 核对;review 的 total 必须等于 150 减全部扣分,不能另算一套。")), (0, react.createElement)("li", null, (0, react.createElement)("strong", null, "P4/P6 过不去怎么办?"), (0, react.createElement)("p", null, "P4 有 blocker 就领 P5 任务书修订,再回 P4 重评;P6 失败同样回修订。不要 waive blocker,也不要只跑半个 scope 就归档。")), (0, react.createElement)("li", null, (0, react.createElement)("strong", null, "想改已经归档的章节?"), (0, react.createElement)("p", null, "先 novel_reopen,带非空 reason。它会从归档备份恢复,重开后重新走 P3→P8;不要直接编辑归档卡或正文文件。")), (0, react.createElement)("li", null, (0, react.createElement)("strong", null, "状态文档坏了能手工修吗?"), (0, react.createElement)("p", null, "先用 novel_state 看 raw 和 next;需要修复时用 novel_amend,并在 reason 写清原因。手改 YAML 会丢失事务保护。")), (0, react.createElement)("li", null, (0, react.createElement)("strong", null, "有多个项目,面板看的是哪本?"), (0, react.createElement)("p", null, "面板用顶部项目选择器切换查看对象;选择只改 ?root= 和浏览器 localStorage,不调用 activate、不写会话绑定。写作项目请用 /novel use 或显式 project_root。")), (0, react.createElement)("li", null, (0, react.createElement)("strong", null, "选错书、想清掉绑定怎么办?"), (0, react.createElement)("p", null, "先用 /novel current 看当前会话绑定;要换书用 /novel use <路径|书名|.>,要清掉只影响当前会话的绑定用 /novel unbind。不要靠 active 猜,也不要手改 state/session-bindings.yaml。")), (0, react.createElement)("li", null, (0, react.createElement)("strong", null, "扫描到了,为什么还没切过去?"), (0, react.createElement)("p", null, "scan 只读发现;scan --register 只登记项目,不选择、不改 active、不绑会话。真正切换写作项目要用 /novel use,或第一次调用显式传 project_root。")))), (0, react.createElement)("section", { className: "nwe-tutorial-block" }, (0, react.createElement)("h3", null, "可以直接复制给模型的起手话术"), (0, react.createElement)(CopyPrompt, {
|
|
1620
|
+
text: TUTORIAL_PROMPT,
|
|
1621
|
+
label: "复制起手话术"
|
|
1622
|
+
})), (0, react.createElement)("section", { className: "nwe-tutorial-block" }, (0, react.createElement)("h3", null, "续写下一章时,直接复制这段给 Agent"), (0, react.createElement)("p", null, "这段会先确认状态,再处理缺章卡和目标章数已满,最后按 P1→P8 推进;归档前会要求你确认 state_delta。"), (0, react.createElement)(CopyPrompt, {
|
|
1623
|
+
text: TUTORIAL_NEXT_PROMPT,
|
|
1624
|
+
label: "复制续写指令"
|
|
1625
|
+
})), (0, react.createElement)("section", { className: "nwe-tutorial-block" }, (0, react.createElement)("h3", null, "一章完成检查清单"), (0, react.createElement)("ul", { className: "nwe-checklist" }, (0, react.createElement)("li", null, "本章 P1/P2 计划已提交,P3 正文已通过格式与字数门控。"), (0, react.createElement)("li", null, "P4 评审 total 对账无误,没有未修 blocker。"), (0, react.createElement)("li", null, "P6 五个语义项都有不同句子的正文引用,完整 scope 通过。"), (0, react.createElement)("li", null, "P8 任务书当前,novel_archive 带完整 state_delta 成功。"), (0, react.createElement)("li", null, "面板出现已归档章节,门控区没有新的阻塞。")))), (0, react.createElement)("div", { className: "nwe-modal-foot" }, `教程基于 ${PANEL_VERSION} · 具体字段以当前任务书和工具返回为准 · 面板只读`)));
|
|
1523
1626
|
const NovelHelp = () => {
|
|
1524
1627
|
const [open, setOpen] = (0, react.useState)(false);
|
|
1525
1628
|
const triggerRef = (0, react.useRef)(null);
|
|
@@ -1539,6 +1642,18 @@ brief P6 → check → brief P8 → archive 的顺序推进。
|
|
|
1539
1642
|
wasOpen.current = true;
|
|
1540
1643
|
const previousOverflow = doc.body.style.overflow;
|
|
1541
1644
|
doc.body.style.overflow = "hidden";
|
|
1645
|
+
const panelRoot = triggerRef.current?.closest("[data-plugin=\"dsh-novel-write-engine\"]") ?? doc.querySelector("[data-plugin=\"dsh-novel-write-engine\"]");
|
|
1646
|
+
const inerted = [];
|
|
1647
|
+
if (panelRoot !== null) for (const child of Array.from(panelRoot.children)) {
|
|
1648
|
+
if (child.classList.contains("nwe-modal-backdrop")) continue;
|
|
1649
|
+
inerted.push({
|
|
1650
|
+
element: child,
|
|
1651
|
+
inert: child.getAttribute("inert"),
|
|
1652
|
+
aria: child.getAttribute("aria-hidden")
|
|
1653
|
+
});
|
|
1654
|
+
child.setAttribute("inert", "");
|
|
1655
|
+
child.setAttribute("aria-hidden", "true");
|
|
1656
|
+
}
|
|
1542
1657
|
const onKeyDown = (event) => {
|
|
1543
1658
|
if (event.key === "Escape") {
|
|
1544
1659
|
event.preventDefault();
|
|
@@ -1550,8 +1665,18 @@ brief P6 → check → brief P8 → archive 的顺序推进。
|
|
|
1550
1665
|
return () => {
|
|
1551
1666
|
doc.removeEventListener("keydown", onKeyDown);
|
|
1552
1667
|
doc.body.style.overflow = previousOverflow;
|
|
1668
|
+
for (const item of inerted) {
|
|
1669
|
+
if (item.inert === null) item.element.removeAttribute("inert");
|
|
1670
|
+
else item.element.setAttribute("inert", item.inert);
|
|
1671
|
+
if (item.aria === null) item.element.removeAttribute("aria-hidden");
|
|
1672
|
+
else item.element.setAttribute("aria-hidden", item.aria);
|
|
1673
|
+
}
|
|
1553
1674
|
};
|
|
1554
1675
|
}, [open]);
|
|
1676
|
+
const doc = globalThis.document;
|
|
1677
|
+
const panelRoot = triggerRef.current?.closest("[data-plugin=\"dsh-novel-write-engine\"]") ?? doc?.querySelector("[data-plugin=\"dsh-novel-write-engine\"]") ?? null;
|
|
1678
|
+
const dialog = open ? tutorialDialog(close, closeRef) : null;
|
|
1679
|
+
const overlay = dialog !== null && panelRoot !== null ? (0, react_dom.createPortal)(dialog, panelRoot) : dialog;
|
|
1555
1680
|
return (0, react.createElement)(react.Fragment, null, (0, react.createElement)("button", {
|
|
1556
1681
|
type: "button",
|
|
1557
1682
|
className: "nwe-help",
|
|
@@ -1561,7 +1686,7 @@ brief P6 → check → brief P8 → archive 的顺序推进。
|
|
|
1561
1686
|
"aria-haspopup": "dialog",
|
|
1562
1687
|
"aria-expanded": open,
|
|
1563
1688
|
title: "插件使用教程"
|
|
1564
|
-
}, helpIcon(), (0, react.createElement)("span", { className: "nwe-help-label" }, "教程")),
|
|
1689
|
+
}, helpIcon(), (0, react.createElement)("span", { className: "nwe-help-label" }, "教程")), overlay);
|
|
1565
1690
|
};
|
|
1566
1691
|
const nextCard = (view) => (0, react.createElement)("section", {
|
|
1567
1692
|
className: "nwe-next",
|
|
@@ -1580,7 +1705,7 @@ brief P6 → check → brief P8 → archive 的顺序推进。
|
|
|
1580
1705
|
className: "nwe-block-icon",
|
|
1581
1706
|
"aria-hidden": "true"
|
|
1582
1707
|
}, "!"), (0, react.createElement)("div", null, (0, react.createElement)("div", { className: "nwe-block-code" }, item.code), (0, react.createElement)("div", { className: "nwe-block-detail" }, item.detail), item.hint === "" ? null : (0, react.createElement)("div", { className: "nwe-block-hint" }, item.hint))))));
|
|
1583
|
-
const chaptersSection = (view) => (0, react.createElement)("section", { className: "nwe-section nwe-chapters-section" }, (0, react.createElement)("div", { className: "nwe-section-head" }, (0, react.createElement)("h2", { className: "nwe-section-title" }, "章节"), (0, react.createElement)("span", { className: "nwe-section-note" }, `${view.stats.chapters}
|
|
1708
|
+
const chaptersSection = (view) => (0, react.createElement)("section", { className: "nwe-section nwe-chapters-section" }, (0, react.createElement)("div", { className: "nwe-section-head" }, (0, react.createElement)("h2", { className: "nwe-section-title" }, "章节"), (0, react.createElement)("span", { className: "nwe-section-note" }, `${view.stats.chapters} 已归档 · 显示 ${view.rows.length} 条${view.stats.chapters > view.rows.filter((row) => row.tone === "ok").length ? `(最近 ${view.rows.length} 条)` : ""}`)), view.rows.length === 0 ? (0, react.createElement)("div", {
|
|
1584
1709
|
className: "nwe-empty",
|
|
1585
1710
|
style: {
|
|
1586
1711
|
marginTop: 12,
|
|
@@ -1600,7 +1725,13 @@ brief P6 → check → brief P8 → archive 的顺序推进。
|
|
|
1600
1725
|
const dims = data.quality.dimensions.map((dimension) => (0, react.createElement)("div", {
|
|
1601
1726
|
className: "nwe-v6-bar",
|
|
1602
1727
|
key: dimension.id
|
|
1603
|
-
}, (0, react.createElement)("span", null, dimension.id), (0, react.createElement)("span",
|
|
1728
|
+
}, (0, react.createElement)("span", null, dimension.id), (0, react.createElement)("span", {
|
|
1729
|
+
role: "progressbar",
|
|
1730
|
+
"aria-label": `${dimension.id} 得分`,
|
|
1731
|
+
"aria-valuemin": 0,
|
|
1732
|
+
"aria-valuemax": dimension.max === void 0 || dimension.max <= 0 ? 10 : dimension.max,
|
|
1733
|
+
"aria-valuenow": dimension.average
|
|
1734
|
+
}, (0, react.createElement)("i", { style: { width: `${Math.min(100, Math.max(0, dimension.average / (dimension.max === void 0 || dimension.max <= 0 ? 10 : dimension.max) * 100))}%` } })), (0, react.createElement)("span", null, dimension.samples === 0 ? "—" : dimension.average.toFixed(1))));
|
|
1604
1735
|
const worldEntries = (entries) => entries.map((entry) => (0, react.createElement)("li", { key: entry.name }, (0, react.createElement)("b", null, entry.name), (0, react.createElement)("span", null, entry.description), entry.anchors === void 0 || entry.anchors.length === 0 ? null : (0, react.createElement)("span", { className: "nwe-v6-note" }, `锚点:${entry.anchors.join(" / ")}`)));
|
|
1605
1736
|
return (0, react.createElement)("section", {
|
|
1606
1737
|
className: "nwe-section",
|
|
@@ -1608,7 +1739,7 @@ brief P6 → check → brief P8 → archive 的顺序推进。
|
|
|
1608
1739
|
}, (0, react.createElement)("div", { className: "nwe-section-head" }, (0, react.createElement)("h2", { className: "nwe-section-title" }, "这本书"), (0, react.createElement)("span", { className: "nwe-section-note" }, "只读投影 · 不含正文")), (0, react.createElement)("div", { className: "nwe-v6-grid" }, v6Card("bp", "书脊与蓝图", [v6Kv([
|
|
1609
1740
|
["作者", data.identity.author || "未填写"],
|
|
1610
1741
|
["简介", data.identity.description || "未填写"],
|
|
1611
|
-
["创建", data.identity.created_at
|
|
1742
|
+
["创建", localTime(data.identity.created_at)],
|
|
1612
1743
|
["目标", `${data.identity.targets.words_per_chapter[0]}–${data.identity.targets.words_per_chapter[1]} 字 / 章 · ${data.identity.targets.total_chapters ?? "不限"} 章`],
|
|
1613
1744
|
["主题", data.blueprint.themes.join(" / ") || "—"],
|
|
1614
1745
|
["基调 / POV", `${data.blueprint.tone || "—"} / ${data.blueprint.pov || "—"}`],
|
|
@@ -1617,12 +1748,12 @@ brief P6 → check → brief P8 → archive 的顺序推进。
|
|
|
1617
1748
|
]), (0, react.createElement)("p", { className: "nwe-v6-note" }, data.blueprint.logline || data.blueprint.premise || "还没有蓝图摘要")], true), v6Card("progress", "进度与当前章", [v6Kv([
|
|
1618
1749
|
["已归档", `${data.progress.archived} / ${data.progress.target ?? "不限"} 章`],
|
|
1619
1750
|
["字数", `归档 ${data.progress.words_archived} · 含草稿 ${data.progress.words_with_draft}`],
|
|
1620
|
-
["剩余", data.progress.remaining_words === null ? "—" : `${data.progress.remaining_words} 字(${data.progress.remaining_words_basis})`],
|
|
1751
|
+
["剩余", data.progress.remaining_words === null ? "—" : `${data.progress.remaining_words} 字(${remainingBasisLabel(data.progress.remaining_words_basis)})`],
|
|
1621
1752
|
["当前章", `${currentLabel} · ${data.current.card?.title ?? "无章卡"}`],
|
|
1622
1753
|
["轮次", `评 ${data.current.rounds.review} / 修 ${data.current.rounds.revise} / 检 ${data.current.rounds.check}`],
|
|
1623
1754
|
["正文 hash", data.current.manuscript_hash ?? "—"],
|
|
1624
1755
|
["当前字数", data.current.metrics === null ? "—" : String(data.current.metrics.total_word_count)]
|
|
1625
|
-
]), data.current.card === null ? null : (0, react.createElement)("div", { className: "nwe-v6-tags" }, ...data.current.card.cast.map((name) => (0, react.createElement)("span", { key: name }, name)))]), v6Card("cast", "人物与关系", [v6Kv([["角色总数", String(data.cast.total)], ["关系", `${data.relationships.total} 条 · ${Object.entries(data.relationships.types).map(([type, count]) => `${type} ${count}`).join(" / ") || "—"}`]]), (0, react.createElement)("ul", { className: "nwe-v6-list" }, ...data.cast.top.map((person) => (0, react.createElement)("li", { key: person.name }, (0, react.createElement)("b", null, person.name), (0, react.createElement)("span", null, `${person.role} · 出场 ${person.appearances} · 最近第 ${person.last_seen} 章`))))]), v6Card("world", "世界与设定", [
|
|
1756
|
+
]), data.current.card === null ? null : (0, react.createElement)("div", { className: "nwe-v6-tags" }, ...data.current.card.cast.map((name) => (0, react.createElement)("span", { key: name }, name)))]), v6Card("volumes", "卷进度", [view.book.volumes.length === 0 ? (0, react.createElement)("p", { className: "nwe-v6-note" }, "还没有卷") : (0, react.createElement)("ul", { className: "nwe-v6-list" }, ...view.book.volumes.map((volume) => (0, react.createElement)("li", { key: `${volume.title}-${volume.range}` }, (0, react.createElement)("b", null, volume.title), (0, react.createElement)("span", null, `${volume.range} · ${volume.planned} 计划 / ${volume.archived} 归档`))))]), v6Card("cast", "人物与关系", [v6Kv([["角色总数", String(data.cast.total)], ["关系", `${data.relationships.total} 条 · ${Object.entries(data.relationships.types).map(([type, count]) => `${type} ${count}`).join(" / ") || "—"}`]]), (0, react.createElement)("ul", { className: "nwe-v6-list" }, ...data.cast.top.map((person) => (0, react.createElement)("li", { key: person.name }, (0, react.createElement)("b", null, person.name), (0, react.createElement)("span", null, `${person.role} · 出场 ${person.appearances} · 最近第 ${person.last_seen} 章`))))]), v6Card("world", "世界与设定", [
|
|
1626
1757
|
v6Kv([
|
|
1627
1758
|
["地点 / 势力", `${data.worldbook.counts.locations} / ${data.worldbook.counts.factions}`],
|
|
1628
1759
|
["规则 / 术语", `${data.worldbook.counts.rules} / ${data.worldbook.counts.glossary}`],
|
|
@@ -1638,37 +1769,522 @@ brief P6 → check → brief P8 → archive 的顺序推进。
|
|
|
1638
1769
|
]), v6Card("foreshadow", "伏笔与时间线", [v6Kv([["伏笔", `未解 ${data.foreshadows.open} · 休眠 ${data.foreshadows.dormant} · 逾期 ${data.foreshadows.overdue} · 已解 ${data.foreshadows.resolved}`], ["时间线", `${data.timeline.events} 个事件 · 第 ${data.timeline.first_chapter ?? "—"}–${data.timeline.last_chapter ?? "—"} 章`]]), (0, react.createElement)("ul", { className: "nwe-v6-list" }, ...data.foreshadows.oldest.map((item) => (0, react.createElement)("li", { key: item.id }, (0, react.createElement)("b", null, item.id), (0, react.createElement)("span", null, `${item.title} · 已 ${item.age} 章`))))]), v6Card("quality", "质量与节奏", [
|
|
1639
1770
|
v6Kv([
|
|
1640
1771
|
["全书均分", `${data.quality.average.toFixed(1)} · 中位 ${data.quality.median.toFixed(1)} · ${data.quality.min}–${data.quality.max}`],
|
|
1641
|
-
["近十均分", data.quality.last_ten_average.toFixed(1)],
|
|
1642
|
-
["样本 / 评审", `${data.quality.samples} / ${data.quality.attempts}`],
|
|
1643
|
-
["趋势", data.quality.trend === null ? data.quality.trend_basis : `${data.quality.trend >= 0 ? "+" : ""}${data.quality.trend}`],
|
|
1644
|
-
["节奏",
|
|
1645
|
-
["钩子 / 推进", `${
|
|
1772
|
+
["近十均分", data.quality.last_ten_average === null ? "—" : data.quality.last_ten_average.toFixed(1)],
|
|
1773
|
+
["样本 / 评审", data.quality.samples === 0 ? "见质量页" : `${data.quality.samples} / ${data.quality.attempts}`],
|
|
1774
|
+
["趋势", data.quality.trend === null ? data.quality.samples === 0 ? "见质量页" : data.quality.trend_basis === "insufficient_samples" ? "样本不足" : data.quality.trend_basis === "insufficient_history" ? "历史不足" : "最近五章对比前五章" : `${data.quality.trend >= 0 ? "+" : ""}${data.quality.trend}`],
|
|
1775
|
+
["节奏", countsText(data.pacing.scene_types)],
|
|
1776
|
+
["钩子 / 推进", `${countsText(data.pacing.hook_strength, hookLabel)} / ${countsText(data.pacing.plot_advancement, plotLabel)}`],
|
|
1646
1777
|
["最长 / 最短", data.pacing.longest === null ? "—" : `第 ${data.pacing.longest.chapter} 章 ${data.pacing.longest.words} 字 / 第 ${data.pacing.shortest?.chapter ?? "—"} 章 ${data.pacing.shortest?.words ?? "—"} 字`],
|
|
1647
1778
|
["返工", `评 ${data.effort.reviews} · 修 ${data.effort.revisions} · 检 ${data.effort.checks} · 回滚 ${data.effort.rollbacks}`]
|
|
1648
1779
|
]),
|
|
1649
|
-
(0, react.createElement)("div", {
|
|
1780
|
+
dims.length === 0 ? (0, react.createElement)("div", { className: "nwe-v6-note" }, "质量维度请切换到「质量」页查看;概览只取轻量摘要。") : (0, react.createElement)("div", {
|
|
1650
1781
|
className: "nwe-v6-bars",
|
|
1651
1782
|
"aria-label": "十五维质量"
|
|
1652
1783
|
}, ...dims),
|
|
1653
|
-
(0, react.createElement)("details", null, (0, react.createElement)("summary", null, "逐章 effort"), (0, react.createElement)("ul", { className: "nwe-v6-list" }, ...data.effort.chapters.map((chapter) => (0, react.createElement)("li", { key: chapter.num }, (0, react.createElement)("b", null, `第 ${chapter.num} 章`), (0, react.createElement)("span", null, `评 ${chapter.reviews} / 修 ${chapter.revisions} / 检 ${chapter.checks} / 回滚 ${chapter.rollbacks} · ${chapter.final_check
|
|
1784
|
+
(0, react.createElement)("details", null, (0, react.createElement)("summary", null, "逐章 effort"), (0, react.createElement)("ul", { className: "nwe-v6-list" }, ...data.effort.chapters.map((chapter) => (0, react.createElement)("li", { key: chapter.num }, (0, react.createElement)("b", null, `第 ${chapter.num} 章`), (0, react.createElement)("span", null, `评 ${chapter.reviews} / 修 ${chapter.revisions} / 检 ${chapter.checks} / 回滚 ${chapter.rollbacks} · ${finalCheckLabel(chapter.final_check)}`)))))
|
|
1654
1785
|
]), v6Card("health", "健康与恢复", [
|
|
1655
1786
|
v6Kv([
|
|
1656
1787
|
["大纲", data.health.outline.present ? "可读" : data.health.outline.error === null ? "还没有大纲" : "损坏"],
|
|
1657
1788
|
["世界观", data.health.worldbook.present ? "可读" : data.health.worldbook.error === null ? "还没有设定" : "损坏"],
|
|
1658
|
-
["pending", data.health.pending === null ? "无" : `${data.health.pending.state} · ${data.health.pending.detail}`]
|
|
1789
|
+
["pending", data.health.pending === null ? "无" : `${statusLabel(data.health.pending.state)} · ${data.health.pending.detail}`]
|
|
1659
1790
|
]),
|
|
1660
1791
|
data.health.outline.error === null ? null : (0, react.createElement)("p", { className: "nwe-v6-note" }, data.health.outline.error),
|
|
1661
1792
|
data.health.worldbook.error === null ? null : (0, react.createElement)("p", { className: "nwe-v6-note" }, data.health.worldbook.error),
|
|
1662
1793
|
(0, react.createElement)("ul", { className: "nwe-v6-list" }, ...data.health.state_errors.map((error) => (0, react.createElement)("li", { key: `${error.path}:${error.code}` }, (0, react.createElement)("b", null, error.code), (0, react.createElement)("span", null, `${error.path}: ${error.message}`))))
|
|
1663
1794
|
])));
|
|
1664
1795
|
};
|
|
1796
|
+
const PANEL_TABS = [
|
|
1797
|
+
{
|
|
1798
|
+
id: "overview",
|
|
1799
|
+
label: "概览"
|
|
1800
|
+
},
|
|
1801
|
+
{
|
|
1802
|
+
id: "chapters",
|
|
1803
|
+
label: "章节"
|
|
1804
|
+
},
|
|
1805
|
+
{
|
|
1806
|
+
id: "characters",
|
|
1807
|
+
label: "人物"
|
|
1808
|
+
},
|
|
1809
|
+
{
|
|
1810
|
+
id: "foreshadows",
|
|
1811
|
+
label: "线索"
|
|
1812
|
+
},
|
|
1813
|
+
{
|
|
1814
|
+
id: "worldbook",
|
|
1815
|
+
label: "设定"
|
|
1816
|
+
},
|
|
1817
|
+
{
|
|
1818
|
+
id: "quality",
|
|
1819
|
+
label: "质量"
|
|
1820
|
+
},
|
|
1821
|
+
{
|
|
1822
|
+
id: "health",
|
|
1823
|
+
label: "健康"
|
|
1824
|
+
}
|
|
1825
|
+
];
|
|
1826
|
+
const EMPTY_CHAPTER_QUERY = {
|
|
1827
|
+
search: "",
|
|
1828
|
+
status: "all",
|
|
1829
|
+
scene: "",
|
|
1830
|
+
sort: "num_desc",
|
|
1831
|
+
cursor: ""
|
|
1832
|
+
};
|
|
1833
|
+
const tabBar = (tab, onTab) => (0, react.createElement)("nav", {
|
|
1834
|
+
className: "nwe-tabs",
|
|
1835
|
+
role: "tablist",
|
|
1836
|
+
"aria-label": "网文工坊栏目"
|
|
1837
|
+
}, ...PANEL_TABS.map((item) => (0, react.createElement)("button", {
|
|
1838
|
+
key: item.id,
|
|
1839
|
+
type: "button",
|
|
1840
|
+
role: "tab",
|
|
1841
|
+
id: `nwe-tab-${item.id}`,
|
|
1842
|
+
"aria-controls": `nwe-panel-${item.id}`,
|
|
1843
|
+
"aria-selected": tab === item.id,
|
|
1844
|
+
className: `nwe-tab${tab === item.id ? " is-active" : ""}`,
|
|
1845
|
+
onClick: () => onTab(item.id)
|
|
1846
|
+
}, item.label)));
|
|
1847
|
+
const asRow = (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
|
|
1848
|
+
const textOf = (value, fallback = "—") => typeof value === "string" && value !== "" ? value : typeof value === "number" && Number.isFinite(value) ? String(value) : fallback;
|
|
1849
|
+
const roleLabel = (value) => value === "protagonist" ? "主角" : value === "supporting" ? "配角" : value === "extra" ? "龙套" : textOf(value);
|
|
1850
|
+
const STATUS_LABELS = {
|
|
1851
|
+
planted: "已埋",
|
|
1852
|
+
advanced: "已推进",
|
|
1853
|
+
resolved: "已回收",
|
|
1854
|
+
archived: "已归档",
|
|
1855
|
+
draft: "草稿",
|
|
1856
|
+
dormant: "休眠",
|
|
1857
|
+
planned: "计划",
|
|
1858
|
+
writing: "写作中",
|
|
1859
|
+
done: "完成",
|
|
1860
|
+
open: "待处理",
|
|
1861
|
+
fixed: "已修复",
|
|
1862
|
+
waived: "已豁免",
|
|
1863
|
+
committed: "已提交",
|
|
1864
|
+
partial: "不完整",
|
|
1865
|
+
complete: "已完成",
|
|
1866
|
+
untouched: "未开始",
|
|
1867
|
+
recoverable: "可恢复",
|
|
1868
|
+
abortable: "可中止"
|
|
1869
|
+
};
|
|
1870
|
+
const statusLabel = (value) => typeof value === "string" ? STATUS_LABELS[value] ?? value : textOf(value);
|
|
1871
|
+
const finalCheckLabel = (value) => value === "pass" ? "通过" : value === "fail" ? "未过" : "—";
|
|
1872
|
+
const remainingBasisLabel = (value) => value === "actual_average" ? "按已归档章均字" : value === "minimum_target" ? "按最低目标" : value === "none" ? "无" : value;
|
|
1873
|
+
const countsText = (value, label = (key) => key) => {
|
|
1874
|
+
const row = asRow(value);
|
|
1875
|
+
if (row === null) return textOf(value);
|
|
1876
|
+
const entries = Object.entries(row).filter(([, count]) => typeof count === "number");
|
|
1877
|
+
return entries.length === 0 ? "—" : entries.map(([key, count]) => `${label(key)} ${count}`).join(" · ");
|
|
1878
|
+
};
|
|
1879
|
+
const hookLabel = (key) => key === "1" ? "弱" : key === "2" ? "较弱" : key === "3" ? "中" : key === "4" ? "较强" : key === "5" ? "强" : key;
|
|
1880
|
+
const plotLabel = (key) => key === "high" ? "高" : key === "mid" ? "中" : key === "low" ? "低" : key;
|
|
1881
|
+
const sectionKv = (row) => {
|
|
1882
|
+
const fields = [
|
|
1883
|
+
["角色", roleLabel(row.role)],
|
|
1884
|
+
["状态", statusLabel(row.status)],
|
|
1885
|
+
["首次出场", textOf(row.first_appearance ?? row.first)],
|
|
1886
|
+
["最近出场", textOf(row.last)],
|
|
1887
|
+
["出场次数", textOf(row.appearances)],
|
|
1888
|
+
["类型", textOf(row.type)],
|
|
1889
|
+
["地点", textOf(row.location)],
|
|
1890
|
+
["双方", row.a === void 0 ? "—" : `${textOf(row.a)} ↔ ${textOf(row.b)}`],
|
|
1891
|
+
["别名", textOf(row.alias)],
|
|
1892
|
+
["数值", textOf(row.value)],
|
|
1893
|
+
["单位", textOf(row.unit)],
|
|
1894
|
+
["描述", textOf(row.description)],
|
|
1895
|
+
["锚点", textOf(row.anchor)],
|
|
1896
|
+
["容忍", textOf(row.tolerance)],
|
|
1897
|
+
["备注", textOf(row.note)],
|
|
1898
|
+
["推进记录", Array.isArray(row.advance_log) ? `${row.advance_log.length} 条` : "—"],
|
|
1899
|
+
["成长", Array.isArray(row.growth) ? `${row.growth.length} 条` : "—"],
|
|
1900
|
+
["当前状态", textOf(row.current_state)],
|
|
1901
|
+
["人物档案", textOf(row.profile)],
|
|
1902
|
+
["章龄", textOf(row.age)],
|
|
1903
|
+
["术语", textOf(row.term)],
|
|
1904
|
+
["释义", textOf(row.definition)],
|
|
1905
|
+
["时间", textOf(row.time)],
|
|
1906
|
+
["事件", textOf(row.event)]
|
|
1907
|
+
];
|
|
1908
|
+
const used = /* @__PURE__ */ new Set([
|
|
1909
|
+
"role",
|
|
1910
|
+
"status",
|
|
1911
|
+
"first_appearance",
|
|
1912
|
+
"first",
|
|
1913
|
+
"last",
|
|
1914
|
+
"appearances",
|
|
1915
|
+
"type",
|
|
1916
|
+
"location",
|
|
1917
|
+
"a",
|
|
1918
|
+
"b",
|
|
1919
|
+
"alias",
|
|
1920
|
+
"value",
|
|
1921
|
+
"unit",
|
|
1922
|
+
"description",
|
|
1923
|
+
"anchor",
|
|
1924
|
+
"tolerance",
|
|
1925
|
+
"note",
|
|
1926
|
+
"advance_log",
|
|
1927
|
+
"growth"
|
|
1928
|
+
]);
|
|
1929
|
+
const extraLabels = {
|
|
1930
|
+
average: "平均",
|
|
1931
|
+
samples: "可读评审",
|
|
1932
|
+
median: "中位",
|
|
1933
|
+
min: "最小",
|
|
1934
|
+
max: "最大",
|
|
1935
|
+
trend: "趋势",
|
|
1936
|
+
attempts: "评审轮次",
|
|
1937
|
+
reviews: "评",
|
|
1938
|
+
revisions: "修",
|
|
1939
|
+
checks: "检",
|
|
1940
|
+
rollbacks: "回滚",
|
|
1941
|
+
chapters: "章节",
|
|
1942
|
+
words: "字数",
|
|
1943
|
+
total: "总数",
|
|
1944
|
+
present: "存在",
|
|
1945
|
+
error: "错误",
|
|
1946
|
+
path: "路径",
|
|
1947
|
+
code: "代码",
|
|
1948
|
+
message: "信息",
|
|
1949
|
+
chapter: "章",
|
|
1950
|
+
field: "字段",
|
|
1951
|
+
planned: "计划",
|
|
1952
|
+
actual: "实际",
|
|
1953
|
+
state: "状态",
|
|
1954
|
+
detail: "详情",
|
|
1955
|
+
txid: "事务",
|
|
1956
|
+
num: "章号",
|
|
1957
|
+
scene_type: "场景",
|
|
1958
|
+
goal: "目标",
|
|
1959
|
+
conflict: "冲突",
|
|
1960
|
+
hook: "钩子",
|
|
1961
|
+
notes: "备注",
|
|
1962
|
+
story_time: "故事时间",
|
|
1963
|
+
since: "始于",
|
|
1964
|
+
word_count: "字数",
|
|
1965
|
+
score: "评分",
|
|
1966
|
+
archived_at: "归档时间",
|
|
1967
|
+
characters: "人物",
|
|
1968
|
+
evolution: "演变"
|
|
1969
|
+
};
|
|
1970
|
+
for (const [key, value] of Object.entries(row)) {
|
|
1971
|
+
if (used.has(key) || key === "title" || key === "name" || key === "id") continue;
|
|
1972
|
+
const label = extraLabels[key];
|
|
1973
|
+
if (key === "archived_at") {
|
|
1974
|
+
fields.push([label, localTime(typeof value === "string" ? value : null)]);
|
|
1975
|
+
continue;
|
|
1976
|
+
}
|
|
1977
|
+
if (label !== void 0 && (typeof value === "string" || typeof value === "number" || typeof value === "boolean")) fields.push([label, textOf(value)]);
|
|
1978
|
+
}
|
|
1979
|
+
const visible = fields.filter(([, value]) => value !== "—");
|
|
1980
|
+
return (0, react.createElement)("dl", { className: "nwe-kv" }, ...visible.flatMap(([label, value]) => [(0, react.createElement)("dt", { key: `dt-${label}` }, label), (0, react.createElement)("dd", { key: `dd-${label}` }, value)]));
|
|
1981
|
+
};
|
|
1982
|
+
const sectionList = (title, items) => {
|
|
1983
|
+
if (!Array.isArray(items) || items.length === 0) return (0, react.createElement)("div", { className: "nwe-empty" }, `${title}:暂无数据`);
|
|
1984
|
+
return (0, react.createElement)("div", { className: "nwe-section-list" }, ...items.map((item, index) => {
|
|
1985
|
+
const row = asRow(item);
|
|
1986
|
+
const label = row === null ? String(item) : textOf(row.title ?? row.name ?? row.id ?? row.alias ?? row.a ?? row.term ?? row.event, `第 ${index + 1} 条`);
|
|
1987
|
+
return (0, react.createElement)("details", {
|
|
1988
|
+
className: "nwe-data-row",
|
|
1989
|
+
key: `${label}-${index}`
|
|
1990
|
+
}, (0, react.createElement)("summary", null, label), row === null ? (0, react.createElement)("div", { className: "nwe-data-json" }, String(item)) : sectionKv(row));
|
|
1991
|
+
}));
|
|
1992
|
+
};
|
|
1993
|
+
const sectionBlock = (title, items, key) => {
|
|
1994
|
+
const list = Array.isArray(items) ? items : [];
|
|
1995
|
+
if (list.length === 0) return null;
|
|
1996
|
+
return (0, react.createElement)("section", {
|
|
1997
|
+
className: "nwe-subsection",
|
|
1998
|
+
key
|
|
1999
|
+
}, (0, react.createElement)("h3", null, `${title}(${list.length})`), sectionList(title, list));
|
|
2000
|
+
};
|
|
2001
|
+
const worldbookSection = (items) => {
|
|
2002
|
+
const row = asRow(items);
|
|
2003
|
+
if (row === null) return sectionList("设定", items);
|
|
2004
|
+
const groups = [
|
|
2005
|
+
["rules", "规则"],
|
|
2006
|
+
["factions", "势力"],
|
|
2007
|
+
["locations", "地点"],
|
|
2008
|
+
["characters", "人物小传"],
|
|
2009
|
+
["glossary", "术语"],
|
|
2010
|
+
["backstory", "背景"],
|
|
2011
|
+
["facts", "数值事实"],
|
|
2012
|
+
["themes", "主题"],
|
|
2013
|
+
["outline_cards", "章卡"],
|
|
2014
|
+
["drift", "大纲漂移"]
|
|
2015
|
+
];
|
|
2016
|
+
const ledger = asRow(row.ledger);
|
|
2017
|
+
const blueprint = asRow(row.blueprint);
|
|
2018
|
+
const volumes = Array.isArray(row.volumes) ? row.volumes : [];
|
|
2019
|
+
const intro = [
|
|
2020
|
+
typeof row.premise === "string" && row.premise !== "" ? {
|
|
2021
|
+
title: "前提",
|
|
2022
|
+
description: row.premise
|
|
2023
|
+
} : null,
|
|
2024
|
+
typeof row.setting === "string" && row.setting !== "" ? {
|
|
2025
|
+
title: "设定",
|
|
2026
|
+
description: row.setting
|
|
2027
|
+
} : null,
|
|
2028
|
+
blueprint === null ? null : {
|
|
2029
|
+
title: "蓝图",
|
|
2030
|
+
description: [
|
|
2031
|
+
Array.isArray(blueprint.themes) ? `主题 ${blueprint.themes.join(" / ")}` : "",
|
|
2032
|
+
typeof blueprint.tone === "string" ? `基调 ${blueprint.tone}` : "",
|
|
2033
|
+
typeof blueprint.pov === "string" ? `POV ${blueprint.pov}` : "",
|
|
2034
|
+
typeof blueprint.ending === "string" ? `结局 ${blueprint.ending}` : ""
|
|
2035
|
+
].filter((part) => part !== "").join(" · ")
|
|
2036
|
+
},
|
|
2037
|
+
volumes.length === 0 ? null : {
|
|
2038
|
+
title: "卷",
|
|
2039
|
+
description: volumes.map((volume) => {
|
|
2040
|
+
const item = asRow(volume);
|
|
2041
|
+
return item === null ? "" : `${textOf(item.title)} ${textOf(item.start_chapter)}–${textOf(item.end_chapter)}`;
|
|
2042
|
+
}).filter((part) => part !== "").join(" / ")
|
|
2043
|
+
}
|
|
2044
|
+
].filter((entry) => entry !== null);
|
|
2045
|
+
const ledgerGroups = [
|
|
2046
|
+
["locations", "台账地点"],
|
|
2047
|
+
["abilities", "台账能力"],
|
|
2048
|
+
["artifacts", "台账物品"],
|
|
2049
|
+
["factions", "台账势力"],
|
|
2050
|
+
["rules", "台账规则"]
|
|
2051
|
+
];
|
|
2052
|
+
const blocks = [
|
|
2053
|
+
...intro.length === 0 ? [] : [sectionBlock("蓝图", intro, "blueprint")],
|
|
2054
|
+
...groups.map(([key, label]) => sectionBlock(label, row[key], key)),
|
|
2055
|
+
...ledgerGroups.map(([key, label]) => sectionBlock(label, ledger?.[key], `ledger-${key}`))
|
|
2056
|
+
].filter((block) => block !== null);
|
|
2057
|
+
return blocks.length === 0 ? (0, react.createElement)("div", { className: "nwe-empty" }, "设定:暂无数据") : (0, react.createElement)("div", { className: "nwe-section-list" }, ...blocks);
|
|
2058
|
+
};
|
|
2059
|
+
const qualitySection = (items, chapter = 0, detail = null, onDetail = () => void 0) => {
|
|
2060
|
+
const row = asRow(items);
|
|
2061
|
+
if (row === null) return sectionList("质量", items);
|
|
2062
|
+
const quality = asRow(row.quality);
|
|
2063
|
+
const dimensions = Array.isArray(row.dimensions) ? row.dimensions : [];
|
|
2064
|
+
const summary = quality === null || Number(quality.samples ?? 0) === 0 ? (0, react.createElement)("div", { className: "nwe-empty" }, "质量:暂无评审") : (0, react.createElement)("div", { className: "nwe-quality-summary" }, (0, react.createElement)("span", null, `均分 ${textOf(quality.average)}`), (0, react.createElement)("span", null, `可读评审 ${textOf(quality.samples)}`), (0, react.createElement)("span", null, `中位 ${textOf(quality.median)}`), (0, react.createElement)("span", null, `区间 ${textOf(quality.min)}–${textOf(quality.max)}`));
|
|
2065
|
+
const bars = dimensions.map((entry, index) => {
|
|
2066
|
+
const dimension = asRow(entry);
|
|
2067
|
+
if (dimension === null) return null;
|
|
2068
|
+
const average = Number(dimension.average);
|
|
2069
|
+
const max = Number(dimension.max);
|
|
2070
|
+
const id = textOf(dimension.id, `维度 ${index + 1}`);
|
|
2071
|
+
const percent = max > 0 ? Math.max(0, Math.min(100, average / max * 100)) : 0;
|
|
2072
|
+
return (0, react.createElement)("div", {
|
|
2073
|
+
className: "nwe-dim",
|
|
2074
|
+
key: id
|
|
2075
|
+
}, (0, react.createElement)("span", null, id), (0, react.createElement)("div", {
|
|
2076
|
+
className: "nwe-v6-bar",
|
|
2077
|
+
role: "progressbar",
|
|
2078
|
+
"aria-label": `${id} 得分`,
|
|
2079
|
+
"aria-valuemin": 0,
|
|
2080
|
+
"aria-valuemax": max,
|
|
2081
|
+
"aria-valuenow": Number.isFinite(average) ? average : 0
|
|
2082
|
+
}, (0, react.createElement)("span", null, (0, react.createElement)("i", { style: { width: `${percent}%` } }))), (0, react.createElement)("span", null, `${Number.isFinite(average) ? average : "—"} / ${max > 0 ? max : "—"}`));
|
|
2083
|
+
});
|
|
2084
|
+
const pacing = asRow(row.pacing);
|
|
2085
|
+
const longest = asRow(pacing?.longest);
|
|
2086
|
+
const shortest = asRow(pacing?.shortest);
|
|
2087
|
+
const pacingRows = pacing === null ? [] : [
|
|
2088
|
+
{
|
|
2089
|
+
title: "节奏",
|
|
2090
|
+
description: countsText(pacing.scene_types)
|
|
2091
|
+
},
|
|
2092
|
+
{
|
|
2093
|
+
title: "钩子 / 推进",
|
|
2094
|
+
description: `${countsText(pacing.hook_strength, hookLabel)} / ${countsText(pacing.plot_advancement, plotLabel)}`
|
|
2095
|
+
},
|
|
2096
|
+
{
|
|
2097
|
+
title: "最长 / 最短",
|
|
2098
|
+
description: pacing.longest === null ? "—" : `第 ${textOf(longest?.chapter)} 章 ${textOf(longest?.words)} 字 / 第 ${textOf(shortest?.chapter)} 章 ${textOf(shortest?.words)} 字`
|
|
2099
|
+
}
|
|
2100
|
+
];
|
|
2101
|
+
const extras = [pacingRows.length === 0 ? null : sectionBlock("节奏", pacingRows, "pacing"), sectionBlock("投入", row.effort === null || row.effort === void 0 ? [] : [row.effort], "effort")].filter((block) => block !== null);
|
|
2102
|
+
return (0, react.createElement)("div", { className: "nwe-section-list" }, summary, chapter > 0 ? (0, react.createElement)("button", {
|
|
2103
|
+
type: "button",
|
|
2104
|
+
className: "nwe-refresh",
|
|
2105
|
+
onClick: () => onDetail(chapter)
|
|
2106
|
+
}, "查看本章质量详情") : null, ...bars, ...extras, qualityDetailPanel(detail, onDetail));
|
|
2107
|
+
};
|
|
2108
|
+
const healthSection = (items) => {
|
|
2109
|
+
const row = asRow(items);
|
|
2110
|
+
if (row === null) return sectionList("健康", items);
|
|
2111
|
+
const flags = ["outline", "worldbook"].map((key) => {
|
|
2112
|
+
const value = asRow(row[key]);
|
|
2113
|
+
const present = value?.present === true;
|
|
2114
|
+
const existed = value?.file_existed === true;
|
|
2115
|
+
return (0, react.createElement)("span", {
|
|
2116
|
+
className: `nwe-badge ${present ? "is-ok" : "is-bad"}`,
|
|
2117
|
+
key
|
|
2118
|
+
}, `${key === "outline" ? "大纲" : "设定"}:${present ? "可读" : existed ? "损坏" : "缺失"}`);
|
|
2119
|
+
});
|
|
2120
|
+
const stateErrors = Array.isArray(row.state_errors) ? row.state_errors : [];
|
|
2121
|
+
const drift = Array.isArray(row.drift) ? row.drift : [];
|
|
2122
|
+
const blocks = [sectionBlock("状态错误", stateErrors, "state-errors"), sectionBlock("大纲漂移", drift, "drift")].filter((block) => block !== null);
|
|
2123
|
+
return (0, react.createElement)("div", { className: "nwe-section-list" }, (0, react.createElement)("div", { className: "nwe-health-flags" }, ...flags), ...blocks);
|
|
2124
|
+
};
|
|
2125
|
+
const chapterToolbar = (query, onQuery) => {
|
|
2126
|
+
return (0, react.createElement)("div", {
|
|
2127
|
+
className: "nwe-chapter-toolbar",
|
|
2128
|
+
role: "search",
|
|
2129
|
+
"aria-label": "筛选章节"
|
|
2130
|
+
}, (0, react.createElement)("label", null, "搜索标题", (0, react.createElement)("input", {
|
|
2131
|
+
type: "search",
|
|
2132
|
+
value: query.search,
|
|
2133
|
+
placeholder: "输入章节标题",
|
|
2134
|
+
onChange: (event) => onQuery({
|
|
2135
|
+
search: event.target.value,
|
|
2136
|
+
cursor: ""
|
|
2137
|
+
})
|
|
2138
|
+
})), (0, react.createElement)("label", null, "状态", (0, react.createElement)("select", {
|
|
2139
|
+
value: query.status,
|
|
2140
|
+
onChange: (event) => onQuery({
|
|
2141
|
+
status: event.target.value,
|
|
2142
|
+
cursor: ""
|
|
2143
|
+
})
|
|
2144
|
+
}, (0, react.createElement)("option", { value: "all" }, "全部"), (0, react.createElement)("option", { value: "archived" }, "已归档"), (0, react.createElement)("option", { value: "draft" }, "草稿"))), (0, react.createElement)("label", null, "场景", (0, react.createElement)("select", {
|
|
2145
|
+
value: query.scene,
|
|
2146
|
+
onChange: (event) => onQuery({
|
|
2147
|
+
scene: event.target.value,
|
|
2148
|
+
cursor: ""
|
|
2149
|
+
})
|
|
2150
|
+
}, (0, react.createElement)("option", { value: "" }, "全部"), ...[
|
|
2151
|
+
"战斗/动作章",
|
|
2152
|
+
"日常过渡章",
|
|
2153
|
+
"情感爆发章",
|
|
2154
|
+
"纯叙事章"
|
|
2155
|
+
].map((scene) => (0, react.createElement)("option", {
|
|
2156
|
+
value: scene,
|
|
2157
|
+
key: scene
|
|
2158
|
+
}, scene)))), (0, react.createElement)("label", null, "排序", (0, react.createElement)("select", {
|
|
2159
|
+
value: query.sort,
|
|
2160
|
+
onChange: (event) => onQuery({
|
|
2161
|
+
sort: event.target.value,
|
|
2162
|
+
cursor: ""
|
|
2163
|
+
})
|
|
2164
|
+
}, (0, react.createElement)("option", { value: "num_desc" }, "章号倒序"), (0, react.createElement)("option", { value: "num_asc" }, "章号正序"))));
|
|
2165
|
+
};
|
|
2166
|
+
const chapterDetailPanel = (detail, onClose) => {
|
|
2167
|
+
if (detail === null) return null;
|
|
2168
|
+
const payload = detail.section.payload;
|
|
2169
|
+
const items = asRow(payload?.items);
|
|
2170
|
+
const manuscript = typeof items?.manuscript === "string" ? items.manuscript : null;
|
|
2171
|
+
const plan = typeof items?.plan === "string" ? items.plan : null;
|
|
2172
|
+
const worldPlan = typeof items?.world_plan === "string" ? items.world_plan : null;
|
|
2173
|
+
const detailRow = asRow(items?.row);
|
|
2174
|
+
const rawSummaryEvents = items?.summary_events ?? detailRow?.summary_events;
|
|
2175
|
+
const summaryEvents = Array.isArray(rawSummaryEvents) ? rawSummaryEvents.filter((event) => typeof event === "string") : [];
|
|
2176
|
+
return (0, react.createElement)("section", {
|
|
2177
|
+
className: "nwe-chapter-detail",
|
|
2178
|
+
"aria-label": `第 ${detail.chapter} 章详情`
|
|
2179
|
+
}, (0, react.createElement)("div", { className: "nwe-section-head" }, (0, react.createElement)("h3", { className: "nwe-section-title" }, `第 ${detail.chapter} 章详情`), (0, react.createElement)("button", {
|
|
2180
|
+
type: "button",
|
|
2181
|
+
className: "nwe-refresh",
|
|
2182
|
+
onClick: () => onClose(null)
|
|
2183
|
+
}, "收起")), detail.section.loading && payload === null ? (0, react.createElement)("div", { className: "nwe-section-note" }, "正在读取章节详情…") : detail.section.error !== null ? (0, react.createElement)("div", { className: "nwe-empty" }, `章节详情读取失败:${detail.section.error}`) : (0, react.createElement)(react.Fragment, null, manuscript === null ? (0, react.createElement)("div", { className: "nwe-empty" }, "正文尚未写入。") : (0, react.createElement)("details", { open: true }, (0, react.createElement)("summary", null, `正文 · ${Array.from(manuscript).length} 字`), (0, react.createElement)("pre", { className: "nwe-detail-text" }, manuscript)), plan === null ? null : (0, react.createElement)("details", null, (0, react.createElement)("summary", null, "归档 plan"), (0, react.createElement)("pre", { className: "nwe-detail-text" }, plan)), worldPlan === null ? null : (0, react.createElement)("details", null, (0, react.createElement)("summary", null, "世界计划"), (0, react.createElement)("pre", { className: "nwe-detail-text" }, worldPlan)), summaryEvents.length === 0 ? null : (0, react.createElement)("details", null, (0, react.createElement)("summary", null, "本章摘要"), (0, react.createElement)("ul", { className: "nwe-v6-list" }, ...summaryEvents.map((event, index) => (0, react.createElement)("li", { key: `${index}-${event}` }, event))))));
|
|
2184
|
+
};
|
|
2185
|
+
const qualityDetailPanel = (detail, onClose) => {
|
|
2186
|
+
if (detail === null) return null;
|
|
2187
|
+
const payload = detail.section.payload;
|
|
2188
|
+
const items = asRow(payload?.items);
|
|
2189
|
+
const review = typeof items?.review === "string" ? items.review : null;
|
|
2190
|
+
const check = typeof items?.check === "string" ? items.check : null;
|
|
2191
|
+
return (0, react.createElement)("section", {
|
|
2192
|
+
className: "nwe-chapter-detail",
|
|
2193
|
+
"aria-label": `第 ${detail.chapter} 章质量详情`
|
|
2194
|
+
}, (0, react.createElement)("div", { className: "nwe-section-head" }, (0, react.createElement)("h3", { className: "nwe-section-title" }, `第 ${detail.chapter} 章质量详情`), (0, react.createElement)("button", {
|
|
2195
|
+
type: "button",
|
|
2196
|
+
className: "nwe-refresh",
|
|
2197
|
+
onClick: () => onClose(null)
|
|
2198
|
+
}, "收起")), detail.section.loading && payload === null ? (0, react.createElement)("div", { className: "nwe-section-note" }, "正在读取质量详情…") : detail.section.error !== null ? (0, react.createElement)("div", { className: "nwe-empty" }, `质量详情读取失败:${detail.section.error}`) : review === null && check === null ? (0, react.createElement)("div", { className: "nwe-empty" }, "还没有评审或检查文档。") : (0, react.createElement)(react.Fragment, null, review === null ? null : (0, react.createElement)("details", { open: true }, (0, react.createElement)("summary", null, "评审 review"), (0, react.createElement)("pre", { className: "nwe-detail-text" }, review)), check === null ? null : (0, react.createElement)("details", null, (0, react.createElement)("summary", null, "检查 check"), (0, react.createElement)("pre", { className: "nwe-detail-text" }, check))));
|
|
2199
|
+
};
|
|
2200
|
+
const chapterTable = (section, view, query, onQuery, detail = null, onDetail = () => void 0) => {
|
|
2201
|
+
if (section === null || section.loading && section.payload === null) return chaptersSection(view);
|
|
2202
|
+
if (section.error !== null && section.payload === null) return (0, react.createElement)(react.Fragment, null, (0, react.createElement)("div", { className: "nwe-empty" }, `章节读取失败:${section.error}`), chaptersSection(view));
|
|
2203
|
+
const payload = section.payload;
|
|
2204
|
+
const items = payload?.items;
|
|
2205
|
+
if (!Array.isArray(items)) return chaptersSection(view);
|
|
2206
|
+
const rows = items.map(asRow).filter((row) => row !== null);
|
|
2207
|
+
const total = payload?.total ?? rows.length;
|
|
2208
|
+
const cursor = query.cursor;
|
|
2209
|
+
return (0, react.createElement)("section", {
|
|
2210
|
+
className: "nwe-chapters-section",
|
|
2211
|
+
"aria-label": "章节表"
|
|
2212
|
+
}, (0, react.createElement)("div", { className: "nwe-section-head" }, (0, react.createElement)("h2", { className: "nwe-section-title" }, "章节"), (0, react.createElement)("span", { className: "nwe-section-note" }, payload?.truncated === true && rows.length < total ? `显示 ${rows.length} / 共 ${total}` : payload?.truncated === true ? `共 ${total} 章 · 部分文本已截断` : `共 ${total} 章`)), rows.length === 0 ? (0, react.createElement)("div", { className: "nwe-empty" }, "没有匹配的章节。") : (0, react.createElement)("div", { className: "nwe-table-wrap" }, (0, react.createElement)("table", { className: "nwe-chapter-table" }, (0, react.createElement)("thead", null, (0, react.createElement)("tr", null, ...[
|
|
2213
|
+
"章",
|
|
2214
|
+
"标题",
|
|
2215
|
+
"状态",
|
|
2216
|
+
"字数",
|
|
2217
|
+
"评分",
|
|
2218
|
+
"场景",
|
|
2219
|
+
"终检",
|
|
2220
|
+
"归档",
|
|
2221
|
+
"详情"
|
|
2222
|
+
].map((label) => (0, react.createElement)("th", {
|
|
2223
|
+
key: label,
|
|
2224
|
+
scope: "col"
|
|
2225
|
+
}, label)))), (0, react.createElement)("tbody", null, ...rows.map((row, index) => {
|
|
2226
|
+
const num = textOf(row.num, String(index + 1));
|
|
2227
|
+
const score = typeof row.score === "number" ? String(row.score) : "—";
|
|
2228
|
+
return (0, react.createElement)("tr", { key: `${num}-${index}` }, (0, react.createElement)("td", { className: "is-num" }, `第 ${num} 章`), (0, react.createElement)("td", { className: "nwe-table-title" }, textOf(row.title)), (0, react.createElement)("td", null, row.status === "draft" ? "草稿" : "已归档"), (0, react.createElement)("td", null, textOf(row.word_count)), (0, react.createElement)("td", null, score), (0, react.createElement)("td", null, textOf(row.scene_type)), (0, react.createElement)("td", null, finalCheckLabel(row.final_check)), (0, react.createElement)("td", null, localTime(typeof row.archived_at === "string" ? row.archived_at : null)), (0, react.createElement)("td", null, (0, react.createElement)("button", {
|
|
2229
|
+
type: "button",
|
|
2230
|
+
className: "nwe-refresh",
|
|
2231
|
+
onClick: () => onDetail(Number(num) || index + 1)
|
|
2232
|
+
}, "详情")));
|
|
2233
|
+
})))), (0, react.createElement)("div", { className: "nwe-pager" }, (0, react.createElement)("span", null, payload?.next_cursor !== null && payload?.next_cursor !== void 0 ? "已截断,可继续翻页" : payload?.truncated === true ? "部分文本已截断" : "已显示全部"), (0, react.createElement)("span", null, cursor === "" ? null : (0, react.createElement)("button", {
|
|
2234
|
+
type: "button",
|
|
2235
|
+
className: "nwe-refresh",
|
|
2236
|
+
onClick: () => onQuery({ cursor: "" })
|
|
2237
|
+
}, "回到第一页"), payload?.next_cursor === null || payload?.next_cursor === void 0 ? null : (0, react.createElement)("button", {
|
|
2238
|
+
type: "button",
|
|
2239
|
+
className: "nwe-refresh",
|
|
2240
|
+
onClick: () => onQuery({ cursor: String(payload.next_cursor) })
|
|
2241
|
+
}, "下一页"))), chapterDetailPanel(detail, onDetail));
|
|
2242
|
+
};
|
|
2243
|
+
const leadsSection = (items) => {
|
|
2244
|
+
const row = asRow(items);
|
|
2245
|
+
if (row === null) return sectionList("线索", items);
|
|
2246
|
+
const blocks = [
|
|
2247
|
+
sectionBlock("伏笔", row.foreshadows, "foreshadows"),
|
|
2248
|
+
sectionBlock("时间线", row.timeline, "timeline"),
|
|
2249
|
+
sectionBlock("关系", row.relationships, "relationships")
|
|
2250
|
+
].filter((block) => block !== null);
|
|
2251
|
+
return blocks.length === 0 ? (0, react.createElement)("div", { className: "nwe-empty" }, "线索:暂无数据") : (0, react.createElement)("div", { className: "nwe-section-list" }, ...blocks);
|
|
2252
|
+
};
|
|
2253
|
+
const sectionMeta = (section) => {
|
|
2254
|
+
const payload = section.payload;
|
|
2255
|
+
if (payload === null || payload.total === void 0) return null;
|
|
2256
|
+
const returned = payload.returned ?? 0;
|
|
2257
|
+
const text = payload.truncated !== true ? `共 ${payload.total}` : returned < payload.total ? `显示 ${returned} / 共 ${payload.total}` : `共 ${payload.total} · 部分文本已截断`;
|
|
2258
|
+
return (0, react.createElement)("div", {
|
|
2259
|
+
className: "nwe-section-note",
|
|
2260
|
+
role: "status"
|
|
2261
|
+
}, text);
|
|
2262
|
+
};
|
|
2263
|
+
const sectionPanel = (tab, section, search = "", onSearch = () => void 0, chapter = 0, qualityDetail = null, onQualityDetail = () => void 0) => {
|
|
2264
|
+
if (section === null || section.loading && section.payload === null) return (0, react.createElement)("div", { className: "nwe-section-note" }, "正在读取栏目…");
|
|
2265
|
+
if (section.error !== null && section.payload === null) return (0, react.createElement)("div", { className: "nwe-empty" }, `栏目读取失败:${section.error}`);
|
|
2266
|
+
const items = section.payload?.items;
|
|
2267
|
+
const searchLabel = tab === "characters" ? "搜索人物" : tab === "foreshadows" ? "搜索线索" : tab === "worldbook" ? "搜索设定" : "";
|
|
2268
|
+
const withSearch = (content) => (0, react.createElement)(react.Fragment, null, (0, react.createElement)("label", { className: "nwe-section-search" }, searchLabel, (0, react.createElement)("input", {
|
|
2269
|
+
type: "search",
|
|
2270
|
+
value: search,
|
|
2271
|
+
placeholder: "输入关键词",
|
|
2272
|
+
onChange: (event) => onSearch(event.target.value)
|
|
2273
|
+
})), sectionMeta(section), content);
|
|
2274
|
+
if (tab === "characters") return withSearch(sectionList("人物", items));
|
|
2275
|
+
if (tab === "foreshadows") return withSearch(leadsSection(items));
|
|
2276
|
+
if (tab === "worldbook") return withSearch(worldbookSection(items));
|
|
2277
|
+
if (tab === "quality") return qualitySection(items, chapter, qualityDetail, onQualityDetail);
|
|
2278
|
+
if (tab === "health") return healthSection(items);
|
|
2279
|
+
return sectionList("数据", Array.isArray(items) ? items : [items]);
|
|
2280
|
+
};
|
|
1665
2281
|
/**
|
|
1666
2282
|
* Pure view: given a view model, return the element tree. Keeping the hooks in
|
|
1667
2283
|
* `NovelPanel` makes every visual state renderable without a browser.
|
|
1668
2284
|
*/
|
|
1669
|
-
function panelElement(view, loading, onRefresh, onSelectProject = () => void 0) {
|
|
2285
|
+
function panelElement(view, loading, onRefresh, onSelectProject = () => void 0, tab = "overview", onTab = () => void 0, section = null, chapterQuery = EMPTY_CHAPTER_QUERY, onChapterQuery = () => void 0, sectionSearch = "", onSectionSearch = () => void 0, chapterDetail = null, onChapterDetail = () => void 0, qualityDetail = null, onQualityDetail = () => void 0) {
|
|
1670
2286
|
const ready = view.state === "ready";
|
|
1671
|
-
const selectable = ready || view.state === "choose" || view.state === "missing" || view.state === "
|
|
2287
|
+
const selectable = view.projects.length > 0 || ready || view.state === "choose" || view.state === "missing" || view.state === "error" || view.state === "outdated" || view.state === "empty";
|
|
1672
2288
|
return (0, react.createElement)("section", {
|
|
1673
2289
|
"data-plugin": "dsh-novel-write-engine",
|
|
1674
2290
|
className: "nwe-root",
|
|
@@ -1686,14 +2302,29 @@ brief P6 → check → brief P8 → archive 的顺序推进。
|
|
|
1686
2302
|
}, refreshIcon(), loading ? "刷新中…" : "刷新"))), ready && view.damaged ? (0, react.createElement)("div", {
|
|
1687
2303
|
className: "nwe-damaged",
|
|
1688
2304
|
role: "status"
|
|
1689
|
-
}, "项目定义不可读;先用 novel_state 查看,再用 novel_amend 按 next 修复。面板不会把它伪装成正常项目。") : null, ready && view.nextTitle !== "" ? nextCard(view) : null, ready ? (0, react.createElement)("div", {
|
|
2305
|
+
}, "项目定义不可读;先用 novel_state 查看,再用 novel_amend 按 next 修复。面板不会把它伪装成正常项目。") : null, ready && view.nextTitle !== "" ? nextCard(view) : null, ready ? tabBar(tab, onTab) : null, ready && tab === "overview" ? (0, react.createElement)("div", {
|
|
1690
2306
|
className: "nwe-stats",
|
|
1691
2307
|
role: "group",
|
|
1692
2308
|
"aria-label": "项目统计"
|
|
1693
|
-
}, stat("已归档", String(view.stats.chapters), "章"), stat("总字数", view.stats.words.toLocaleString("zh-CN"), (view.stats.draftWords ?? 0) > 0 ? `字(归档 ${(view.stats.archivedWords ?? 0).toLocaleString("zh-CN")} + 草稿 ${view.stats.draftWords.toLocaleString("zh-CN")})` : "字"), stat("全书均分", view.stats.chapters === 0 ? "—" : String(view.stats.averageScore), "/150"), stat("待处理", String(view.stats.blocking), "项")) : null, ready ? bookDataSection(view) : null, view.state === "loading" ? loadingState() : null, view.state === "choose" ? choosePanel(view, onSelectProject) : null, view.state === "missing" ? missingPanel(view) : null, view.state === "error" || view.state === "empty" || view.state === "outdated" ? statePanel(view.state, view, loading, onRefresh) : null, ready
|
|
2309
|
+
}, stat("已归档", String(view.stats.chapters), "章"), stat("总字数", view.stats.words.toLocaleString("zh-CN"), (view.stats.draftWords ?? 0) > 0 ? `字(归档 ${(view.stats.archivedWords ?? 0).toLocaleString("zh-CN")} + 草稿 ${view.stats.draftWords.toLocaleString("zh-CN")})` : "字"), stat("全书均分", view.stats.chapters === 0 ? "—" : String(view.stats.averageScore), "/150"), stat("待处理", String(view.stats.blocking), "项")) : null, ready && tab === "overview" ? bookDataSection(view) ?? bookSection(view) : null, view.state === "loading" ? loadingState() : null, view.state === "choose" ? choosePanel(view, onSelectProject) : null, view.state === "missing" ? missingPanel(view) : null, view.state === "error" || view.state === "empty" || view.state === "outdated" ? statePanel(view.state, view, loading, onRefresh) : null, ready && tab === "overview" ? (0, react.createElement)("div", {
|
|
2310
|
+
className: "nwe-desk",
|
|
2311
|
+
role: "tabpanel",
|
|
2312
|
+
id: "nwe-panel-overview",
|
|
2313
|
+
"aria-labelledby": "nwe-tab-overview"
|
|
2314
|
+
}, (0, react.createElement)("div", { className: "nwe-desk-main" }, (0, react.createElement)("div", { className: "nwe-desk-heading" }, (0, react.createElement)("span", { className: "nwe-desk-kicker" }, "当前一章"), (0, react.createElement)("span", { className: "nwe-desk-note" }, view.stageLabel)), phases(view), blockingSection(view), advisoriesSection(view), pendingSection(view), legacySection(view), chaptersSection(view)), (0, react.createElement)("aside", {
|
|
1694
2315
|
className: "nwe-desk-side",
|
|
1695
2316
|
"aria-label": "全书与活动"
|
|
1696
|
-
},
|
|
2317
|
+
}, activitySection(view))) : null, ready && tab === "chapters" ? (0, react.createElement)("div", {
|
|
2318
|
+
className: "nwe-desk",
|
|
2319
|
+
role: "tabpanel",
|
|
2320
|
+
id: "nwe-panel-chapters",
|
|
2321
|
+
"aria-labelledby": "nwe-tab-chapters"
|
|
2322
|
+
}, (0, react.createElement)("div", { className: "nwe-desk-main" }, blockingSection(view), chapterToolbar(chapterQuery, onChapterQuery), chapterTable(section, view, chapterQuery, onChapterQuery, chapterDetail, onChapterDetail))) : null, ready && tab !== "overview" && tab !== "chapters" ? (0, react.createElement)("section", {
|
|
2323
|
+
className: "nwe-section nwe-tab-panel",
|
|
2324
|
+
role: "tabpanel",
|
|
2325
|
+
id: `nwe-panel-${tab}`,
|
|
2326
|
+
"aria-labelledby": `nwe-tab-${tab}`
|
|
2327
|
+
}, tab === "health" ? (0, react.createElement)(react.Fragment, null, blockingSection(view), advisoriesSection(view), pendingSection(view), legacySection(view)) : null, sectionPanel(tab, section, sectionSearch, onSectionSearch, view.chapter, qualityDetail, onQualityDetail)) : null, footer(view, onRefresh));
|
|
1697
2328
|
}
|
|
1698
2329
|
const PANEL_ROOT_KEY = "nwe.panel-root.v1";
|
|
1699
2330
|
const storage = () => globalThis.localStorage;
|
|
@@ -1715,22 +2346,42 @@ brief P6 → check → brief P8 → archive 的顺序推进。
|
|
|
1715
2346
|
};
|
|
1716
2347
|
const panelPath = (root) => {
|
|
1717
2348
|
const base = PANEL_ROUTE.replace(/^\/+/, "");
|
|
1718
|
-
return root === null ? base : `${base}?root=${encodeURIComponent(root)}`;
|
|
2349
|
+
return root === null ? `${base}?lean=1` : `${base}?root=${encodeURIComponent(root)}&lean=1`;
|
|
1719
2350
|
};
|
|
1720
2351
|
/** Stateful wrapper: fetch once, refresh on demand and after returning to the tab. */
|
|
1721
2352
|
function NovelPanel() {
|
|
1722
2353
|
const [view, setView] = (0, react.useState)(() => buildView(null));
|
|
1723
2354
|
const [loading, setLoading] = (0, react.useState)(true);
|
|
1724
2355
|
const [switching, setSwitching] = (0, react.useState)(false);
|
|
2356
|
+
const [sync, setSync] = (0, react.useState)({
|
|
2357
|
+
lastSync: null,
|
|
2358
|
+
syncError: null
|
|
2359
|
+
});
|
|
2360
|
+
const [tab, setTab] = (0, react.useState)("overview");
|
|
2361
|
+
const [section, setSection] = (0, react.useState)(null);
|
|
2362
|
+
const [chapterQuery, setChapterQuery] = (0, react.useState)(EMPTY_CHAPTER_QUERY);
|
|
2363
|
+
const [sectionSearch, setSectionSearch] = (0, react.useState)("");
|
|
2364
|
+
const [chapterDetail, setChapterDetail] = (0, react.useState)(null);
|
|
2365
|
+
const [qualityDetail, setQualityDetail] = (0, react.useState)(null);
|
|
2366
|
+
const detailController = (0, react.useRef)(null);
|
|
2367
|
+
const [revisionTick, setRevisionTick] = (0, react.useState)(0);
|
|
2368
|
+
const syncFailures = (0, react.useRef)(0);
|
|
2369
|
+
const nextSyncAt = (0, react.useRef)(0);
|
|
1725
2370
|
const [selectedRoot, setSelectedRoot] = (0, react.useState)(() => readStoredRoot());
|
|
1726
2371
|
const alive = (0, react.useRef)(true);
|
|
1727
2372
|
const requestId = (0, react.useRef)(0);
|
|
1728
2373
|
const visibleRequest = (0, react.useRef)(0);
|
|
1729
2374
|
const controllerRef = (0, react.useRef)(null);
|
|
1730
2375
|
const queryRootRef = (0, react.useRef)(selectedRoot);
|
|
2376
|
+
const sectionEtagRef = (0, react.useRef)(null);
|
|
2377
|
+
const bundleEtagRef = (0, react.useRef)(null);
|
|
2378
|
+
const lastSilentAt = (0, react.useRef)(0);
|
|
2379
|
+
const quietPolls = (0, react.useRef)(0);
|
|
1731
2380
|
const loadRef = (0, react.useRef)(async () => void 0);
|
|
1732
2381
|
const load = (0, react.useCallback)(async (silent = false, root = queryRootRef.current) => {
|
|
1733
2382
|
if (silent && visibleRequest.current !== 0) return;
|
|
2383
|
+
if (silent && Date.now() - lastSilentAt.current < 1e3) return;
|
|
2384
|
+
if (silent) lastSilentAt.current = Date.now();
|
|
1734
2385
|
const id = ++requestId.current;
|
|
1735
2386
|
controllerRef.current?.abort();
|
|
1736
2387
|
const controller = new AbortController();
|
|
@@ -1744,8 +2395,36 @@ brief P6 → check → brief P8 → archive 的顺序推进。
|
|
|
1744
2395
|
const anySignal = timeout === void 0 ? controller.signal : typeof any === "function" ? any([controller.signal, timeout]) : controller.signal;
|
|
1745
2396
|
const timeoutTimer = timeout === void 0 || typeof any !== "function" ? setTimeout(() => controller.abort(), 1e4) : void 0;
|
|
1746
2397
|
try {
|
|
1747
|
-
const
|
|
2398
|
+
const rootKey = root ?? "";
|
|
2399
|
+
const cachedBundleEtag = bundleEtagRef.current?.root === rootKey ? bundleEtagRef.current.etag : void 0;
|
|
2400
|
+
const result = await fetchPanel(fetch, panelPath(root), anySignal, cachedBundleEtag);
|
|
1748
2401
|
if (!alive.current || id !== requestId.current) return;
|
|
2402
|
+
if (result.notModified === true) {
|
|
2403
|
+
syncFailures.current = 0;
|
|
2404
|
+
quietPolls.current += 1;
|
|
2405
|
+
nextSyncAt.current = Date.now() + (quietPolls.current >= 2 ? 6e4 : 3e4);
|
|
2406
|
+
setSync({
|
|
2407
|
+
lastSync: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2408
|
+
syncError: null
|
|
2409
|
+
});
|
|
2410
|
+
return;
|
|
2411
|
+
}
|
|
2412
|
+
if (result.error === null) {
|
|
2413
|
+
syncFailures.current = 0;
|
|
2414
|
+
quietPolls.current = 0;
|
|
2415
|
+
nextSyncAt.current = 0;
|
|
2416
|
+
setSync({
|
|
2417
|
+
lastSync: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2418
|
+
syncError: null
|
|
2419
|
+
});
|
|
2420
|
+
} else {
|
|
2421
|
+
syncFailures.current += 1;
|
|
2422
|
+
nextSyncAt.current = Date.now() + (syncFailures.current === 1 ? 3e4 : 6e4);
|
|
2423
|
+
setSync((previous) => ({
|
|
2424
|
+
...previous,
|
|
2425
|
+
syncError: result.error
|
|
2426
|
+
}));
|
|
2427
|
+
}
|
|
1749
2428
|
const payload = result.payload;
|
|
1750
2429
|
if (payload !== null && payload.missing?.reason === "not_registered" && root !== null) {
|
|
1751
2430
|
queryRootRef.current = null;
|
|
@@ -1754,7 +2433,14 @@ brief P6 → check → brief P8 → archive 的顺序推进。
|
|
|
1754
2433
|
await loadRef.current(false, null);
|
|
1755
2434
|
return;
|
|
1756
2435
|
}
|
|
1757
|
-
if (payload !== null)
|
|
2436
|
+
if (payload !== null) {
|
|
2437
|
+
if (payload.etag !== void 0) bundleEtagRef.current = {
|
|
2438
|
+
root: rootKey,
|
|
2439
|
+
etag: payload.etag
|
|
2440
|
+
};
|
|
2441
|
+
setSelectedRoot(payload.selection.root);
|
|
2442
|
+
setRevisionTick((tick) => tick + 1);
|
|
2443
|
+
}
|
|
1758
2444
|
setView((previous) => {
|
|
1759
2445
|
const next = buildView(payload, result.error);
|
|
1760
2446
|
if (silent && result.error !== null && previous.projects.length > 0) return previous;
|
|
@@ -1770,11 +2456,81 @@ brief P6 → check → brief P8 → archive 的顺序推进。
|
|
|
1770
2456
|
}
|
|
1771
2457
|
}, []);
|
|
1772
2458
|
loadRef.current = load;
|
|
2459
|
+
(0, react.useEffect)(() => {
|
|
2460
|
+
if (tab === "overview" || view.state !== "ready") {
|
|
2461
|
+
setSection(null);
|
|
2462
|
+
return;
|
|
2463
|
+
}
|
|
2464
|
+
const root = queryRootRef.current;
|
|
2465
|
+
const base = panelPath(root);
|
|
2466
|
+
const params = new URLSearchParams({ section: tab });
|
|
2467
|
+
if (tab === "chapters") {
|
|
2468
|
+
if (chapterQuery.search !== "") params.set("search", chapterQuery.search);
|
|
2469
|
+
if (chapterQuery.status !== "all") params.set("status", chapterQuery.status);
|
|
2470
|
+
if (chapterQuery.scene !== "") params.set("scene", chapterQuery.scene);
|
|
2471
|
+
params.set("sort", chapterQuery.sort);
|
|
2472
|
+
if (chapterQuery.cursor !== "") params.set("cursor", chapterQuery.cursor);
|
|
2473
|
+
} else if (tab === "characters" || tab === "foreshadows" || tab === "worldbook") {
|
|
2474
|
+
if (sectionSearch.trim() !== "") params.set("search", sectionSearch.trim());
|
|
2475
|
+
}
|
|
2476
|
+
const path = `${base}${base.includes("?") ? "&" : "?"}${params.toString()}`;
|
|
2477
|
+
const queryKey = `${tab}|${root ?? ""}|${JSON.stringify(chapterQuery)}|${sectionSearch}`;
|
|
2478
|
+
const cachedEtag = sectionEtagRef.current?.key === queryKey ? sectionEtagRef.current.etag : void 0;
|
|
2479
|
+
const controller = new AbortController();
|
|
2480
|
+
setSection((previous) => previous === null ? {
|
|
2481
|
+
loading: true,
|
|
2482
|
+
error: null,
|
|
2483
|
+
payload: null
|
|
2484
|
+
} : {
|
|
2485
|
+
...previous,
|
|
2486
|
+
loading: true,
|
|
2487
|
+
error: null
|
|
2488
|
+
});
|
|
2489
|
+
fetchPanel(fetch, path, controller.signal, cachedEtag).then((result) => {
|
|
2490
|
+
if (controller.signal.aborted) return;
|
|
2491
|
+
if (result.notModified === true) {
|
|
2492
|
+
setSection((previous) => previous === null ? {
|
|
2493
|
+
loading: false,
|
|
2494
|
+
error: null,
|
|
2495
|
+
payload: null
|
|
2496
|
+
} : {
|
|
2497
|
+
...previous,
|
|
2498
|
+
loading: false,
|
|
2499
|
+
error: null
|
|
2500
|
+
});
|
|
2501
|
+
return;
|
|
2502
|
+
}
|
|
2503
|
+
if (result.payload?.etag !== void 0) sectionEtagRef.current = {
|
|
2504
|
+
key: queryKey,
|
|
2505
|
+
etag: result.payload.etag
|
|
2506
|
+
};
|
|
2507
|
+
setSection({
|
|
2508
|
+
loading: false,
|
|
2509
|
+
error: result.error,
|
|
2510
|
+
payload: result.payload
|
|
2511
|
+
});
|
|
2512
|
+
});
|
|
2513
|
+
return () => controller.abort();
|
|
2514
|
+
}, [
|
|
2515
|
+
tab,
|
|
2516
|
+
view.state,
|
|
2517
|
+
selectedRoot,
|
|
2518
|
+
chapterQuery,
|
|
2519
|
+
revisionTick,
|
|
2520
|
+
sectionSearch
|
|
2521
|
+
]);
|
|
1773
2522
|
const selectProject = (0, react.useCallback)((root) => {
|
|
1774
2523
|
queryRootRef.current = root;
|
|
1775
2524
|
storeRoot(root);
|
|
1776
2525
|
setSelectedRoot(root);
|
|
2526
|
+
setChapterDetail(null);
|
|
2527
|
+
setQualityDetail(null);
|
|
2528
|
+
detailController.current?.abort();
|
|
1777
2529
|
setSwitching(true);
|
|
2530
|
+
setChapterQuery((previous) => ({
|
|
2531
|
+
...previous,
|
|
2532
|
+
cursor: ""
|
|
2533
|
+
}));
|
|
1778
2534
|
setView((previous) => ({
|
|
1779
2535
|
...previous,
|
|
1780
2536
|
state: "loading",
|
|
@@ -1799,7 +2555,9 @@ brief P6 → check → brief P8 → archive 的顺序推进。
|
|
|
1799
2555
|
const doc = globalThis.document;
|
|
1800
2556
|
if (win === void 0 || doc === void 0) return;
|
|
1801
2557
|
const refreshWhenVisible = () => {
|
|
1802
|
-
if (doc.visibilityState
|
|
2558
|
+
if (doc.visibilityState === "hidden") return;
|
|
2559
|
+
if (Date.now() < nextSyncAt.current) return;
|
|
2560
|
+
load(true);
|
|
1803
2561
|
};
|
|
1804
2562
|
const timer = win.setInterval(refreshWhenVisible, 15e3);
|
|
1805
2563
|
win.addEventListener("focus", refreshWhenVisible);
|
|
@@ -1807,15 +2565,55 @@ brief P6 → check → brief P8 → archive 的顺序推进。
|
|
|
1807
2565
|
return () => {
|
|
1808
2566
|
alive.current = false;
|
|
1809
2567
|
controllerRef.current?.abort();
|
|
2568
|
+
detailController.current?.abort();
|
|
1810
2569
|
win.clearInterval(timer);
|
|
1811
2570
|
win.removeEventListener("focus", refreshWhenVisible);
|
|
1812
2571
|
doc.removeEventListener("visibilitychange", refreshWhenVisible);
|
|
1813
2572
|
};
|
|
1814
2573
|
}, [load]);
|
|
2574
|
+
const loadDetail = (section, chapter, setter) => {
|
|
2575
|
+
detailController.current?.abort();
|
|
2576
|
+
if (chapter === null) {
|
|
2577
|
+
setter(null);
|
|
2578
|
+
return;
|
|
2579
|
+
}
|
|
2580
|
+
const root = queryRootRef.current;
|
|
2581
|
+
const controller = new AbortController();
|
|
2582
|
+
detailController.current = controller;
|
|
2583
|
+
const path = `${panelPath(root)}§ion=${section}&chapter=${encodeURIComponent(String(chapter))}`;
|
|
2584
|
+
setter({
|
|
2585
|
+
chapter,
|
|
2586
|
+
section: {
|
|
2587
|
+
loading: true,
|
|
2588
|
+
error: null,
|
|
2589
|
+
payload: null
|
|
2590
|
+
}
|
|
2591
|
+
});
|
|
2592
|
+
fetchPanel(fetch, path, controller.signal).then((result) => {
|
|
2593
|
+
if (!alive.current || controller.signal.aborted || queryRootRef.current !== root) return;
|
|
2594
|
+
setter({
|
|
2595
|
+
chapter,
|
|
2596
|
+
section: {
|
|
2597
|
+
loading: false,
|
|
2598
|
+
error: result.error,
|
|
2599
|
+
payload: result.payload
|
|
2600
|
+
}
|
|
2601
|
+
});
|
|
2602
|
+
});
|
|
2603
|
+
};
|
|
2604
|
+
const openChapterDetail = (chapter) => loadDetail("chapter_detail", chapter, setChapterDetail);
|
|
2605
|
+
const openQualityDetail = (chapter) => loadDetail("quality_detail", chapter, setQualityDetail);
|
|
1815
2606
|
return panelElement({
|
|
1816
2607
|
...view,
|
|
1817
|
-
selectedRoot
|
|
1818
|
-
|
|
2608
|
+
selectedRoot,
|
|
2609
|
+
...sync
|
|
2610
|
+
}, loading || switching, () => {
|
|
2611
|
+
nextSyncAt.current = 0;
|
|
2612
|
+
load(false, queryRootRef.current);
|
|
2613
|
+
}, selectProject, tab, setTab, section, chapterQuery, (patch) => setChapterQuery((previous) => ({
|
|
2614
|
+
...previous,
|
|
2615
|
+
...patch
|
|
2616
|
+
})), sectionSearch, setSectionSearch, chapterDetail, openChapterDetail, qualityDetail, openQualityDetail);
|
|
1819
2617
|
}
|
|
1820
2618
|
/** Sidebar icon: a small book mark, inheriting the current text colour. */
|
|
1821
2619
|
function NovelIcon({ size }) {
|