dsh-novel 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +149 -0
  3. package/cordis.patch.yml +3 -0
  4. package/lib/client.js +1560 -0
  5. package/lib/client.js.map +1 -0
  6. package/lib/index.js +1436 -0
  7. package/lib/index.js.map +1 -0
  8. package/lib/types/client/NovelWorkspaceView.d.ts +3 -0
  9. package/lib/types/client/entity-client.d.ts +17 -0
  10. package/lib/types/client/index.d.ts +3 -0
  11. package/lib/types/client/noveel-shapes.d.ts +1 -0
  12. package/lib/types/client/novel-api.d.ts +1 -0
  13. package/lib/types/client/section-icons.d.ts +5 -0
  14. package/lib/types/client/sections/ChapterSection.d.ts +2 -0
  15. package/lib/types/client/sections/LevelSection.d.ts +2 -0
  16. package/lib/types/client/sections/ListDetailSection.d.ts +13 -0
  17. package/lib/types/client/sections/OutlineSection.d.ts +2 -0
  18. package/lib/types/client/sections/OverviewSection.d.ts +2 -0
  19. package/lib/types/client/sections/SkillSection.d.ts +3 -0
  20. package/lib/types/client/sections/TimelineSection.d.ts +2 -0
  21. package/lib/types/client/sections/WorldSection.d.ts +2 -0
  22. package/lib/types/client/sections/owned-sections.d.ts +4 -0
  23. package/lib/types/client/sections/types.d.ts +14 -0
  24. package/lib/types/client/styles.d.ts +1 -0
  25. package/lib/types/client/ui/EntitySidebar.d.ts +10 -0
  26. package/lib/types/client/ui/RelationPanel.d.ts +6 -0
  27. package/lib/types/client/ui/TemplateRead.d.ts +13 -0
  28. package/lib/types/client/ui/TimelinePanel.d.ts +8 -0
  29. package/lib/types/client/ui/controls.d.ts +22 -0
  30. package/lib/types/client/ui/live-reload.d.ts +2 -0
  31. package/lib/types/index.d.ts +5 -0
  32. package/lib/types/novel/contract.d.ts +2 -0
  33. package/lib/types/novel/defaults.d.ts +66 -0
  34. package/lib/types/novel/domains.d.ts +12 -0
  35. package/lib/types/novel/host-skills.d.ts +58 -0
  36. package/lib/types/novel/http.d.ts +5 -0
  37. package/lib/types/novel/normalize-entity.d.ts +18 -0
  38. package/lib/types/novel/noveel-shapes.d.ts +98 -0
  39. package/lib/types/novel/outline-labels.d.ts +10 -0
  40. package/lib/types/novel/schema.d.ts +3 -0
  41. package/lib/types/novel/seed.d.ts +27 -0
  42. package/lib/types/novel/store.d.ts +96 -0
  43. package/lib/types/novel/template-fields.d.ts +17 -0
  44. package/lib/types/novel/timeline-labels.d.ts +2 -0
  45. package/lib/types/novel/tools.d.ts +3 -0
  46. package/package.json +77 -0
  47. package/seed/skills/chapter//345/216/273AI/345/221/263/344/270/216/350/264/250/346/243/200.md +34 -0
  48. package/seed/skills/chapter//345/257/271/350/257/235/344/270/216/346/203/205/347/273/252.md +52 -0
  49. package/seed/skills/chapter//347/253/240/350/212/202/346/225/260/346/215/256/347/273/264/346/212/244.md +61 -0
  50. package/seed/skills/chapter//347/253/240/350/212/202/350/212/202/345/245/217/344/270/216/345/274/200/345/244/264.md +61 -0
  51. package/seed/skills/chapter//347/253/240/351/246/226/351/222/251/345/255/220.md +40 -0
  52. package/seed/skills/creature//345/274/202/345/205/275/344/270/216/347/201/265/346/244/215.md +11 -0
  53. package/seed/skills/creature//347/224/237/347/211/251/346/241/243/346/241/210/350/246/201/347/264/240.md +13 -0
  54. package/seed/skills/genres//344/273/231/344/276/240/overview//351/242/230/346/235/220/350/247/204/345/210/231.md +32 -0
  55. package/seed/skills/genres//344/273/231/344/276/240/review/outline//345/244/247/347/272/262/345/256/241/346/237/245.md +21 -0
  56. package/seed/skills/genres//345/216/206/345/217/262/overview//351/242/230/346/235/220/350/247/204/345/210/231.md +32 -0
  57. package/seed/skills/genres//345/216/206/345/217/262/review/outline//345/244/247/347/272/262/345/256/241/346/237/245.md +21 -0
  58. package/seed/skills/genres//346/202/254/347/226/221/overview//351/242/230/346/235/220/350/247/204/345/210/231.md +33 -0
  59. package/seed/skills/genres//346/202/254/347/226/221/review/outline//345/244/247/347/272/262/345/256/241/346/237/245.md +21 -0
  60. package/seed/skills/genres//346/234/253/344/270/226/overview//351/242/230/346/235/220/350/247/204/345/210/231.md +33 -0
  61. package/seed/skills/genres//346/234/253/344/270/226/review/outline//345/244/247/347/272/262/345/256/241/346/237/245.md +21 -0
  62. package/seed/skills/genres//347/216/204/345/271/273/overview//351/242/230/346/235/220/350/247/204/345/210/231.md +32 -0
  63. package/seed/skills/genres//347/216/204/345/271/273/review/outline//345/244/247/347/272/262/345/256/241/346/237/245.md +22 -0
  64. package/seed/skills/genres//347/247/221/345/271/273/overview//351/242/230/346/235/220/350/247/204/345/210/231.md +33 -0
  65. package/seed/skills/genres//347/247/221/345/271/273/review/outline//345/244/247/347/272/262/345/256/241/346/237/245.md +21 -0
  66. package/seed/skills/genres//350/250/200/346/203/205/overview//351/242/230/346/235/220/350/247/204/345/210/231.md +31 -0
  67. package/seed/skills/genres//350/250/200/346/203/205/review/outline//345/244/247/347/272/262/345/256/241/346/237/245.md +21 -0
  68. package/seed/skills/genres//351/203/275/345/270/202/overview//351/242/230/346/235/220/350/247/204/345/210/231.md +32 -0
  69. package/seed/skills/genres//351/203/275/345/270/202/review/outline//345/244/247/347/272/262/345/256/241/346/237/245.md +21 -0
  70. package/seed/skills/item//346/265/201/350/275/254/344/270/216/345/275/222/345/261/236.md +22 -0
  71. package/seed/skills/item//347/211/251/345/223/201/346/241/243/346/241/210/350/246/201/347/264/240.md +13 -0
  72. package/seed/skills/level//346/210/220/351/225/277/344/270/216/347/210/275/347/202/271.md +44 -0
  73. package/seed/skills/level//350/203/275/345/212/233/344/270/216/344/273/243/344/273/267.md +42 -0
  74. package/seed/skills/outline//345/211/247/346/203/205/351/252/250/346/236/266/346/265/201/346/260/264/347/272/277.md +51 -0
  75. package/seed/skills/outline//347/273/223/346/236/204/344/270/216/345/215/267/347/253/240/346/250/241/346/235/277.md +68 -0
  76. package/seed/skills/outline//351/273/204/351/207/221/344/270/211/347/253/240/344/270/216/351/252/214/346/224/266.md +42 -0
  77. package/seed/skills/overview//345/215/226/347/202/271/344/270/216/345/256/232/344/275/215.md +52 -0
  78. package/seed/skills/overview//345/274/200/344/271/246/347/247/215/345/255/220.md +60 -0
  79. package/seed/skills/overview//346/240/207/351/242/230/345/210/233/344/275/234.md +43 -0
  80. package/seed/skills/review/chapter//347/253/240/350/212/202/345/256/241/346/237/245.md +72 -0
  81. package/seed/skills/review/outline//345/244/247/347/272/262/345/256/241/346/237/245.md +74 -0
  82. package/seed/skills/role//344/272/272/347/211/251/345/241/221/351/200/240.md +52 -0
  83. package/seed/skills/role//345/205/263/347/263/273/344/270/216/351/205/215/350/247/222.md +53 -0
  84. package/seed/skills/role//350/247/222/350/211/262/346/241/243/346/241/210.md +52 -0
  85. package/seed/skills/timeline//344/272/213/344/273/266/350/275/264/350/247/204/345/210/222.md +33 -0
  86. package/seed/skills/timeline//347/212/266/346/200/201/345/220/214/346/255/245.md +41 -0
  87. package/seed/skills/world//344/270/226/347/225/214/350/247/204/345/210/231.md +30 -0
  88. package/seed/skills/world//344/274/217/347/254/224/350/264/246/346/234/254.md +44 -0
  89. package/seed/skills/world//345/212/277/345/212/233/344/270/216/345/234/260/345/233/276.md +52 -0
  90. package/seed/skills/writing-style/da-feng-da-geng-ren/STYLE.md +132 -0
  91. package/seed/skills/writing-style/da-feng-da-geng-ren/meta.json +6 -0
  92. package/seed/skills/writing-style/hei-dao-da-feng/STYLE.md +144 -0
  93. package/seed/skills/writing-style/hei-dao-da-feng/meta.json +6 -0
  94. package/seed/skills/writing-style/hei-dao-xie-huang/STYLE.md +139 -0
  95. package/seed/skills/writing-style/hei-dao-xie-huang/meta.json +6 -0
  96. package/seed/templates//344/273/231/344/276/240.json +1322 -0
  97. package/seed/templates//345/216/206/345/217/262.json +1318 -0
  98. package/seed/templates//346/202/254/347/226/221.json +1318 -0
  99. package/seed/templates//346/234/253/344/270/226.json +1127 -0
  100. package/seed/templates//347/216/204/345/271/273.json +1322 -0
  101. package/seed/templates//347/247/221/345/271/273.json +1322 -0
  102. package/seed/templates//350/250/200/346/203/205.json +1318 -0
  103. package/seed/templates//351/203/275/345/270/202.json +1318 -0
package/lib/client.js ADDED
@@ -0,0 +1,1560 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "dsh-novel",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
+ let react = require("react");
8
+ let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
9
+ let react_jsx_runtime = require("react/jsx-runtime");
10
+ //#region src/novel/domains.ts
11
+ const DOMAIN_LABELS = {
12
+ info: "基本信息",
13
+ overview: "概述",
14
+ world: "世界",
15
+ role: "人物",
16
+ creature: "生物",
17
+ item: "物品",
18
+ level: "等级",
19
+ timeline: "时间线",
20
+ outline: "大纲",
21
+ chapter: "章节",
22
+ skill: "技能",
23
+ roleRelation: "人物关系",
24
+ roleTimeline: "人物时间线",
25
+ creatureTimeline: "生物时间线",
26
+ itemTimeline: "物品时间线",
27
+ worldTimeline: "世界时间线"
28
+ };
29
+ /** Conversation「内容」Tab 内的竖向分区;基本信息并入概述。模板为内置,不展示。 */
30
+ const CONTENT_SECTIONS = [
31
+ "overview",
32
+ "world",
33
+ "role",
34
+ "creature",
35
+ "item",
36
+ "level",
37
+ "timeline",
38
+ "outline",
39
+ "chapter",
40
+ "skill"
41
+ ];
42
+ //#endregion
43
+ //#region src/novel/contract.ts
44
+ /** Same-origin HTTP prefix for the novel Host API. */
45
+ const NOVEL_API_PREFIX = "/dsh-novel/api";
46
+ //#endregion
47
+ //#region src/client/novel-api.ts
48
+ async function novelApi(path, init) {
49
+ const response = await fetch(`${NOVEL_API_PREFIX}${path}`, {
50
+ cache: "no-store",
51
+ headers: {
52
+ "content-type": "application/json",
53
+ ...init?.headers ?? {}
54
+ },
55
+ ...init
56
+ });
57
+ const text = await response.text();
58
+ const body = text.length === 0 ? void 0 : JSON.parse(text);
59
+ if (!response.ok) throw new Error(body?.error ?? `HTTP ${String(response.status)}`);
60
+ return body;
61
+ }
62
+ //#endregion
63
+ //#region src/novel/outline-labels.ts
64
+ function asOrdinal(value) {
65
+ if (typeof value === "number" && Number.isFinite(value)) return value;
66
+ if (typeof value === "string" && value.trim() !== "") {
67
+ const n = Number(value.trim());
68
+ if (Number.isFinite(n)) return n;
69
+ }
70
+ }
71
+ function canonicalizeOutlineKind(raw) {
72
+ if (typeof raw !== "string") return void 0;
73
+ const key = raw.trim();
74
+ if (key === "chapter_segment" || key === "chapter" || key === "章") return "chapter_segment";
75
+ if (key === "volume" || key === "卷") return "volume";
76
+ }
77
+ function outlineKindOf(data) {
78
+ const explicit = canonicalizeOutlineKind(data.kind ?? data.nodeKind);
79
+ if (explicit !== void 0) return explicit;
80
+ return typeof data.parentId === "string" && data.parentId.length > 0 || typeof data.parentName === "string" && data.parentName.length > 0 ? "chapter_segment" : "volume";
81
+ }
82
+ function volumeChapterLabel(ordinal, unit, name) {
83
+ const title = name.trim().length > 0 ? name.trim() : "未命名";
84
+ const n = asOrdinal(ordinal);
85
+ if (n === void 0) return title;
86
+ return `第${n}${unit} ${title}`;
87
+ }
88
+ function shortName(data, fallback) {
89
+ return typeof data.name === "string" && data.name.trim().length > 0 ? data.name.trim() : fallback;
90
+ }
91
+ function outlineNodeLabel(data, fallback) {
92
+ const unit = outlineKindOf(data) === "chapter_segment" ? "章" : "卷";
93
+ return volumeChapterLabel(data.ordinal, unit, shortName(data, fallback));
94
+ }
95
+ function chapterNodeLabel(data, fallback) {
96
+ return volumeChapterLabel(data.ordinal ?? data.chapterNo, "章", shortName(data, fallback));
97
+ }
98
+ function compareOrdinal(a, b) {
99
+ const oa = asOrdinal(a);
100
+ const ob = asOrdinal(b);
101
+ if (oa === void 0 && ob === void 0) return 0;
102
+ if (oa === void 0) return 1;
103
+ if (ob === void 0) return -1;
104
+ return oa - ob;
105
+ }
106
+ function sortByOrdinalName(items, ordinalOf) {
107
+ return [...items].sort((a, b) => {
108
+ const cmp = compareOrdinal(ordinalOf(a), ordinalOf(b));
109
+ if (cmp !== 0) return cmp;
110
+ return (a.name ?? "").localeCompare(b.name ?? "", "zh");
111
+ });
112
+ }
113
+ //#endregion
114
+ //#region src/novel/normalize-entity.ts
115
+ function asRecord$2(value) {
116
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
117
+ }
118
+ /** Merge nested `profile` onto the root; existing top-level keys win. */
119
+ function hoistProfile(data) {
120
+ return {
121
+ ...asRecord$2(data.profile),
122
+ ...data
123
+ };
124
+ }
125
+ /** Merge outline `extra` onto the root; existing top-level keys win. */
126
+ function hoistOutlineExtra(data) {
127
+ return {
128
+ ...asRecord$2(data.extra),
129
+ ...data
130
+ };
131
+ }
132
+ /** Shape used by the content tab: hoist profile/extra so template keys resolve. */
133
+ function displayEntityData(data) {
134
+ return hoistOutlineExtra(hoistProfile(data));
135
+ }
136
+ //#endregion
137
+ //#region src/client/entity-client.ts
138
+ function asRecord$1(value) {
139
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
140
+ }
141
+ async function listEntities(path, domain) {
142
+ return novelApi(`/entities?path=${encodeURIComponent(path)}&domain=${encodeURIComponent(domain)}`);
143
+ }
144
+ function dataOf(item) {
145
+ return asRecord$1(item.data);
146
+ }
147
+ function viewData(item) {
148
+ if (item === void 0) return {};
149
+ return {
150
+ name: item.name ?? "",
151
+ ...displayEntityData(dataOf(item))
152
+ };
153
+ }
154
+ function parentIdOf(item, items) {
155
+ const data = asRecord$1(item.data);
156
+ if (typeof data.parentId === "string" && data.parentId.length > 0) return data.parentId;
157
+ if (typeof data.parentName === "string" && data.parentName.length > 0) return items.find((row) => row.name === data.parentName)?.id;
158
+ }
159
+ function filterOwned(items, ownerKey, ownerId) {
160
+ return items.filter((item) => String(dataOf(item)[ownerKey] ?? "") === ownerId);
161
+ }
162
+ //#endregion
163
+ //#region src/client/section-icons.tsx
164
+ function CreatureIcon() {
165
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
166
+ width: 16,
167
+ height: 16,
168
+ viewBox: "0 0 16 16",
169
+ fill: "none",
170
+ "aria-hidden": "true",
171
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
172
+ d: "M8 2.2c1.4 0 2.5 1.2 2.5 2.6 0 .5-.1 1-.4 1.4 1.3.5 2.2 1.8 2.2 3.3 0 2-1.9 3.6-4.3 3.6S3.7 11.5 3.7 9.5c0-1.5.9-2.8 2.2-3.3-.3-.4-.4-.9-.4-1.4C5.5 3.4 6.6 2.2 8 2.2Z",
173
+ stroke: "currentColor",
174
+ strokeWidth: "1.2"
175
+ })
176
+ });
177
+ }
178
+ function BookIcon() {
179
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
180
+ width: 16,
181
+ height: 16,
182
+ viewBox: "0 0 16 16",
183
+ fill: "none",
184
+ "aria-hidden": "true",
185
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
186
+ d: "M8 3.2c-1.1-.8-2.6-1.1-4.4-.8v9.2c1.7-.3 3.2 0 4.4.8 1.1-.8 2.6-1.1 4.4-.8V2.4c-1.8-.3-3.3 0-4.4.8Z",
187
+ stroke: "currentColor",
188
+ strokeWidth: "1.2",
189
+ strokeLinejoin: "round"
190
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
191
+ d: "M8 3.2v9.2",
192
+ stroke: "currentColor",
193
+ strokeWidth: "1.2"
194
+ })]
195
+ });
196
+ }
197
+ function SectionIcon({ section }) {
198
+ switch (section) {
199
+ case "overview": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BookIcon, {});
200
+ case "world": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconGlobeOutline14, {});
201
+ case "role": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconUserOutline16, {});
202
+ case "creature": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CreatureIcon, {});
203
+ case "item": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconArchiveOutline20, { size: 16 });
204
+ case "level": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconGoalOutline16, {});
205
+ case "timeline": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconQueueOutline14, {});
206
+ case "outline": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChecklistOutline14, {});
207
+ case "chapter": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconListPenOutline16, {});
208
+ case "skill": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconSkillOutline16, {});
209
+ }
210
+ }
211
+ //#endregion
212
+ //#region src/novel/template-fields.ts
213
+ function asRecord(value) {
214
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
215
+ }
216
+ function asFields(raw) {
217
+ if (!Array.isArray(raw)) return [];
218
+ return raw.flatMap((row) => {
219
+ if (row === null || typeof row !== "object") return [];
220
+ const rec = row;
221
+ if (typeof rec.key !== "string" || typeof rec.label !== "string") return [];
222
+ const options = flattenOptions(rec.options);
223
+ const field = {
224
+ key: rec.key,
225
+ label: rec.label
226
+ };
227
+ if (typeof rec.component === "string") field.component = rec.component;
228
+ if (options.length > 0) field.options = options;
229
+ return [field];
230
+ });
231
+ }
232
+ function flattenOptions(raw) {
233
+ if (!Array.isArray(raw)) return [];
234
+ const out = [];
235
+ for (const entry of raw) {
236
+ if (entry === null || typeof entry !== "object") continue;
237
+ const rec = entry;
238
+ if (typeof rec.value === "string" && typeof rec.label === "string") {
239
+ out.push({
240
+ label: rec.label,
241
+ value: rec.value
242
+ });
243
+ continue;
244
+ }
245
+ if (Array.isArray(rec.options)) for (const nested of rec.options) {
246
+ if (nested === null || typeof nested !== "object") continue;
247
+ const item = nested;
248
+ if (typeof item.value === "string" && typeof item.label === "string") out.push({
249
+ label: item.label,
250
+ value: item.value
251
+ });
252
+ }
253
+ }
254
+ return out;
255
+ }
256
+ /** Pick human-editable fields from a Noveel form_templates.config blob. */
257
+ function fieldsOf(template, section, extra) {
258
+ const block = asRecord(asRecord(template)[section]);
259
+ switch (section) {
260
+ case "overview": return asFields(block.fields);
261
+ case "role":
262
+ case "creature":
263
+ case "item": return extra === "timeline" ? asFields(asRecord(block.timeline).extraFields) : asFields(block.detailFields);
264
+ case "world": return extra === "timeline" ? asFields(asRecord(block.timeline).extraFields) : asFields(block.defaultDetailFields);
265
+ case "level": return asFields(block.levelFields);
266
+ case "timeline": return asFields(block.extraFields);
267
+ case "outline": return extra === "chapterSegment" ? asFields(block.chapterSegmentFields) : asFields(block.volumeFields);
268
+ default: return asFields(block.fields ?? block.detailFields);
269
+ }
270
+ }
271
+ function getByPath(obj, path) {
272
+ const parts = path.split(".").filter(Boolean);
273
+ let cur = obj;
274
+ for (const part of parts) {
275
+ if (cur == null || typeof cur !== "object" || Array.isArray(cur)) return void 0;
276
+ cur = cur[part];
277
+ }
278
+ return cur;
279
+ }
280
+ //#endregion
281
+ //#region src/client/ui/TemplateRead.tsx
282
+ function ReadField({ label, children }) {
283
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
284
+ className: "dshNovelViewField",
285
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", { children: label }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
286
+ className: "dshNovelReadValue",
287
+ children: children === "" || children === void 0 || children === null ? "—" : children
288
+ })]
289
+ });
290
+ }
291
+ function displayText(value) {
292
+ if (value === void 0 || value === null || value === "") return "";
293
+ if (Array.isArray(value)) return value.map((item) => String(item)).filter(Boolean).join("、");
294
+ if (typeof value === "boolean") return value ? "是" : "否";
295
+ return String(value);
296
+ }
297
+ function TemplateRead({ template, section, extra, value, omitKeys }) {
298
+ const skip = new Set(omitKeys ?? []);
299
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: fieldsOf(template, section, extra).filter((field) => !skip.has(field.key)).map((field) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ReadField, {
300
+ label: field.label,
301
+ children: displayText(getByPath(value, field.key))
302
+ }, field.key)) });
303
+ }
304
+ //#endregion
305
+ //#region src/client/ui/live-reload.ts
306
+ const HINT = "暂无。可在对话中让 AI 补充。";
307
+ function emptyHint(hasFilter) {
308
+ return hasFilter ? "无匹配项" : HINT;
309
+ }
310
+ function useLiveReload(reload, ms = 5e3) {
311
+ (0, react.useEffect)(() => {
312
+ const tick = () => {
313
+ reload();
314
+ };
315
+ const id = window.setInterval(tick, ms);
316
+ const onVis = () => {
317
+ if (document.visibilityState === "visible") tick();
318
+ };
319
+ document.addEventListener("visibilitychange", onVis);
320
+ return () => {
321
+ window.clearInterval(id);
322
+ document.removeEventListener("visibilitychange", onVis);
323
+ };
324
+ }, [reload, ms]);
325
+ }
326
+ //#endregion
327
+ //#region src/client/sections/OverviewSection.tsx
328
+ function OverviewSection({ path, template, styles }) {
329
+ const [row, setRow] = (0, react.useState)({});
330
+ const reload = (0, react.useCallback)(async () => {
331
+ setRow(await novelApi(`/novel?path=${encodeURIComponent(path)}`));
332
+ }, [path]);
333
+ (0, react.useEffect)(() => {
334
+ reload();
335
+ }, [reload]);
336
+ useLiveReload(reload);
337
+ const blueprint = asRecord$1(row.blueprint_json);
338
+ const styleName = styles.find((style) => style.id === row.writing_style_id)?.name ?? String(row.writing_style_id ?? "");
339
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("section", {
340
+ className: "dshNovelViewForm",
341
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
342
+ className: "dshNovelViewFormBody",
343
+ children: [
344
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h1", {
345
+ className: "dshNovelTitle",
346
+ children: String(row.title ?? "未命名")
347
+ }),
348
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
349
+ className: "dshNovelMeta",
350
+ children: [row.category, styleName].filter((item) => item !== void 0 && item !== "").join(" · ") || "—"
351
+ }),
352
+ row.description !== void 0 && row.description !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
353
+ className: "dshNovelProse",
354
+ children: String(row.description)
355
+ }),
356
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TemplateRead, {
357
+ template,
358
+ section: "overview",
359
+ value: blueprint
360
+ })
361
+ ]
362
+ })
363
+ });
364
+ }
365
+ //#endregion
366
+ //#region src/novel/noveel-shapes.ts
367
+ const WORLD_KINDS = [
368
+ {
369
+ value: "universe",
370
+ label: "宇宙"
371
+ },
372
+ {
373
+ value: "multiverse",
374
+ label: "多元宇宙"
375
+ },
376
+ {
377
+ value: "galaxy",
378
+ label: "星系"
379
+ },
380
+ {
381
+ value: "realm",
382
+ label: "界域"
383
+ },
384
+ {
385
+ value: "domain",
386
+ label: "域"
387
+ },
388
+ {
389
+ value: "plane",
390
+ label: "位面"
391
+ },
392
+ {
393
+ value: "planet",
394
+ label: "星球"
395
+ },
396
+ {
397
+ value: "continent",
398
+ label: "大陆"
399
+ },
400
+ {
401
+ value: "mainland",
402
+ label: "大陆"
403
+ },
404
+ {
405
+ value: "region",
406
+ label: "地区"
407
+ },
408
+ {
409
+ value: "mountain",
410
+ label: "山脉"
411
+ },
412
+ {
413
+ value: "secret_realm",
414
+ label: "秘境/禁地"
415
+ },
416
+ {
417
+ value: "country",
418
+ label: "国家"
419
+ },
420
+ {
421
+ value: "empire",
422
+ label: "帝国"
423
+ },
424
+ {
425
+ value: "kingdom",
426
+ label: "王国"
427
+ },
428
+ {
429
+ value: "city",
430
+ label: "城市"
431
+ },
432
+ {
433
+ value: "town",
434
+ label: "城镇"
435
+ },
436
+ {
437
+ value: "settlement",
438
+ label: "聚落"
439
+ },
440
+ {
441
+ value: "faction",
442
+ label: "势力"
443
+ },
444
+ {
445
+ value: "sect",
446
+ label: "宗门/门派"
447
+ },
448
+ {
449
+ value: "clan",
450
+ label: "家族/氏族"
451
+ },
452
+ {
453
+ value: "guild",
454
+ label: "公会"
455
+ },
456
+ {
457
+ value: "army",
458
+ label: "军队"
459
+ },
460
+ {
461
+ value: "corporation",
462
+ label: "公司/集团"
463
+ },
464
+ {
465
+ value: "alliance",
466
+ label: "联盟"
467
+ },
468
+ {
469
+ value: "cult",
470
+ label: "教派"
471
+ },
472
+ {
473
+ value: "academy",
474
+ label: "学院"
475
+ },
476
+ {
477
+ value: "custom",
478
+ label: "其他"
479
+ }
480
+ ];
481
+ function emptyLevelSystem() {
482
+ return {
483
+ name: "自定义体系",
484
+ category: "自定义",
485
+ description: "",
486
+ levels: [{
487
+ name: "第一阶",
488
+ subLevels: [
489
+ { name: "初期" },
490
+ { name: "中期" },
491
+ { name: "后期" }
492
+ ],
493
+ promotionCondition: "达到上一阶大圆满,并满足作品内设定的突破条件。",
494
+ abilities: "待补充该等级典型能力与表现。"
495
+ }]
496
+ };
497
+ }
498
+ //#endregion
499
+ //#region src/client/ui/controls.tsx
500
+ function SearchField({ className: _className, ...rest }) {
501
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
502
+ className: "dshNovelViewSearch",
503
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
504
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconSearchOutline16, {}),
505
+ placeholder: "搜索…",
506
+ ...rest
507
+ })
508
+ });
509
+ }
510
+ function DetailTabs({ tabs, value, onChange }) {
511
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
512
+ className: "dshNovelTabs",
513
+ role: "tablist",
514
+ children: tabs.map((tab) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
515
+ type: "button",
516
+ role: "tab",
517
+ "aria-selected": value === tab.id,
518
+ "data-active": value === tab.id || void 0,
519
+ onClick: () => {
520
+ onChange(tab.id);
521
+ },
522
+ children: tab.label
523
+ }, tab.id))
524
+ });
525
+ }
526
+ function SectionToolbar({ tabs, actions }) {
527
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
528
+ className: "dshNovelViewFormHead",
529
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
530
+ className: "dshNovelViewFormHeadTabs",
531
+ children: tabs
532
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
533
+ className: "dshNovelViewFormHeadActions",
534
+ children: actions
535
+ })]
536
+ });
537
+ }
538
+ //#endregion
539
+ //#region src/client/ui/EntitySidebar.tsx
540
+ function EntitySidebar({ items, selectedId, onSelect, tree, label, className, files }) {
541
+ const [filter, setFilter] = (0, react.useState)("");
542
+ const [expanded, setExpanded] = (0, react.useState)(/* @__PURE__ */ new Set());
543
+ const nameOf = label ?? ((item) => item.name ?? item.id);
544
+ const q = filter.trim();
545
+ const visibleItems = (0, react.useMemo)(() => {
546
+ if (q.length === 0) return items;
547
+ const match = /* @__PURE__ */ new Set();
548
+ for (const item of items) if (nameOf(item).includes(q)) match.add(item.id);
549
+ for (const item of items) {
550
+ if (!match.has(item.id)) continue;
551
+ let current = parentIdOf(item, items);
552
+ while (current !== void 0) {
553
+ match.add(current);
554
+ const ancestor = items.find((row) => row.id === current);
555
+ current = ancestor === void 0 ? void 0 : parentIdOf(ancestor, items);
556
+ }
557
+ }
558
+ return items.filter((item) => match.has(item.id));
559
+ }, [
560
+ items,
561
+ nameOf,
562
+ q
563
+ ]);
564
+ (0, react.useEffect)(() => {
565
+ if (q.length === 0) return;
566
+ setExpanded((current) => {
567
+ const next = new Set(current);
568
+ for (const item of visibleItems) {
569
+ let ancestor = parentIdOf(item, items);
570
+ while (ancestor !== void 0) {
571
+ next.add(ancestor);
572
+ const row = items.find((entry) => entry.id === ancestor);
573
+ ancestor = row === void 0 ? void 0 : parentIdOf(row, items);
574
+ }
575
+ }
576
+ return next;
577
+ });
578
+ }, [
579
+ items,
580
+ q,
581
+ visibleItems
582
+ ]);
583
+ const shown = (0, react.useMemo)(() => {
584
+ if (!tree) return visibleItems.map((item) => ({
585
+ ...item,
586
+ depth: 0,
587
+ hasChildren: false
588
+ }));
589
+ return buildTree(visibleItems, items);
590
+ }, [
591
+ tree,
592
+ visibleItems,
593
+ items
594
+ ]).filter((row) => {
595
+ if (!tree) return true;
596
+ const parent = parentIdOf(row, items);
597
+ if (parent === void 0) return true;
598
+ let current = parent;
599
+ while (current !== void 0) {
600
+ if (!expanded.has(current)) return false;
601
+ const ancestor = items.find((item) => item.id === current);
602
+ current = ancestor === void 0 ? void 0 : parentIdOf(ancestor, items);
603
+ }
604
+ return true;
605
+ });
606
+ const toggle = (id) => {
607
+ setExpanded((current) => {
608
+ const next = new Set(current);
609
+ if (next.has(id)) next.delete(id);
610
+ else next.add(id);
611
+ return next;
612
+ });
613
+ };
614
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("aside", {
615
+ className: `dshNovelViewList${className === void 0 ? "" : ` ${className}`}`,
616
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
617
+ className: "dshNovelViewListToolbar",
618
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SearchField, {
619
+ value: filter,
620
+ onChange: (event) => {
621
+ setFilter(event.target.value);
622
+ }
623
+ })
624
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
625
+ className: "dshNovelViewListBody",
626
+ children: [shown.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
627
+ className: "dshNovelViewMuted",
628
+ children: q ? "无匹配项" : "暂无。可在对话中让 AI 补充。"
629
+ }), shown.map((row) => {
630
+ const folder = isFolder(row);
631
+ const open = expanded.has(row.id);
632
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
633
+ type: "button",
634
+ className: "dshNovelViewRow",
635
+ "data-kind": folder ? "folder" : "file",
636
+ "data-active": !folder && row.id === selectedId ? true : void 0,
637
+ style: { paddingLeft: 8 + row.depth * 12 },
638
+ onClick: () => {
639
+ if (folder) toggle(row.id);
640
+ else onSelect(row.id);
641
+ },
642
+ children: [
643
+ tree && row.hasChildren ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
644
+ className: "dshNovelViewChevron",
645
+ "data-open": open || void 0,
646
+ onClick: (event) => {
647
+ event.stopPropagation();
648
+ toggle(row.id);
649
+ },
650
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronRightOutline14, {})
651
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "dshNovelViewChevron" }),
652
+ files ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
653
+ className: "dshNovelViewRowIcon",
654
+ children: folder ? open ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconFolderOpenOutline16, {}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconFolderClose16, {}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconListPenOutline16, {})
655
+ }) : null,
656
+ nameOf(row)
657
+ ]
658
+ }, row.id);
659
+ })]
660
+ })]
661
+ });
662
+ }
663
+ function isFolder(item) {
664
+ return asRecord$1(item.data).kind === "folder";
665
+ }
666
+ function buildTree(items, all) {
667
+ const byParent = /* @__PURE__ */ new Map();
668
+ for (const item of items) {
669
+ const parent = parentIdOf(item, all);
670
+ const list = byParent.get(parent) ?? [];
671
+ list.push(item);
672
+ byParent.set(parent, list);
673
+ }
674
+ const out = [];
675
+ const walk = (parent, depth) => {
676
+ for (const child of byParent.get(parent) ?? []) {
677
+ out.push({
678
+ ...child,
679
+ depth,
680
+ hasChildren: (byParent.get(child.id) ?? []).length > 0
681
+ });
682
+ walk(child.id, depth + 1);
683
+ }
684
+ };
685
+ walk(void 0, 0);
686
+ const seen = new Set(out.map((row) => row.id));
687
+ for (const item of items) if (!seen.has(item.id)) out.push({
688
+ ...item,
689
+ depth: 0,
690
+ hasChildren: false
691
+ });
692
+ return out;
693
+ }
694
+ //#endregion
695
+ //#region src/client/ui/TimelinePanel.tsx
696
+ function TimelinePanel({ path, domain, ownerKey, ownerId, chapters }) {
697
+ const [rows, setRows] = (0, react.useState)([]);
698
+ const reload = (0, react.useCallback)(async () => {
699
+ setRows(filterOwned(await listEntities(path, domain), ownerKey, ownerId));
700
+ }, [
701
+ path,
702
+ domain,
703
+ ownerId,
704
+ ownerKey
705
+ ]);
706
+ (0, react.useEffect)(() => {
707
+ reload();
708
+ }, [reload]);
709
+ useLiveReload(reload);
710
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
711
+ className: "dshNovelViewFormBody",
712
+ children: [rows.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
713
+ className: "dshNovelViewMuted",
714
+ children: emptyHint()
715
+ }), rows.map((row) => {
716
+ const data = dataOf(row);
717
+ const chapter = chapters.find((item) => item.id === data.relatedChapterId);
718
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("article", {
719
+ className: "dshNovelEntry",
720
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [
721
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
722
+ className: "dshNovelEntryTitle",
723
+ children: String(data.title ?? row.name ?? "未命名")
724
+ }),
725
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
726
+ className: "dshNovelViewMuted",
727
+ children: [data.timeLabel, chapter?.name].filter(Boolean).map(String).join(" · ")
728
+ }),
729
+ data.content !== void 0 && data.content !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
730
+ className: "dshNovelProse",
731
+ children: String(data.content)
732
+ })
733
+ ] })
734
+ }, row.id);
735
+ })]
736
+ });
737
+ }
738
+ //#endregion
739
+ //#region src/client/sections/WorldSection.tsx
740
+ function WorldSection({ path, template, chapters }) {
741
+ const [items, setItems] = (0, react.useState)([]);
742
+ const [selectedId, setSelectedId] = (0, react.useState)();
743
+ const [tab, setTab] = (0, react.useState)("detail");
744
+ const selected = items.find((item) => item.id === selectedId);
745
+ const data = viewData(selected);
746
+ const kindLabel = WORLD_KINDS.find((kind) => kind.value === data.kind)?.label ?? String(data.kind ?? "");
747
+ const parentId = selected === void 0 ? void 0 : parentIdOf(selected, items);
748
+ const parentName = parentId === void 0 ? typeof data.parentName === "string" && data.parentName.length > 0 ? data.parentName : void 0 : items.find((item) => item.id === parentId)?.name;
749
+ const reload = (0, react.useCallback)(async () => {
750
+ const rows = await listEntities(path, "world");
751
+ setItems(rows);
752
+ setSelectedId((current) => current !== void 0 && rows.some((row) => row.id === current) ? current : rows[0]?.id);
753
+ }, [path]);
754
+ (0, react.useEffect)(() => {
755
+ reload();
756
+ }, [reload]);
757
+ useLiveReload(reload);
758
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(EntitySidebar, {
759
+ items,
760
+ selectedId,
761
+ tree: true,
762
+ onSelect: (id) => {
763
+ setSelectedId(id);
764
+ setTab("detail");
765
+ }
766
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
767
+ className: "dshNovelViewForm",
768
+ children: [selectedId !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SectionToolbar, { tabs: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DetailTabs, {
769
+ tabs: [{
770
+ id: "detail",
771
+ label: "详情"
772
+ }, {
773
+ id: "timeline",
774
+ label: "时间线"
775
+ }],
776
+ value: tab,
777
+ onChange: setTab
778
+ }) }), selected === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
779
+ className: "dshNovelViewFormBody",
780
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
781
+ className: "dshNovelViewMuted",
782
+ children: emptyHint()
783
+ })
784
+ }) : tab === "detail" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
785
+ className: "dshNovelViewFormBody",
786
+ children: [
787
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
788
+ className: "dshNovelTitle",
789
+ children: String(data.name ?? "未命名")
790
+ }),
791
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
792
+ className: "dshNovelMeta",
793
+ children: [kindLabel, parentName].filter(Boolean).join(" · ")
794
+ }),
795
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ReadField, {
796
+ label: "类型",
797
+ children: kindLabel
798
+ }),
799
+ parentName !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ReadField, {
800
+ label: "父节点",
801
+ children: parentName
802
+ }),
803
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TemplateRead, {
804
+ template,
805
+ section: "world",
806
+ value: data
807
+ })
808
+ ]
809
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TimelinePanel, {
810
+ path,
811
+ domain: "worldTimeline",
812
+ ownerKey: "worldNodeId",
813
+ ownerId: selectedId,
814
+ chapters
815
+ })]
816
+ })] });
817
+ }
818
+ //#endregion
819
+ //#region src/client/ui/RelationPanel.tsx
820
+ function RelationPanel({ path, ownerId, roles }) {
821
+ const [rows, setRows] = (0, react.useState)([]);
822
+ const reload = (0, react.useCallback)(async () => {
823
+ const all = await listEntities(path, "roleRelation");
824
+ setRows(all.filter((item) => {
825
+ const data = dataOf(item);
826
+ return String(data.roleId ?? "") === ownerId || String(data.targetRoleId ?? "") === ownerId;
827
+ }));
828
+ }, [path, ownerId]);
829
+ (0, react.useEffect)(() => {
830
+ reload();
831
+ }, [reload]);
832
+ useLiveReload(reload);
833
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
834
+ className: "dshNovelViewFormBody",
835
+ children: [rows.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
836
+ className: "dshNovelViewMuted",
837
+ children: emptyHint()
838
+ }), rows.map((row) => {
839
+ const data = dataOf(row);
840
+ const otherId = String(data.roleId ?? "") === ownerId ? data.targetRoleId : data.roleId;
841
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("article", {
842
+ className: "dshNovelEntry",
843
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
844
+ className: "dshNovelEntryTitle",
845
+ children: [
846
+ (roles.find((role) => role.id === otherId) ?? roles.find((role) => role.name === otherId))?.name ?? String(otherId ?? ""),
847
+ " · ",
848
+ String(data.relationType ?? "")
849
+ ]
850
+ }), data.content !== void 0 && data.content !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
851
+ className: "dshNovelProse",
852
+ children: String(data.content)
853
+ })] })
854
+ }, row.id);
855
+ })]
856
+ });
857
+ }
858
+ //#endregion
859
+ //#region src/client/sections/ListDetailSection.tsx
860
+ function ListDetailSection({ path, domain, template, templateSection, chapters, tree, tabs, timelineDomain, ownerKey }) {
861
+ const [items, setItems] = (0, react.useState)([]);
862
+ const [selectedId, setSelectedId] = (0, react.useState)();
863
+ const [tab, setTab] = (0, react.useState)("detail");
864
+ const selected = items.find((item) => item.id === selectedId);
865
+ const data = viewData(selected);
866
+ const reload = (0, react.useCallback)(async () => {
867
+ const rows = await listEntities(path, domain);
868
+ setItems(rows);
869
+ setSelectedId((current) => {
870
+ if (current !== void 0 && rows.some((row) => row.id === current)) return current;
871
+ return rows[0]?.id;
872
+ });
873
+ }, [domain, path]);
874
+ (0, react.useEffect)(() => {
875
+ reload();
876
+ }, [reload]);
877
+ useLiveReload(reload);
878
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(EntitySidebar, {
879
+ items,
880
+ selectedId,
881
+ tree,
882
+ onSelect: (id) => {
883
+ setSelectedId(id);
884
+ setTab("detail");
885
+ }
886
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
887
+ className: "dshNovelViewForm",
888
+ children: [tabs.length > 1 && selectedId !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SectionToolbar, { tabs: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DetailTabs, {
889
+ tabs: tabs.map((id) => ({
890
+ id,
891
+ label: id === "detail" ? "详情" : id === "timeline" ? "时间线" : "关系"
892
+ })),
893
+ value: tab,
894
+ onChange: setTab
895
+ }) }), selected === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
896
+ className: "dshNovelViewFormBody",
897
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
898
+ className: "dshNovelViewMuted",
899
+ children: emptyHint()
900
+ })
901
+ }) : tab === "detail" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
902
+ className: "dshNovelViewFormBody",
903
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
904
+ className: "dshNovelTitle",
905
+ children: String(data.name ?? "未命名")
906
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TemplateRead, {
907
+ template,
908
+ section: templateSection,
909
+ value: data
910
+ })]
911
+ }) : tab === "timeline" && timelineDomain && ownerKey ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TimelinePanel, {
912
+ path,
913
+ domain: timelineDomain,
914
+ ownerKey,
915
+ ownerId: selectedId,
916
+ chapters
917
+ }) : tab === "relation" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RelationPanel, {
918
+ path,
919
+ ownerId: selectedId,
920
+ roles: items
921
+ }) : null]
922
+ })] });
923
+ }
924
+ //#endregion
925
+ //#region src/client/sections/owned-sections.tsx
926
+ function RoleSection(props) {
927
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ListDetailSection, {
928
+ ...props,
929
+ domain: "role",
930
+ templateSection: "role",
931
+ tabs: [
932
+ "detail",
933
+ "relation",
934
+ "timeline"
935
+ ],
936
+ timelineDomain: "roleTimeline",
937
+ ownerKey: "roleId"
938
+ });
939
+ }
940
+ function CreatureSection(props) {
941
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ListDetailSection, {
942
+ ...props,
943
+ domain: "creature",
944
+ templateSection: "creature",
945
+ tabs: ["detail", "timeline"],
946
+ timelineDomain: "creatureTimeline",
947
+ ownerKey: "creatureId"
948
+ });
949
+ }
950
+ function ItemSection(props) {
951
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ListDetailSection, {
952
+ ...props,
953
+ domain: "item",
954
+ templateSection: "item",
955
+ tabs: ["detail", "timeline"],
956
+ timelineDomain: "itemTimeline",
957
+ ownerKey: "itemId"
958
+ });
959
+ }
960
+ //#endregion
961
+ //#region src/client/sections/LevelSection.tsx
962
+ function parseSystem(data) {
963
+ const system = asRecord$1(data.system);
964
+ const levels = Array.isArray(system.levels) ? system.levels : emptyLevelSystem().levels;
965
+ return {
966
+ name: String(system.name ?? ""),
967
+ description: String(system.description ?? ""),
968
+ levels: levels.map((row) => {
969
+ const rec = asRecord$1(row);
970
+ const subs = Array.isArray(rec.subLevels) ? rec.subLevels : [];
971
+ return {
972
+ name: String(rec.name ?? ""),
973
+ promotionCondition: String(rec.promotionCondition ?? ""),
974
+ abilities: String(rec.abilities ?? ""),
975
+ subLevels: subs.map((sub) => String(asRecord$1(sub).name ?? "")).filter(Boolean)
976
+ };
977
+ })
978
+ };
979
+ }
980
+ function LevelSection({ path }) {
981
+ const [items, setItems] = (0, react.useState)([]);
982
+ const [selectedId, setSelectedId] = (0, react.useState)();
983
+ const [tab, setTab] = (0, react.useState)("detail");
984
+ const selected = items.find((item) => item.id === selectedId);
985
+ const data = viewData(selected);
986
+ const system = parseSystem(data);
987
+ const reload = (0, react.useCallback)(async () => {
988
+ const rows = await listEntities(path, "level");
989
+ setItems(rows);
990
+ setSelectedId((current) => current !== void 0 && rows.some((row) => row.id === current) ? current : rows[0]?.id);
991
+ }, [path]);
992
+ (0, react.useEffect)(() => {
993
+ reload();
994
+ }, [reload]);
995
+ useLiveReload(reload);
996
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(EntitySidebar, {
997
+ items,
998
+ selectedId,
999
+ label: (item) => {
1000
+ const system = asRecord$1(asRecord$1(item.data).system);
1001
+ return item.name ?? (typeof system.name === "string" ? system.name : void 0) ?? item.id;
1002
+ },
1003
+ onSelect: (id) => {
1004
+ setSelectedId(id);
1005
+ setTab("detail");
1006
+ }
1007
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
1008
+ className: "dshNovelViewForm",
1009
+ children: [selectedId !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SectionToolbar, { tabs: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DetailTabs, {
1010
+ tabs: [{
1011
+ id: "detail",
1012
+ label: "详情"
1013
+ }, {
1014
+ id: "tiers",
1015
+ label: "档位"
1016
+ }],
1017
+ value: tab,
1018
+ onChange: setTab
1019
+ }) }), selected === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1020
+ className: "dshNovelViewFormBody",
1021
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1022
+ className: "dshNovelViewMuted",
1023
+ children: emptyHint()
1024
+ })
1025
+ }) : tab === "detail" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1026
+ className: "dshNovelViewFormBody",
1027
+ children: [
1028
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
1029
+ className: "dshNovelTitle",
1030
+ children: String(data.name || system.name || "未命名")
1031
+ }),
1032
+ system.description !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1033
+ className: "dshNovelProse",
1034
+ children: system.description
1035
+ }),
1036
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ReadField, {
1037
+ label: "体系",
1038
+ children: system.name
1039
+ })
1040
+ ]
1041
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1042
+ className: "dshNovelViewFormBody",
1043
+ children: [system.levels.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1044
+ className: "dshNovelViewMuted",
1045
+ children: emptyHint()
1046
+ }), system.levels.map((level, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("article", {
1047
+ className: "dshNovelTier",
1048
+ children: [
1049
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1050
+ className: "dshNovelEntryTitle",
1051
+ children: level.name
1052
+ }),
1053
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1054
+ className: "dshNovelPills",
1055
+ children: level.subLevels.map((sub) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Pill, { children: sub }, sub))
1056
+ }),
1057
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ReadField, {
1058
+ label: "突破条件",
1059
+ children: level.promotionCondition
1060
+ }),
1061
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ReadField, {
1062
+ label: "能力",
1063
+ children: level.abilities
1064
+ })
1065
+ ]
1066
+ }, index))]
1067
+ })]
1068
+ })] });
1069
+ }
1070
+ //#endregion
1071
+ //#region src/novel/timeline-labels.ts
1072
+ const EVENT_TYPE_LABELS = {
1073
+ plot: "主线",
1074
+ conflict: "冲突",
1075
+ suspense: "悬念",
1076
+ foreshadow: "伏笔",
1077
+ turning: "转折",
1078
+ resolution: "收束",
1079
+ other: "其他"
1080
+ };
1081
+ const TRACKED_EVENT_TYPES = /* @__PURE__ */ new Set([
1082
+ "conflict",
1083
+ "suspense",
1084
+ "foreshadow"
1085
+ ]);
1086
+ function asEventType(value) {
1087
+ if (typeof value !== "string") return void 0;
1088
+ const key = value.trim();
1089
+ return key.length > 0 ? key : void 0;
1090
+ }
1091
+ function asResolved(value) {
1092
+ if (typeof value === "boolean") return value;
1093
+ if (value === "true") return true;
1094
+ if (value === "false") return false;
1095
+ }
1096
+ function timelineEventTypeLabel(eventType) {
1097
+ const key = asEventType(eventType);
1098
+ if (key === void 0) return void 0;
1099
+ return EVENT_TYPE_LABELS[key] ?? key;
1100
+ }
1101
+ function timelineResolvedLabel(eventType, isResolved) {
1102
+ const key = asEventType(eventType);
1103
+ if (key === void 0 || !TRACKED_EVENT_TYPES.has(key)) return void 0;
1104
+ if (asResolved(isResolved) === true) return "已解决";
1105
+ return "未解决";
1106
+ }
1107
+ //#endregion
1108
+ //#region src/client/sections/TimelineSection.tsx
1109
+ function TimelineSection({ path, template, chapters }) {
1110
+ const [items, setItems] = (0, react.useState)([]);
1111
+ const [selectedId, setSelectedId] = (0, react.useState)();
1112
+ const selected = items.find((item) => item.id === selectedId);
1113
+ const data = viewData(selected);
1114
+ const chapter = chapters.find((item) => item.id === data.relatedChapterId);
1115
+ const typeLabel = timelineEventTypeLabel(data.eventType);
1116
+ const resolvedLabel = timelineResolvedLabel(data.eventType, data.isResolved);
1117
+ const reload = (0, react.useCallback)(async () => {
1118
+ const rows = await listEntities(path, "timeline");
1119
+ setItems(rows);
1120
+ setSelectedId((current) => current !== void 0 && rows.some((row) => row.id === current) ? current : rows[0]?.id);
1121
+ }, [path]);
1122
+ (0, react.useEffect)(() => {
1123
+ reload();
1124
+ }, [reload]);
1125
+ useLiveReload(reload);
1126
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(EntitySidebar, {
1127
+ items,
1128
+ selectedId,
1129
+ onSelect: setSelectedId
1130
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("section", {
1131
+ className: "dshNovelViewForm",
1132
+ children: selected === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1133
+ className: "dshNovelViewFormBody",
1134
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1135
+ className: "dshNovelViewMuted",
1136
+ children: emptyHint()
1137
+ })
1138
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1139
+ className: "dshNovelViewFormBody",
1140
+ children: [
1141
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
1142
+ className: "dshNovelTitle",
1143
+ children: String(data.title ?? data.name ?? "未命名")
1144
+ }),
1145
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1146
+ className: "dshNovelMeta",
1147
+ children: [
1148
+ data.timeLabel,
1149
+ typeLabel,
1150
+ resolvedLabel,
1151
+ chapter?.name
1152
+ ].filter(Boolean).map(String).join(" · ")
1153
+ }),
1154
+ typeLabel !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ReadField, {
1155
+ label: "类型",
1156
+ children: typeLabel
1157
+ }),
1158
+ resolvedLabel !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ReadField, {
1159
+ label: "状态",
1160
+ children: resolvedLabel === "已解决" && data.resolveChapterNo !== void 0 && data.resolveChapterNo !== "" ? `${resolvedLabel} · 回收章 ${String(data.resolveChapterNo)}` : resolvedLabel
1161
+ }),
1162
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TemplateRead, {
1163
+ template,
1164
+ section: "timeline",
1165
+ extra: "timeline",
1166
+ value: data
1167
+ })
1168
+ ]
1169
+ })
1170
+ })] });
1171
+ }
1172
+ //#endregion
1173
+ //#region src/client/sections/OutlineSection.tsx
1174
+ function labelData$1(item) {
1175
+ const data = dataOf(item);
1176
+ return {
1177
+ ...data,
1178
+ name: item.name ?? data.name
1179
+ };
1180
+ }
1181
+ function OutlineSection({ path, template }) {
1182
+ const [items, setItems] = (0, react.useState)([]);
1183
+ const [selectedId, setSelectedId] = (0, react.useState)();
1184
+ const selected = items.find((item) => item.id === selectedId);
1185
+ const data = viewData(selected);
1186
+ const isChapter = outlineKindOf(data) === "chapter_segment";
1187
+ const parent = items.find((item) => item.id === data.parentId);
1188
+ const parentLabel = parent === void 0 ? void 0 : outlineNodeLabel(labelData$1(parent), parent.name ?? parent.id);
1189
+ const reload = (0, react.useCallback)(async () => {
1190
+ const rows = sortByOrdinalName(await listEntities(path, "outline"), (item) => dataOf(item).ordinal);
1191
+ setItems(rows);
1192
+ setSelectedId((current) => current !== void 0 && rows.some((row) => row.id === current) ? current : rows[0]?.id);
1193
+ }, [path]);
1194
+ (0, react.useEffect)(() => {
1195
+ reload();
1196
+ }, [reload]);
1197
+ useLiveReload(reload);
1198
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(EntitySidebar, {
1199
+ items,
1200
+ selectedId,
1201
+ tree: true,
1202
+ label: (item) => outlineNodeLabel(labelData$1(item), item.name ?? item.id),
1203
+ onSelect: setSelectedId
1204
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("section", {
1205
+ className: "dshNovelViewForm",
1206
+ children: selected === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1207
+ className: "dshNovelViewFormBody",
1208
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1209
+ className: "dshNovelViewMuted",
1210
+ children: emptyHint()
1211
+ })
1212
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1213
+ className: "dshNovelViewFormBody",
1214
+ children: [
1215
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
1216
+ className: "dshNovelTitle",
1217
+ children: outlineNodeLabel(data, String(data.name ?? "未命名"))
1218
+ }),
1219
+ parentLabel !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1220
+ className: "dshNovelMeta",
1221
+ children: parentLabel
1222
+ }),
1223
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TemplateRead, {
1224
+ template,
1225
+ section: "outline",
1226
+ extra: isChapter ? "chapterSegment" : void 0,
1227
+ value: data,
1228
+ omitKeys: ["name"]
1229
+ })
1230
+ ]
1231
+ })
1232
+ })] });
1233
+ }
1234
+ //#endregion
1235
+ //#region src/client/sections/ChapterSection.tsx
1236
+ function labelData(item) {
1237
+ const data = dataOf(item);
1238
+ return {
1239
+ ...data,
1240
+ name: item.name ?? data.name
1241
+ };
1242
+ }
1243
+ function ChapterSection({ path }) {
1244
+ const [items, setItems] = (0, react.useState)([]);
1245
+ const [outlines, setOutlines] = (0, react.useState)([]);
1246
+ const [selectedId, setSelectedId] = (0, react.useState)();
1247
+ const selected = items.find((item) => item.id === selectedId);
1248
+ const data = viewData(selected);
1249
+ const related = outlines.find((item) => item.id === data.relatedOutlineId);
1250
+ const outlineName = related === void 0 ? void 0 : outlineNodeLabel(labelData(related), related.name ?? related.id);
1251
+ const reload = (0, react.useCallback)(async () => {
1252
+ const [rows, outlineRows] = await Promise.all([listEntities(path, "chapter"), listEntities(path, "outline")]);
1253
+ const sorted = sortByOrdinalName(rows, (item) => dataOf(item).ordinal ?? dataOf(item).chapterNo);
1254
+ setItems(sorted);
1255
+ setOutlines(outlineRows);
1256
+ setSelectedId((current) => current !== void 0 && sorted.some((row) => row.id === current) ? current : sorted[0]?.id);
1257
+ }, [path]);
1258
+ (0, react.useEffect)(() => {
1259
+ reload();
1260
+ }, [reload]);
1261
+ useLiveReload(reload);
1262
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(EntitySidebar, {
1263
+ items,
1264
+ selectedId,
1265
+ label: (item) => chapterNodeLabel(labelData(item), item.name ?? item.id),
1266
+ onSelect: setSelectedId
1267
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("section", {
1268
+ className: "dshNovelViewForm",
1269
+ children: selected === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1270
+ className: "dshNovelViewFormBody",
1271
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1272
+ className: "dshNovelViewMuted",
1273
+ children: emptyHint()
1274
+ })
1275
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1276
+ className: "dshNovelViewFormBody",
1277
+ children: [
1278
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
1279
+ className: "dshNovelTitle",
1280
+ children: chapterNodeLabel(data, String(data.name ?? data.title ?? "未命名"))
1281
+ }),
1282
+ outlineName !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1283
+ className: "dshNovelMeta",
1284
+ children: outlineName
1285
+ }),
1286
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1287
+ className: "dshNovelProse",
1288
+ children: String(data.content ?? "") || "—"
1289
+ })
1290
+ ]
1291
+ })
1292
+ })] });
1293
+ }
1294
+ //#endregion
1295
+ //#region src/client/sections/SkillSection.tsx
1296
+ function SkillSection({ path }) {
1297
+ const [skills, setSkills] = (0, react.useState)([]);
1298
+ const [selectedId, setSelectedId] = (0, react.useState)();
1299
+ const selected = skills.find((item) => item.id === selectedId);
1300
+ const reload = (0, react.useCallback)(async () => {
1301
+ const rows = await novelApi(`/skills?path=${encodeURIComponent(path)}`);
1302
+ setSkills(rows);
1303
+ setSelectedId((current) => current !== void 0 && rows.some((row) => row.id === current) ? current : void 0);
1304
+ }, [path]);
1305
+ (0, react.useEffect)(() => {
1306
+ reload();
1307
+ }, [reload]);
1308
+ useLiveReload(reload);
1309
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(EntitySidebar, {
1310
+ className: "dshNovelSkillTree",
1311
+ files: true,
1312
+ tree: true,
1313
+ items: (0, react.useMemo)(() => skillTreeItems(skills), [skills]),
1314
+ selectedId,
1315
+ onSelect: setSelectedId
1316
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("section", {
1317
+ className: "dshNovelViewForm",
1318
+ children: selected === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1319
+ className: "dshNovelViewFormBody",
1320
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1321
+ className: "dshNovelViewEmpty",
1322
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1323
+ className: "dshNovelViewMuted",
1324
+ children: skills.length === 0 ? emptyHint() : "选择一份技能阅读。"
1325
+ })
1326
+ })
1327
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1328
+ className: "dshNovelViewFormBody",
1329
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("article", {
1330
+ className: "dshNovelDoc",
1331
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1332
+ className: "dshNovelCrumb",
1333
+ children: crumbOf(selected)
1334
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1335
+ className: "dshNovelMarkdown",
1336
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.MarkdownText, { text: selected.body })
1337
+ })]
1338
+ })
1339
+ })
1340
+ })] });
1341
+ }
1342
+ function crumbOf(skill) {
1343
+ const folder = (skill.folder ?? "").trim();
1344
+ return folder.length === 0 ? skill.title : `${folder.replaceAll("/", " / ")} / ${skill.title}`;
1345
+ }
1346
+ function skillTreeItems(skills) {
1347
+ const folders = /* @__PURE__ */ new Set();
1348
+ for (const skill of skills) {
1349
+ const parts = (skill.folder ?? "").split("/").filter(Boolean);
1350
+ let acc = "";
1351
+ for (const part of parts) {
1352
+ acc = acc.length === 0 ? part : `${acc}/${part}`;
1353
+ folders.add(acc);
1354
+ }
1355
+ }
1356
+ const nodes = [];
1357
+ for (const folder of [...folders].sort()) {
1358
+ const parts = folder.split("/");
1359
+ const parent = parts.slice(0, -1).join("/");
1360
+ nodes.push({
1361
+ id: `dir:${folder}`,
1362
+ name: parts[parts.length - 1] ?? folder,
1363
+ data: {
1364
+ kind: "folder",
1365
+ parentId: parent.length > 0 ? `dir:${parent}` : ""
1366
+ }
1367
+ });
1368
+ }
1369
+ for (const skill of skills) {
1370
+ const folder = (skill.folder ?? "").trim();
1371
+ nodes.push({
1372
+ id: skill.id,
1373
+ name: skill.title,
1374
+ data: {
1375
+ kind: "file",
1376
+ parentId: folder.length > 0 ? `dir:${folder}` : ""
1377
+ }
1378
+ });
1379
+ }
1380
+ return nodes;
1381
+ }
1382
+ //#endregion
1383
+ //#region src/client/NovelWorkspaceView.tsx
1384
+ const sessionMemory = /* @__PURE__ */ new Map();
1385
+ /** Novel workspace occupying the conversation.view tab. Does not switch the active view. */
1386
+ function NovelWorkspaceView({ sessionId, useSessions }) {
1387
+ const workspacePath = useSessions((state) => {
1388
+ const session = state.byId[sessionId];
1389
+ return session?.cwd ?? session?.header?.cwd;
1390
+ });
1391
+ const [section, setSection] = (0, react.useState)(sessionMemory.get(sessionId) ?? "overview");
1392
+ const [isNovel, setIsNovel] = (0, react.useState)(false);
1393
+ const [template, setTemplate] = (0, react.useState)();
1394
+ const [chapters, setChapters] = (0, react.useState)([]);
1395
+ const [styles, setStyles] = (0, react.useState)([]);
1396
+ (0, react.useEffect)(() => {
1397
+ sessionMemory.set(sessionId, section);
1398
+ }, [section, sessionId]);
1399
+ const refreshStatus = (0, react.useCallback)(async () => {
1400
+ if (workspacePath === void 0) {
1401
+ setIsNovel(false);
1402
+ return;
1403
+ }
1404
+ const status = await novelApi(`/status?path=${encodeURIComponent(workspacePath)}`);
1405
+ setIsNovel(status.novel);
1406
+ if (!status.novel) return;
1407
+ const listed = await novelApi(`/styles?path=${encodeURIComponent(workspacePath)}`).catch(() => []);
1408
+ if (listed.length > 0) setStyles(listed);
1409
+ setTemplate(await novelApi(`/template?path=${encodeURIComponent(workspacePath)}`));
1410
+ setChapters(await listEntities(workspacePath, "chapter"));
1411
+ }, [workspacePath]);
1412
+ (0, react.useEffect)(() => {
1413
+ refreshStatus();
1414
+ }, [refreshStatus]);
1415
+ useLiveReload(refreshStatus);
1416
+ const pageProps = {
1417
+ path: workspacePath ?? "",
1418
+ template,
1419
+ chapters
1420
+ };
1421
+ if (workspacePath === void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1422
+ className: "dshNovelView",
1423
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1424
+ className: "dshNovelViewEmpty",
1425
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: "先在左侧打开或新建会话,并绑定工作区目录。" })
1426
+ })
1427
+ });
1428
+ if (!isNovel) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1429
+ className: "dshNovelView",
1430
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1431
+ className: "dshNovelViewEmpty",
1432
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: "当前目录还不是小说工作区。" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1433
+ className: "dshNovelViewMuted",
1434
+ children: "在对话里让 AI 初始化小说工作区。"
1435
+ })]
1436
+ })
1437
+ });
1438
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1439
+ className: "dshNovelView",
1440
+ children: [
1441
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("nav", {
1442
+ className: "dshNovelViewRail",
1443
+ "aria-label": "小说分区",
1444
+ children: CONTENT_SECTIONS.map((key) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
1445
+ label: DOMAIN_LABELS[key],
1446
+ side: "right",
1447
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1448
+ type: "button",
1449
+ className: "dshNovelViewRailBtn",
1450
+ "data-active": section === key || void 0,
1451
+ "aria-label": DOMAIN_LABELS[key],
1452
+ onClick: () => {
1453
+ setSection(key);
1454
+ },
1455
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SectionIcon, { section: key })
1456
+ })
1457
+ }, key))
1458
+ }),
1459
+ section === "overview" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(OverviewSection, {
1460
+ path: workspacePath,
1461
+ template,
1462
+ styles
1463
+ }),
1464
+ section === "world" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorldSection, { ...pageProps }),
1465
+ section === "role" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RoleSection, { ...pageProps }),
1466
+ section === "creature" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CreatureSection, { ...pageProps }),
1467
+ section === "item" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ItemSection, { ...pageProps }),
1468
+ section === "level" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LevelSection, { ...pageProps }),
1469
+ section === "timeline" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TimelineSection, { ...pageProps }),
1470
+ section === "outline" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(OutlineSection, { ...pageProps }),
1471
+ section === "chapter" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChapterSection, { ...pageProps }),
1472
+ section === "skill" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SkillSection, { path: workspacePath })
1473
+ ]
1474
+ });
1475
+ }
1476
+ //#endregion
1477
+ //#region src/client/styles.ts
1478
+ const NOVEL_STYLES = `
1479
+ .dshNovelView { display: flex; height: 100%; min-height: 0; width: 100%; overflow: hidden; color: var(--dsw-alias-label-primary, inherit); font-size: 13px; line-height: 1.45; background: var(--dsw-alias-bg-base); }
1480
+ .dshNovelViewRail { display: flex; flex-direction: column; gap: 2px; width: 46px; flex-shrink: 0; overflow: auto; padding: 8px 4px; border-right: 1px solid var(--dsw-alias-border-l2); }
1481
+ .dshNovelViewRail button { appearance: none; display: inline-flex; align-items: center; justify-content: center; width: 38px; height: 38px; padding: 0; border: 0; border-radius: 8px; background: transparent; color: var(--dsw-alias-label-secondary, inherit); cursor: pointer; transition-property: transform, background-color, color; transition-duration: 120ms; transition-timing-function: cubic-bezier(0.2, 0, 0, 1); }
1482
+ .dshNovelViewRail button:hover { background: var(--dsw-alias-interactive-bg-hover); color: var(--dsw-alias-label-primary, inherit); }
1483
+ .dshNovelViewRail button:active { transform: scale(0.96); }
1484
+ .dshNovelViewRail button[data-active] { background: var(--dsw-alias-interactive-bg-hover); color: var(--dsw-alias-label-primary, inherit); }
1485
+ .dshNovelViewList { display: flex; flex-direction: column; width: 200px; flex-shrink: 0; min-width: 0; min-height: 0; border-right: 1px solid var(--dsw-alias-border-l2); overflow: hidden; }
1486
+ .dshNovelSkillTree { width: 240px; }
1487
+ .dshNovelViewListToolbar { display: flex; align-items: center; gap: 4px; padding: 8px 8px 4px; min-width: 0; overflow: hidden; }
1488
+ .dshNovelViewSearch { display: flex; flex: 1; min-width: 0; overflow: hidden; box-sizing: border-box; }
1489
+ .dshNovelViewSearch > span, .dshNovelInput { display: flex !important; width: 100%; min-width: 0; max-width: 100%; box-sizing: border-box; }
1490
+ .dshNovelControl { display: block; width: 100%; max-width: 100%; box-sizing: border-box; min-width: 0; border: 1px solid var(--dsw-alias-border-l2); border-radius: 8px; background: var(--dsw-alias-bg-layer-1, var(--dsw-alias-bg-base)); color: var(--dsw-alias-label-primary, inherit); padding: 5px 8px; font-size: 14px; line-height: 22px; }
1491
+ .dshNovelControl:focus { outline: none; border-color: var(--dsw-alias-brand-primary); }
1492
+ textarea.dshNovelControl { min-height: 88px; resize: vertical; }
1493
+ .dshNovelJsonEditor { flex: 1; min-height: 0; height: 100%; width: 100%; resize: none; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; line-height: 18px; padding: 12px; border: 0; border-radius: 12px; background: var(--dsw-alias-markdown-code-block, var(--dsw-alias-bg-layer-1)); color: var(--dsw-alias-label-primary, inherit); box-sizing: border-box; }
1494
+ .dshNovelJsonEditor:focus { outline: none; box-shadow: inset 0 0 0 1px var(--dsw-alias-brand-primary); }
1495
+ .dshNovelTabs { display: flex; gap: 36px; margin-top: 4px; padding-left: 8px; min-height: 0; }
1496
+ .dshNovelTabs button { appearance: none; position: relative; padding: 0 0 11px; border: 0; background: transparent; font-size: 13px; line-height: 16px; font-weight: 500; color: var(--dsw-alias-label-tertiary); cursor: pointer; }
1497
+ .dshNovelTabs button::after { content: ""; position: absolute; right: 0; bottom: 1px; left: 0; height: 2px; border-radius: 2px; background: transparent; }
1498
+ .dshNovelTabs button[data-active] { color: var(--dsw-alias-state-business-primary); }
1499
+ .dshNovelTabs button[data-active]::after { background: var(--dsw-alias-state-business-primary); }
1500
+ .dshNovelEntry { display: flex; flex-direction: column; gap: 6px; padding: 10px 12px; border: 1px solid var(--dsw-alias-border-l2); border-radius: 6px; }
1501
+ .dshNovelEntryTitle { font-size: 14px; font-weight: 500; line-height: 22px; }
1502
+ .dshNovelTier { display: flex; flex-direction: column; gap: 8px; padding: 12px; border: 1px solid var(--dsw-alias-border-l2); border-radius: 6px; }
1503
+ .dshNovelPills { display: flex; flex-wrap: wrap; gap: 6px; }
1504
+ .dshNovelTitle { margin: 0; font-size: 20px; font-weight: 600; line-height: 28px; }
1505
+ .dshNovelMeta { margin: 0; color: var(--dsw-alias-label-secondary); font-size: 13px; line-height: 20px; }
1506
+ .dshNovelProse { margin: 0; white-space: pre-wrap; font-size: 14px; line-height: 24px; }
1507
+ .dshNovelReadValue { white-space: pre-wrap; font-size: 14px; line-height: 22px; color: var(--dsw-alias-label-primary, inherit); }
1508
+ .dshNovelViewListBody { flex: 1; min-height: 0; overflow: auto; padding: 4px 6px 12px; }
1509
+ .dshNovelViewRow { appearance: none; display: flex; align-items: center; gap: 6px; width: 100%; min-height: 32px; text-align: left; border: 0; background: transparent; color: inherit; padding: 0 8px; border-radius: 6px; cursor: pointer; transition-property: transform, background-color, color; transition-duration: 120ms; transition-timing-function: cubic-bezier(0.2, 0, 0, 1); }
1510
+ .dshNovelViewRow:hover { background: var(--dsw-alias-interactive-bg-hover); }
1511
+ .dshNovelViewRow:active { transform: scale(0.96); }
1512
+ .dshNovelViewRow[data-active] { background: var(--dsw-alias-interactive-bg-hover); color: var(--dsw-alias-label-primary); }
1513
+ .dshNovelViewRow[data-kind="folder"] { color: var(--dsw-alias-label-secondary); font-weight: 500; }
1514
+ .dshNovelViewRowIcon { display: inline-flex; width: 16px; height: 16px; flex-shrink: 0; color: currentColor; }
1515
+ .dshNovelViewChevron { display: inline-flex; align-items: center; justify-content: center; width: 14px; flex-shrink: 0; transition-property: transform; transition-duration: 120ms; color: var(--dsw-alias-label-tertiary, inherit); }
1516
+ .dshNovelViewChevron[data-open] { transform: rotate(90deg); }
1517
+ .dshNovelDoc { display: flex; flex-direction: column; gap: 12px; max-width: 680px; width: 100%; }
1518
+ .dshNovelCrumb { margin: 0; font-size: 12px; line-height: 18px; color: var(--dsw-alias-label-tertiary); }
1519
+ .dshNovelMarkdown { font-size: 14px; line-height: 24px; color: var(--dsw-alias-label-primary); }
1520
+ .dshNovelViewForm { flex: 1; min-width: 0; min-height: 0; display: flex; flex-direction: column; overflow: hidden; }
1521
+ .dshNovelViewFormHead { display: flex; align-items: flex-end; justify-content: space-between; gap: 12px; padding: 4px 16px 0 12px; border-bottom: 1px solid var(--dsw-alias-border-l2); min-width: 0; }
1522
+ .dshNovelViewFormHeadTabs { min-width: 0; overflow: hidden; }
1523
+ .dshNovelViewFormHeadActions { display: flex; flex: none; align-items: center; gap: 8px; margin-left: auto; padding-bottom: 6px; }
1524
+ .dshNovelViewFormBody { flex: 1; min-height: 0; overflow: auto; padding: 12px; display: flex; flex-direction: column; gap: 12px; }
1525
+ .dshNovelViewField { display: flex; flex-direction: column; gap: 4px; }
1526
+ .dshNovelViewField label { font-size: 12px; color: var(--dsw-alias-label-secondary, inherit); }
1527
+ .dshNovelViewEmpty { margin: auto; padding: 32px 20px; display: flex; flex-direction: column; gap: 12px; max-width: 420px; width: 100%; }
1528
+ .dshNovelViewMuted { color: var(--dsw-alias-label-secondary, inherit); font-size: 12px; }
1529
+ .dshNovelViewError { color: var(--dsw-alias-state-error-primary, tomato); font-size: 12px; }
1530
+ `;
1531
+ function installNovelStyles() {
1532
+ const style = document.createElement("style");
1533
+ style.dataset.plugin = "dsh-novel";
1534
+ style.dataset.pluginCss = "dsh-novel/content";
1535
+ style.textContent = NOVEL_STYLES;
1536
+ document.head.appendChild(style);
1537
+ return () => {
1538
+ style.remove();
1539
+ };
1540
+ }
1541
+ //#endregion
1542
+ //#region src/client/index.ts
1543
+ const inject = ["slots", "sessions"];
1544
+ function apply(ctx) {
1545
+ ctx.effect(() => installNovelStyles(), "dsh-novel: content tab styles");
1546
+ ctx.slots.inject("conversation.view", () => ctx.slots.register({
1547
+ name: "conversation.view",
1548
+ id: "novel",
1549
+ order: 20,
1550
+ label: () => "内容"
1551
+ }, NovelWorkspaceView));
1552
+ }
1553
+ //#endregion
1554
+ exports.apply = apply;
1555
+ exports.inject = inject;
1556
+ return module.exports;
1557
+ }
1558
+ });
1559
+
1560
+ //# sourceMappingURL=client.js.map