@lingjingai/script-editor 0.0.1

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/dist/index.js ADDED
@@ -0,0 +1,3568 @@
1
+ import { createContext, memo, useState, useMemo, useCallback, useRef, useContext, useEffect, useLayoutEffect, Fragment as Fragment$1, useDeferredValue } from 'react';
2
+ import { Plus, Trash2, Check, Eraser, ArrowUp, ArrowDown, GripVertical, MessageCircle, Film, Brain, BookOpen, Scissors, ArrowUpToLine, Globe, BookText, ChevronRight, FileText, Users, MapPin, Package, Undo2, Redo2, MessageSquareQuote, ArrowLeft, ChevronLeft, Loader2, AlertCircle } from 'lucide-react';
3
+ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
4
+ import { Virtuoso } from 'react-virtuoso';
5
+ import { createPortal } from 'react-dom';
6
+
7
+ var __defProp = Object.defineProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+
13
+ // src/selection.ts
14
+ function buildSelectionFrom(kind, id) {
15
+ if (!id) return void 0;
16
+ if (kind === "actor") return { kind: "actor", actorId: id };
17
+ if (kind === "location") return { kind: "location", locationId: id };
18
+ return { kind: "prop", propId: id };
19
+ }
20
+ function fromToSelection(from) {
21
+ if (from.kind === "actor") return { kind: "actor", actorId: from.actorId };
22
+ if (from.kind === "location") return { kind: "location", locationId: from.locationId };
23
+ return { kind: "prop", propId: from.propId };
24
+ }
25
+ var EMPTY_SELECTION = { kind: "empty" };
26
+ function withOrigin(sel, origin) {
27
+ return { ...sel, origin };
28
+ }
29
+ function selectionTargetKey(s) {
30
+ switch (s.kind) {
31
+ case "scene":
32
+ return `scene:${s.episodeIdx}:${s.sceneIdx}`;
33
+ case "episode":
34
+ return `episode:${s.episodeIdx}`;
35
+ case "clip":
36
+ return `clip:${s.episodeIdx}:${s.sceneId}:${s.clipId}`;
37
+ case "actor":
38
+ return `actor:${s.actorId}`;
39
+ case "location":
40
+ return `location:${s.locationId}`;
41
+ case "prop":
42
+ return `prop:${s.propId}`;
43
+ case "worldview":
44
+ return "worldview";
45
+ default:
46
+ return "empty";
47
+ }
48
+ }
49
+ function isSameSelectionTarget(a, b) {
50
+ return selectionTargetKey(a) === selectionTargetKey(b);
51
+ }
52
+ function isSceneSelected(selection, episodeIdx, sceneIdx) {
53
+ return selection.kind === "scene" && selection.episodeIdx === episodeIdx && selection.sceneIdx === sceneIdx;
54
+ }
55
+ function isClipSelected(selection, episodeIdx, sceneId, clipId) {
56
+ return selection.kind === "clip" && selection.episodeIdx === episodeIdx && selection.sceneId === sceneId && selection.clipId === clipId;
57
+ }
58
+ function getSelectionEpisodeIdx(selection) {
59
+ if (selection.kind === "episode") return selection.episodeIdx;
60
+ if (selection.kind === "scene") return selection.episodeIdx;
61
+ if (selection.kind === "clip") return selection.episodeIdx;
62
+ return null;
63
+ }
64
+ function isEpisodeOpen(selection, episodeIdx) {
65
+ if (selection.kind === "episode") return selection.episodeIdx === episodeIdx;
66
+ if (selection.kind === "scene") return selection.episodeIdx === episodeIdx;
67
+ if (selection.kind === "clip") return selection.episodeIdx === episodeIdx;
68
+ return false;
69
+ }
70
+
71
+ // src/types.ts
72
+ var ScriptContentType = /* @__PURE__ */ ((ScriptContentType2) => {
73
+ ScriptContentType2["Action"] = "action";
74
+ ScriptContentType2["Dialogue"] = "dialogue";
75
+ ScriptContentType2["Narration"] = "narration";
76
+ ScriptContentType2["InnerThought"] = "inner_thought";
77
+ return ScriptContentType2;
78
+ })(ScriptContentType || {});
79
+
80
+ // src/emotion-labels.ts
81
+ var EMOTION_LABELS = {
82
+ "v.o.": "\u753B\u5916\u97F3",
83
+ absorbed: "\u4E13\u6CE8\u6295\u5165",
84
+ accusing: "\u6307\u8D23",
85
+ aggressive: "\u5484\u5484\u903C\u4EBA",
86
+ agonized: "\u75DB\u82E6",
87
+ alarmed: "\u8B66\u89C9",
88
+ alert: "\u8B66\u60D5",
89
+ amused: "\u73A9\u5473",
90
+ analytical: "\u51B7\u9759\u5206\u6790",
91
+ anguished: "\u60B2\u75DB",
92
+ appalled: "\u9A87\u7136",
93
+ authoritative: "\u6743\u5A01",
94
+ awed: "\u656C\u754F",
95
+ awestruck: "\u9707\u64BC",
96
+ "battle-ready": "\u4E34\u6218",
97
+ begging: "\u54C0\u6C42",
98
+ bewildered: "\u832B\u7136",
99
+ bitter: "\u6028\u6068",
100
+ boastful: "\u5F97\u610F",
101
+ broken: "\u5D29\u6E83",
102
+ brutal: "\u51F6\u72E0",
103
+ calculating: "\u7B97\u8BA1",
104
+ calm: "\u51B7\u9759",
105
+ casual: "\u968F\u610F",
106
+ cautious: "\u8C28\u614E",
107
+ chanting: "\u9F50\u58F0\u9AD8\u547C",
108
+ choking: "\u54FD\u54BD",
109
+ chuckling: "\u4F4E\u7B11",
110
+ cold: "\u51B7\u6DE1",
111
+ commanding: "\u547D\u4EE4\u5F0F",
112
+ confident: "\u81EA\u4FE1",
113
+ confused: "\u56F0\u60D1",
114
+ contemptuous: "\u8F7B\u8511",
115
+ crazed: "\u766B\u72C2",
116
+ cruel: "\u6B8B\u9177",
117
+ crying: "\u54ED\u6CE3",
118
+ "crying out": "\u5931\u58F0\u558A\u53EB",
119
+ "crying softly": "\u8F7B\u58F0\u555C\u6CE3",
120
+ curious: "\u597D\u5947",
121
+ "darkly amused": "\u9634\u51B7\u73A9\u5473",
122
+ deadly: "\u81F4\u547D\u72E0\u5389",
123
+ decisive: "\u679C\u51B3",
124
+ deductive: "\u63A8\u7406\u5F0F",
125
+ defeated: "\u632B\u8D25",
126
+ defensive: "\u6212\u5907",
127
+ defiant: "\u4E0D\u670D\u8F93",
128
+ deliberate: "\u6C89\u7740",
129
+ desperate: "\u7EDD\u671B",
130
+ determined: "\u575A\u5B9A",
131
+ disbelief: "\u96BE\u4EE5\u7F6E\u4FE1",
132
+ disbelieving: "\u4E0D\u6562\u76F8\u4FE1",
133
+ disgusted: "\u538C\u6076",
134
+ dismissive: "\u4E0D\u5C51",
135
+ disturbed: "\u4E0D\u5B89",
136
+ drowsy: "\u56F0\u5026",
137
+ ecstatic: "\u72C2\u559C",
138
+ enraged: "\u66B4\u6012",
139
+ excited: "\u6FC0\u52A8",
140
+ exhausted: "\u75B2\u60EB",
141
+ fierce: "\u51CC\u5389",
142
+ firm: "\u575A\u51B3",
143
+ flashback: "\u56DE\u5FC6\u4E2D",
144
+ focused: "\u4E13\u6CE8",
145
+ frantic: "\u614C\u4E71",
146
+ frightened: "\u60CA\u60E7",
147
+ furious: "\u6124\u6012",
148
+ gasping: "\u5598\u606F",
149
+ gleeful: "\u5F97\u610F\u6D0B\u6D0B",
150
+ grandstanding: "\u4F5C\u79C0\u5F0F",
151
+ grave: "\u51DD\u91CD",
152
+ grim: "\u51B7\u5CFB",
153
+ grinning: "\u54A7\u5634\u800C\u7B11",
154
+ hoarse: "\u6C99\u54D1",
155
+ hopeful: "\u6000\u6709\u5E0C\u671B",
156
+ hopeless: "\u7EDD\u671B\u65E0\u52A9",
157
+ horrified: "\u60CA\u9A87",
158
+ hysterical: "\u6B47\u65AF\u5E95\u91CC",
159
+ "ice-cold": "\u51B0\u51B7",
160
+ iconic: "\u6807\u5FD7\u6027\u59FF\u6001",
161
+ incredulous: "\u96BE\u4EE5\u7F6E\u4FE1",
162
+ inspired: "\u53D7\u5230\u9F13\u821E",
163
+ intense: "\u5F3A\u70C8",
164
+ lazy: "\u61D2\u6563",
165
+ low: "\u4F4E\u58F0",
166
+ maniacal: "\u75AF\u72C2",
167
+ manic: "\u766B\u72C2\u4EA2\u594B",
168
+ measured: "\u514B\u5236",
169
+ menacing: "\u9634\u72E0",
170
+ merciless: "\u6BEB\u4E0D\u7559\u60C5",
171
+ mocking: "\u5632\u8BBD",
172
+ murderous: "\u6740\u6C14\u817E\u817E",
173
+ nervous: "\u7D27\u5F20",
174
+ ominous: "\u4E0D\u7965",
175
+ outraged: "\u6124\u6168",
176
+ panicked: "\u60CA\u614C",
177
+ piercing: "\u5C16\u9510",
178
+ pleading: "\u6073\u6C42",
179
+ pleased: "\u6EE1\u610F",
180
+ precise: "\u7CBE\u51C6\u514B\u5236",
181
+ predatory: "\u730E\u98DF\u8005\u822C",
182
+ probing: "\u8BD5\u63A2",
183
+ protective: "\u4FDD\u62A4\u6B32\u5F3A",
184
+ quiet: "\u8F7B\u58F0",
185
+ realizing: "\u604D\u7136\u610F\u8BC6\u5230",
186
+ "recorded video": "\u5F55\u50CF\u4E2D",
187
+ relieved: "\u5982\u91CA\u91CD\u8D1F",
188
+ resolute: "\u51B3\u7136",
189
+ respectful: "\u606D\u656C",
190
+ roaring: "\u6012\u543C",
191
+ savage: "\u51F6\u6B8B",
192
+ savoring: "\u56DE\u5473\u822C",
193
+ scheming: "\u7B97\u8BA1",
194
+ screaming: "\u5C16\u53EB",
195
+ seething: "\u6012\u706B\u7FFB\u6D8C",
196
+ serious: "\u4E25\u8083",
197
+ shaking: "\u53D1\u6296",
198
+ sharp: "\u51CC\u5389",
199
+ shocked: "\u9707\u60CA",
200
+ shouting: "\u5927\u558A",
201
+ shoving: "\u63A8\u6421\u4E2D",
202
+ shrieking: "\u5C16\u58F0\u60CA\u53EB",
203
+ smiling: "\u5FAE\u7B11",
204
+ smirking: "\u51B7\u7B11",
205
+ smug: "\u81EA\u9E23\u5F97\u610F",
206
+ sobbing: "\u62BD\u6CE3",
207
+ "sobbing uncontrollably": "\u6CE3\u4E0D\u6210\u58F0",
208
+ spiteful: "\u6076\u610F",
209
+ steady: "\u6C89\u7A33",
210
+ steely: "\u51B7\u786C",
211
+ strained: "\u7D27\u7EF7",
212
+ stunned: "\u6014\u4F4F",
213
+ suffocating: "\u7A92\u606F\u822C",
214
+ suspicious: "\u6000\u7591",
215
+ taunting: "\u6311\u8845",
216
+ tense: "\u7D27\u7EF7",
217
+ terrified: "\u60CA\u6050",
218
+ threatening: "\u5A01\u80C1",
219
+ thrilled: "\u5174\u594B\u4E0D\u5DF2",
220
+ thunderous: "\u5982\u96F7\u822C",
221
+ trembling: "\u53D1\u98A4",
222
+ triumphant: "\u5F97\u80DC",
223
+ "to himself": "\u81EA\u8A00\u81EA\u8BED",
224
+ "turning to mia": "\u8F6C\u5411 Mia",
225
+ uneasy: "\u4E0D\u5B89",
226
+ unflinching: "\u6BEB\u4E0D\u9000\u7F29",
227
+ unhinged: "\u5931\u63A7",
228
+ urgent: "\u6025\u5207",
229
+ venomous: "\u6076\u6BD2",
230
+ vicious: "\u51F6\u72E0",
231
+ "video recording": "\u89C6\u9891\u8BB0\u5F55\u4E2D",
232
+ vindicated: "\u626C\u7709\u5410\u6C14",
233
+ voiceover: "\u65C1\u767D",
234
+ weak: "\u865A\u5F31",
235
+ whisper: "\u4F4E\u58F0",
236
+ whispering: "\u8033\u8BED",
237
+ worried: "\u62C5\u5FE7"
238
+ };
239
+ function translateEmotionLabel(emotion) {
240
+ const normalized = emotion?.trim().toLowerCase();
241
+ if (!normalized) return "";
242
+ if (EMOTION_LABELS[normalized]) {
243
+ return EMOTION_LABELS[normalized];
244
+ }
245
+ if (normalized.includes(",")) {
246
+ return normalized.split(",").map((part) => {
247
+ const token = part.trim();
248
+ return EMOTION_LABELS[token] || part.trim();
249
+ }).join("\uFF0C");
250
+ }
251
+ return emotion.trim();
252
+ }
253
+
254
+ // src/utils.ts
255
+ function normalizeId(value) {
256
+ return value?.trim() || "";
257
+ }
258
+ function getActorMap(actors = []) {
259
+ return new Map(actors.map((actor) => [normalizeId(actor.actor_id), actor]));
260
+ }
261
+ function getLocationMap(locations = []) {
262
+ return new Map(locations.map((location) => [normalizeId(location.location_id), location]));
263
+ }
264
+ function getPropMap(props = []) {
265
+ return new Map(props.map((prop) => [normalizeId(prop.prop_id), prop]));
266
+ }
267
+ function getSpeakerMap(speakers = []) {
268
+ return new Map(speakers.map((speaker) => [normalizeId(speaker.speaker_id), speaker]));
269
+ }
270
+ function getActorDisplayName(actor) {
271
+ return actor?.actor_name?.trim() || actor?.name?.trim() || "\u672A\u547D\u540D\u89D2\u8272";
272
+ }
273
+ function getLocationDisplayName(location) {
274
+ return location?.location_name?.trim() || location?.name?.trim() || "\u672A\u547D\u540D\u573A\u666F";
275
+ }
276
+ function getPropDisplayName(prop) {
277
+ return prop?.prop_name?.trim() || prop?.name?.trim() || "\u672A\u547D\u540D\u9053\u5177";
278
+ }
279
+ function getStateDisplayName(state) {
280
+ return state?.state_name?.trim() || state?.name?.trim() || state?.state_id?.trim() || "";
281
+ }
282
+ function getSceneContext(scene) {
283
+ return {
284
+ actors: (scene.context?.actors?.length ? scene.context.actors : scene.actors) ?? [],
285
+ locations: (scene.context?.locations?.length ? scene.context.locations : scene.locations) ?? [],
286
+ props: (scene.context?.props?.length ? scene.context.props : scene.props) ?? []
287
+ };
288
+ }
289
+ function findState(states, stateId) {
290
+ const id = stateId?.trim();
291
+ if (!id || !states) return void 0;
292
+ return states.find((state) => (state.state_id || "").trim() === id);
293
+ }
294
+ var ZH_DIGIT_MAP = {
295
+ "\u96F6": 0,
296
+ "\u3007": 0,
297
+ "\u4E00": 1,
298
+ "\u4E8C": 2,
299
+ "\u4E24": 2,
300
+ "\u4E09": 3,
301
+ "\u56DB": 4,
302
+ "\u4E94": 5,
303
+ "\u516D": 6,
304
+ "\u4E03": 7,
305
+ "\u516B": 8,
306
+ "\u4E5D": 9
307
+ };
308
+ function normalizeFullwidthDigits(value) {
309
+ let result = "";
310
+ for (const char of value) {
311
+ const code = char.charCodeAt(0);
312
+ result += code >= 65296 && code <= 65305 ? String.fromCharCode(code - 65296 + 48) : char;
313
+ }
314
+ return result;
315
+ }
316
+ function parseChineseNumeral(value) {
317
+ const text = value.trim();
318
+ if (!text) return null;
319
+ if (text === "\u5341") return 10;
320
+ if (text.startsWith("\u5341")) return 10 + (ZH_DIGIT_MAP[text.slice(1)] ?? 0);
321
+ if (text.endsWith("\u5341")) return (ZH_DIGIT_MAP[text.slice(0, -1)] ?? 0) * 10;
322
+ if (text.includes("\u767E")) {
323
+ const [left, right] = text.split("\u767E");
324
+ const leftValue = ZH_DIGIT_MAP[left] ?? 0;
325
+ return leftValue * 100 + (parseChineseNumeral(right) ?? 0);
326
+ }
327
+ if (text.includes("\u5341")) {
328
+ const [left, right] = text.split("\u5341");
329
+ return (ZH_DIGIT_MAP[left] ?? 0) * 10 + (right ? ZH_DIGIT_MAP[right] ?? 0 : 0);
330
+ }
331
+ return ZH_DIGIT_MAP[text] ?? null;
332
+ }
333
+ function parseEpisodeMarker(rawDigits) {
334
+ const normalized = normalizeFullwidthDigits(rawDigits.trim());
335
+ if (!normalized) return null;
336
+ if (/^\d+$/.test(normalized)) {
337
+ const parsed = Number(normalized);
338
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
339
+ }
340
+ const chinese = parseChineseNumeral(normalized);
341
+ return chinese && chinese > 0 ? chinese : null;
342
+ }
343
+ var EPISODE_DIGIT_CHARS = "0-9\uFF10-\uFF19\u4E00\u4E8C\u4E09\u56DB\u4E94\u516D\u4E03\u516B\u4E5D\u5341\u767E\u5343\u96F6\u3007\u4E24";
344
+ var EPISODE_MARKER_RE = new RegExp(
345
+ `(?:\u7B2C\\s*([${EPISODE_DIGIT_CHARS}]+)\\s*\u96C6|(?:ep|episode)[_\\-#:. ]*([${EPISODE_DIGIT_CHARS}]+))`,
346
+ "i"
347
+ );
348
+ function getEpisodeNumber(episode, index) {
349
+ const candidates = [episode.episode_id, episode.title];
350
+ for (const candidate of candidates) {
351
+ const value = candidate?.trim();
352
+ if (!value) continue;
353
+ const marker = value.match(EPISODE_MARKER_RE);
354
+ if (marker) {
355
+ const parsed = parseEpisodeMarker(marker[1] || marker[2] || "");
356
+ if (parsed) return parsed;
357
+ }
358
+ const digitsOnly = normalizeFullwidthDigits(value);
359
+ const plain = digitsOnly.match(/\d+/);
360
+ if (plain) {
361
+ const parsed = Number(plain[0]);
362
+ if (Number.isFinite(parsed) && parsed > 0) return parsed;
363
+ }
364
+ }
365
+ return index + 1;
366
+ }
367
+ function getEpisodeNumberLabel(episode, index) {
368
+ return String(getEpisodeNumber(episode, index)).padStart(2, "0");
369
+ }
370
+ function getEpisodeTitle(episode, index) {
371
+ const fallback = `\u7B2C ${getEpisodeNumberLabel(episode, index)} \u96C6`;
372
+ const title = episode.title?.trim();
373
+ if (title) return title;
374
+ const id = episode.episode_id?.trim();
375
+ if (id && !/^ep_/i.test(id)) return id;
376
+ return fallback;
377
+ }
378
+ function getSceneNumber(scene, sceneIndex) {
379
+ const m = scene?.scene_id?.trim().match(/(\d+)$/);
380
+ if (m) {
381
+ const n = Number.parseInt(m[1], 10);
382
+ if (Number.isFinite(n) && n > 0) return n;
383
+ }
384
+ return sceneIndex + 1;
385
+ }
386
+ function getSceneNumberLabel(scene, sceneIndex) {
387
+ return `\u573A ${getSceneNumber(scene, sceneIndex)}`;
388
+ }
389
+ function getSceneSidebarLabel(scene, sceneIndex) {
390
+ const sceneId = scene.scene_id?.trim() || String(sceneIndex + 1);
391
+ const space = scene.environment?.space === "exterior" ? "\u5916" : "\u5185";
392
+ const timeKey = scene.environment?.time || "day";
393
+ const timeMap = { day: "\u65E5", night: "\u591C", dawn: "\u6668", dusk: "\u660F" };
394
+ const time = timeMap[timeKey] || timeKey;
395
+ return `${sceneId} \xB7 ${time}${space}`;
396
+ }
397
+ function isActorBearingContentType(type) {
398
+ return type === "dialogue" /* Dialogue */ || type === "inner_thought" /* InnerThought */;
399
+ }
400
+ function getSpeakerDisplayName(speakerId, speakerMap = /* @__PURE__ */ new Map(), actorMap = /* @__PURE__ */ new Map()) {
401
+ const id = normalizeId(speakerId);
402
+ if (!id) return "";
403
+ const speaker = speakerMap.get(id);
404
+ if (speaker?.display_name?.trim()) return speaker.display_name.trim();
405
+ const sourceKind = speaker?.source_kind?.trim();
406
+ const sourceId = normalizeId(speaker?.source_id);
407
+ if (sourceKind === "actor" && sourceId) {
408
+ return getActorDisplayName(actorMap.get(sourceId)) || sourceId;
409
+ }
410
+ const actorId = id.startsWith("spk_") ? id.slice(4) : "";
411
+ if (actorId && actorMap.has(actorId)) return getActorDisplayName(actorMap.get(actorId));
412
+ return id;
413
+ }
414
+ function getDialogueSpeakerLabel(item, actorMap, speakerMap = /* @__PURE__ */ new Map()) {
415
+ if (item.delivery === "overlap" && Array.isArray(item.lines) && item.lines.length > 0) {
416
+ return item.lines.map((line) => getSpeakerDisplayName(line.speaker_id, speakerMap, actorMap)).filter(Boolean).join(" / ");
417
+ }
418
+ const speakerIds = Array.isArray(item.speakers) ? item.speakers.map((speaker) => normalizeId(speaker.speaker_id)).filter(Boolean) : [];
419
+ if (speakerIds.length > 0) {
420
+ return speakerIds.map((speakerId) => getSpeakerDisplayName(speakerId, speakerMap, actorMap)).join(" / ");
421
+ }
422
+ if (item.speaker_id) return getSpeakerDisplayName(item.speaker_id, speakerMap, actorMap);
423
+ const actorId = normalizeId(item.actor_id);
424
+ const actor = actorMap.get(actorId);
425
+ return actor ? getActorDisplayName(actor) : actorId || "\u672A\u77E5\u89D2\u8272";
426
+ }
427
+ function getDialogueDeliveryLabel(delivery) {
428
+ if (delivery === "simultaneous") return "\u540C\u65F6";
429
+ if (delivery === "overlap") return "\u91CD\u53E0";
430
+ if (delivery === "group") return "\u7FA4\u4F53";
431
+ return "";
432
+ }
433
+
434
+ // src/cn.ts
435
+ function cn(...parts) {
436
+ return parts.filter(Boolean).join(" ");
437
+ }
438
+ function GroupHeader({
439
+ icon: Icon,
440
+ label,
441
+ count,
442
+ open,
443
+ highlighted,
444
+ onToggle
445
+ }) {
446
+ const inner = /* @__PURE__ */ jsxs(Fragment, { children: [
447
+ onToggle ? /* @__PURE__ */ jsx(
448
+ ChevronRight,
449
+ {
450
+ className: cn("lj-se-gh-chev", open && "is-open"),
451
+ strokeWidth: 2
452
+ }
453
+ ) : null,
454
+ /* @__PURE__ */ jsx(Icon, { className: "lj-se-gh-icon", strokeWidth: 1.5 }),
455
+ /* @__PURE__ */ jsx("span", { className: "lj-se-gh-label", children: label }),
456
+ count != null ? /* @__PURE__ */ jsx("span", { className: "lj-se-gh-count", children: count }) : null
457
+ ] });
458
+ return onToggle ? /* @__PURE__ */ jsx("button", { type: "button", className: cn("lj-se-gh", highlighted && "is-hl"), onClick: onToggle, children: inner }) : /* @__PURE__ */ jsx("div", { className: cn("lj-se-gh", highlighted && "is-hl"), children: inner });
459
+ }
460
+ function StudioToc({
461
+ data,
462
+ selection,
463
+ onSelect,
464
+ onAddEpisode
465
+ }) {
466
+ const episodes = data.episodes ?? [];
467
+ const [episodesOpen, setEpisodesOpen] = useState(true);
468
+ const [openEpisodes, setOpenEpisodes] = useState(() => {
469
+ const init = /* @__PURE__ */ new Set();
470
+ if (selection.kind === "scene" || selection.kind === "episode") init.add(selection.episodeIdx);
471
+ else init.add(0);
472
+ return init;
473
+ });
474
+ const toggleEp = (i) => setOpenEpisodes((prev) => {
475
+ const next = new Set(prev);
476
+ next.has(i) ? next.delete(i) : next.add(i);
477
+ return next;
478
+ });
479
+ const pick = (sel) => onSelect(withOrigin(sel, "sidebar"));
480
+ const epHighlighted = selection.kind === "episode" || selection.kind === "scene" || selection.kind === "clip";
481
+ return /* @__PURE__ */ jsxs("nav", { className: "lj-se-toc", children: [
482
+ /* @__PURE__ */ jsxs(
483
+ "button",
484
+ {
485
+ type: "button",
486
+ className: cn("lj-se-tocitem lj-se-toc-l0", selection.kind === "worldview" && "is-active"),
487
+ onClick: () => pick({ kind: "worldview" }),
488
+ children: [
489
+ /* @__PURE__ */ jsx(Globe, { className: "lj-se-tocitem-icon lj-se-ic-4", strokeWidth: 1.5 }),
490
+ /* @__PURE__ */ jsx("span", { className: "lj-se-tocitem-label", children: "\u4E16\u754C\u89C2 & \u98CE\u683C" })
491
+ ]
492
+ }
493
+ ),
494
+ /* @__PURE__ */ jsx(
495
+ GroupHeader,
496
+ {
497
+ icon: BookText,
498
+ label: "\u5267\u96C6",
499
+ count: episodes.length,
500
+ open: episodesOpen,
501
+ highlighted: epHighlighted,
502
+ onToggle: () => setEpisodesOpen((v) => !v)
503
+ }
504
+ ),
505
+ episodesOpen ? episodes.length === 0 ? /* @__PURE__ */ jsx("div", { className: "lj-se-toc-empty", children: "\u5C1A\u65E0\u5267\u96C6" }) : episodes.map((episode, ei) => {
506
+ const scenes = episode.scenes ?? [];
507
+ const epOpen = openEpisodes.has(ei);
508
+ const epActive = selection.kind === "episode" && selection.episodeIdx === ei;
509
+ return /* @__PURE__ */ jsxs("div", { children: [
510
+ /* @__PURE__ */ jsxs("div", { className: cn("lj-se-eprow", epActive && "is-active"), children: [
511
+ /* @__PURE__ */ jsx("button", { type: "button", className: "lj-se-eprow-chev", onClick: () => toggleEp(ei), "aria-label": "\u5C55\u5F00/\u6536\u8D77", children: /* @__PURE__ */ jsx(ChevronRight, { className: cn("lj-se-ic-3", epOpen && "is-open"), strokeWidth: 2 }) }),
512
+ /* @__PURE__ */ jsxs("button", { type: "button", className: "lj-se-eprow-main", onClick: () => pick({ kind: "episode", episodeIdx: ei }), children: [
513
+ /* @__PURE__ */ jsx(FileText, { className: "lj-se-ic-3 lj-se-eprow-glyph", strokeWidth: 1.5 }),
514
+ /* @__PURE__ */ jsx("span", { className: "lj-se-eprow-num", children: getEpisodeNumberLabel(episode, ei) }),
515
+ /* @__PURE__ */ jsx("span", { className: "lj-se-eprow-title", children: getEpisodeTitle(episode, ei) }),
516
+ /* @__PURE__ */ jsxs("span", { className: "lj-se-eprow-count", children: [
517
+ scenes.length,
518
+ " \u573A"
519
+ ] })
520
+ ] })
521
+ ] }),
522
+ epOpen ? scenes.map((scene, si) => {
523
+ const active = selection.kind === "scene" && selection.episodeIdx === ei && selection.sceneIdx === si;
524
+ return /* @__PURE__ */ jsxs(
525
+ "button",
526
+ {
527
+ type: "button",
528
+ className: cn("lj-se-tocitem lj-se-toc-l2", active && "is-active"),
529
+ onClick: () => pick({ kind: "scene", episodeIdx: ei, sceneIdx: si }),
530
+ children: [
531
+ /* @__PURE__ */ jsx("span", { className: "lj-se-toc-dot", children: "\xB7" }),
532
+ /* @__PURE__ */ jsx("span", { className: "lj-se-tocitem-label", children: getSceneNumberLabel(scene, si) }),
533
+ /* @__PURE__ */ jsx("span", { className: "lj-se-tocitem-meta", children: getSceneSidebarLabel(scene, si) })
534
+ ]
535
+ },
536
+ `sc-${si}`
537
+ );
538
+ }) : null
539
+ ] }, `ep-${ei}`);
540
+ }) : null,
541
+ episodesOpen && onAddEpisode ? /* @__PURE__ */ jsxs("button", { type: "button", className: "lj-se-toc-addep", onClick: onAddEpisode, children: [
542
+ /* @__PURE__ */ jsx(Plus, { className: "lj-se-ic-3", strokeWidth: 2 }),
543
+ " \u65B0\u589E\u4E00\u96C6"
544
+ ] }) : null,
545
+ /* @__PURE__ */ jsx(
546
+ EntityGroup,
547
+ {
548
+ icon: Users,
549
+ label: "\u89D2\u8272",
550
+ emptyText: "\u5C1A\u65E0\u89D2\u8272",
551
+ items: (data.actors ?? []).map((a) => ({ id: (a.actor_id ?? "").trim(), name: getActorDisplayName(a) })),
552
+ highlighted: selection.kind === "actor",
553
+ isActive: (id) => selection.kind === "actor" && selection.actorId === id,
554
+ onPick: (id) => pick({ kind: "actor", actorId: id })
555
+ }
556
+ ),
557
+ /* @__PURE__ */ jsx(
558
+ EntityGroup,
559
+ {
560
+ icon: MapPin,
561
+ label: "\u573A\u666F",
562
+ emptyText: "\u5C1A\u65E0\u573A\u666F",
563
+ items: (data.locations ?? []).map((l) => ({ id: (l.location_id ?? "").trim(), name: getLocationDisplayName(l) })),
564
+ highlighted: selection.kind === "location",
565
+ isActive: (id) => selection.kind === "location" && selection.locationId === id,
566
+ onPick: (id) => pick({ kind: "location", locationId: id })
567
+ }
568
+ ),
569
+ /* @__PURE__ */ jsx(
570
+ EntityGroup,
571
+ {
572
+ icon: Package,
573
+ label: "\u9053\u5177",
574
+ emptyText: "\u5C1A\u65E0\u9053\u5177",
575
+ items: (data.props ?? []).map((p) => ({ id: (p.prop_id ?? "").trim(), name: getPropDisplayName(p) })),
576
+ highlighted: selection.kind === "prop",
577
+ isActive: (id) => selection.kind === "prop" && selection.propId === id,
578
+ onPick: (id) => pick({ kind: "prop", propId: id })
579
+ }
580
+ )
581
+ ] });
582
+ }
583
+ function EntityGroup({
584
+ icon: Icon,
585
+ label,
586
+ emptyText,
587
+ items,
588
+ highlighted,
589
+ isActive,
590
+ onPick
591
+ }) {
592
+ const [open, setOpen] = useState(true);
593
+ const valid = items.filter((i) => i.id);
594
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
595
+ /* @__PURE__ */ jsx(GroupHeader, { icon: Icon, label, count: valid.length, open, highlighted, onToggle: () => setOpen((v) => !v) }),
596
+ open ? valid.length === 0 ? /* @__PURE__ */ jsx("div", { className: "lj-se-toc-empty", children: emptyText }) : valid.map((i) => /* @__PURE__ */ jsxs(
597
+ "button",
598
+ {
599
+ type: "button",
600
+ className: cn("lj-se-tocitem lj-se-toc-l1", isActive(i.id) && "is-active"),
601
+ onClick: () => onPick(i.id),
602
+ children: [
603
+ /* @__PURE__ */ jsx(Icon, { className: "lj-se-tocitem-icon lj-se-ic-3", strokeWidth: 1.5 }),
604
+ /* @__PURE__ */ jsx("span", { className: "lj-se-tocitem-label", children: i.name })
605
+ ]
606
+ },
607
+ i.id
608
+ )) : null
609
+ ] });
610
+ }
611
+ function FormatToggle({
612
+ value,
613
+ onChange
614
+ }) {
615
+ return /* @__PURE__ */ jsx("div", { role: "group", "aria-label": "\u5267\u672C\u683C\u5F0F", className: "lj-se-format", children: [
616
+ { key: "standard", label: "\u6807\u51C6" },
617
+ { key: "asian", label: "\u4E9A\u6D32\u683C\u5F0F" }
618
+ ].map((opt) => /* @__PURE__ */ jsx(
619
+ "button",
620
+ {
621
+ type: "button",
622
+ "aria-pressed": value === opt.key,
623
+ className: cn("lj-se-format-btn", value === opt.key && "is-active"),
624
+ onClick: () => onChange(opt.key),
625
+ children: opt.label
626
+ },
627
+ opt.key
628
+ )) });
629
+ }
630
+ function CanvasTopBar({
631
+ contextLabel,
632
+ format,
633
+ onFormatChange,
634
+ canUndo,
635
+ canRedo,
636
+ onUndo,
637
+ onRedo
638
+ }) {
639
+ return /* @__PURE__ */ jsxs("header", { className: "lj-se-topbar", children: [
640
+ /* @__PURE__ */ jsx("div", { className: "lj-se-topbar-left", children: contextLabel }),
641
+ /* @__PURE__ */ jsxs("div", { className: "lj-se-topbar-right", children: [
642
+ /* @__PURE__ */ jsx(FormatToggle, { value: format, onChange: onFormatChange }),
643
+ onUndo || onRedo ? /* @__PURE__ */ jsxs("div", { className: "lj-se-topbar-undo", children: [
644
+ /* @__PURE__ */ jsx("button", { type: "button", className: "lj-se-rowbtn", disabled: !canUndo, title: "\u64A4\u9500", onClick: onUndo, children: /* @__PURE__ */ jsx(Undo2, { className: "lj-se-ic-4", strokeWidth: 1.75 }) }),
645
+ /* @__PURE__ */ jsx("button", { type: "button", className: "lj-se-rowbtn", disabled: !canRedo, title: "\u91CD\u505A", onClick: onRedo, children: /* @__PURE__ */ jsx(Redo2, { className: "lj-se-ic-4", strokeWidth: 1.75 }) })
646
+ ] }) : null
647
+ ] })
648
+ ] });
649
+ }
650
+ function SaveCapsule({
651
+ saveStatus = "idle",
652
+ diffCount = 0,
653
+ canApplyDiff,
654
+ onDiffPrev,
655
+ onDiffNext,
656
+ onAcceptAll,
657
+ onRejectAll,
658
+ returnLabel,
659
+ onReturn
660
+ }) {
661
+ const showSave = saveStatus !== "idle";
662
+ const hasDiff = diffCount > 0;
663
+ const hasReturn = !!(returnLabel && onReturn);
664
+ if (!showSave && !hasDiff && !hasReturn) return null;
665
+ return /* @__PURE__ */ jsx("div", { className: "lj-se-capsule-anchor", children: /* @__PURE__ */ jsxs("div", { className: "lj-se-capsule", children: [
666
+ hasReturn ? /* @__PURE__ */ jsxs("button", { type: "button", className: "lj-se-capsule-return", title: `\u8FD4\u56DE ${returnLabel}`, onClick: onReturn, children: [
667
+ /* @__PURE__ */ jsx(ArrowLeft, { className: "lj-se-ic-4", strokeWidth: 2.5 }),
668
+ /* @__PURE__ */ jsxs("span", { className: "lj-se-capsule-return-label", children: [
669
+ "\u8FD4\u56DE ",
670
+ returnLabel
671
+ ] })
672
+ ] }) : null,
673
+ hasDiff ? /* @__PURE__ */ jsxs("div", { className: "lj-se-capsule-diff", children: [
674
+ /* @__PURE__ */ jsxs("span", { className: "lj-se-capsule-ping", children: [
675
+ /* @__PURE__ */ jsx("span", { className: "lj-se-capsule-dot" }),
676
+ /* @__PURE__ */ jsx("span", { className: "lj-se-capsule-dot is-ping" })
677
+ ] }),
678
+ /* @__PURE__ */ jsx("span", { className: "lj-se-capsule-count", children: diffCount }),
679
+ /* @__PURE__ */ jsx("span", { className: "lj-se-capsule-muted", children: "\u6539\u52A8" }),
680
+ onDiffPrev ? /* @__PURE__ */ jsx("button", { type: "button", className: "lj-se-capsule-nav", title: "\u4E0A\u4E00\u5904", onClick: onDiffPrev, children: /* @__PURE__ */ jsx(ChevronLeft, { className: "lj-se-ic-3", strokeWidth: 2.25 }) }) : null,
681
+ onDiffNext ? /* @__PURE__ */ jsx("button", { type: "button", className: "lj-se-capsule-nav", title: "\u4E0B\u4E00\u5904", onClick: onDiffNext, children: /* @__PURE__ */ jsx(ChevronRight, { className: "lj-se-ic-3", strokeWidth: 2.25 }) }) : null,
682
+ onRejectAll ? /* @__PURE__ */ jsx("button", { type: "button", className: "lj-se-capsule-reject", disabled: !canApplyDiff, onClick: onRejectAll, children: "\u5168\u90E8\u62D2\u7EDD" }) : null,
683
+ onAcceptAll ? /* @__PURE__ */ jsx("button", { type: "button", className: "lj-se-capsule-accept", disabled: !canApplyDiff, onClick: onAcceptAll, children: "\u5168\u90E8\u63A5\u53D7" }) : null
684
+ ] }) : null,
685
+ showSave && !hasDiff ? /* @__PURE__ */ jsx(SaveStateRow, { status: saveStatus }) : null
686
+ ] }) });
687
+ }
688
+ function SaveStateRow({ status }) {
689
+ if (status === "saving")
690
+ return /* @__PURE__ */ jsxs("span", { className: "lj-se-capsule-save is-saving", children: [
691
+ /* @__PURE__ */ jsx(Loader2, { className: "lj-se-ic-4 lj-se-spin", strokeWidth: 2.25 }),
692
+ " \u4FDD\u5B58\u4E2D"
693
+ ] });
694
+ if (status === "saved")
695
+ return /* @__PURE__ */ jsxs("span", { className: "lj-se-capsule-save is-saved", children: [
696
+ /* @__PURE__ */ jsx(Check, { className: "lj-se-ic-4", strokeWidth: 2.5 }),
697
+ " \u5DF2\u4FDD\u5B58"
698
+ ] });
699
+ return /* @__PURE__ */ jsxs("span", { className: "lj-se-capsule-save is-error", children: [
700
+ /* @__PURE__ */ jsx(AlertCircle, { className: "lj-se-ic-4", strokeWidth: 2.25 }),
701
+ " \u51FA\u9519"
702
+ ] });
703
+ }
704
+
705
+ // src/mutations/editor-utils.ts
706
+ function cloneEpisodes(data) {
707
+ return [...data.episodes || []];
708
+ }
709
+ function withSceneUpdated(data, episodeIndex, sceneIndex, updater) {
710
+ const episodes = cloneEpisodes(data);
711
+ const episode = episodes[episodeIndex];
712
+ if (!episode) return data;
713
+ const scenes = [...episode.scenes || []];
714
+ const scene = scenes[sceneIndex];
715
+ if (!scene) return data;
716
+ scenes[sceneIndex] = updater(scene);
717
+ episodes[episodeIndex] = { ...episode, scenes };
718
+ return { ...data, episodes };
719
+ }
720
+ function normalizeCommaList(value) {
721
+ return value.split(/[,,]/).map((item) => item.trim()).filter(Boolean);
722
+ }
723
+ function setSceneContext(scene, patch) {
724
+ const current = getSceneContext(scene);
725
+ const context = {
726
+ actors: patch.actors ?? current.actors,
727
+ locations: patch.locations ?? current.locations,
728
+ props: patch.props ?? current.props
729
+ };
730
+ return {
731
+ ...scene,
732
+ context,
733
+ actors: context.actors,
734
+ locations: context.locations,
735
+ props: context.props
736
+ };
737
+ }
738
+ function getSceneActorsText(scene) {
739
+ return (getSceneContext(scene).actors || []).map((item) => item.actor_id?.trim() || "").filter(Boolean).join("\uFF0C");
740
+ }
741
+ function getPrimarySceneLocationId(scene) {
742
+ const firstFromIds = scene.location_ids?.find(Boolean)?.trim();
743
+ if (firstFromIds) return firstFromIds;
744
+ return getSceneContext(scene).locations?.[0]?.location_id?.trim() || "";
745
+ }
746
+ function buildSceneDraft(sceneId, fallbackSceneIndex) {
747
+ return {
748
+ sceneId: sceneId.trim() || `1-${fallbackSceneIndex + 1}`,
749
+ location: "",
750
+ space: "interior",
751
+ time: "day",
752
+ actors: ""
753
+ };
754
+ }
755
+ function incrementSceneId(sceneId, fallbackEpisodeIndex, fallbackSceneIndex) {
756
+ const normalized = sceneId?.trim();
757
+ if (!normalized) return `${fallbackEpisodeIndex + 1}-${fallbackSceneIndex + 1}`;
758
+ const match = normalized.match(/^(.*?)(\d+)$/);
759
+ if (!match) return `${fallbackEpisodeIndex + 1}-${fallbackSceneIndex + 1}`;
760
+ return `${match[1]}${String(Number.parseInt(match[2], 10) + 1).padStart(match[2].length, "0")}`;
761
+ }
762
+ function nextEpisodeSceneId(data, episodeIndex) {
763
+ let maxSceneNum = 0;
764
+ const episode = data.episodes?.[episodeIndex];
765
+ for (const scene of episode?.scenes || []) {
766
+ const match = scene.scene_id?.trim().match(/^scn_(\d+)$/);
767
+ if (match) maxSceneNum = Math.max(maxSceneNum, Number.parseInt(match[1], 10));
768
+ }
769
+ return `scn_${String(maxSceneNum + 1).padStart(3, "0")}`;
770
+ }
771
+ function collectEpisodeSceneIds(data, episodeIndex) {
772
+ const sceneIds = /* @__PURE__ */ new Set();
773
+ const episode = data.episodes?.[episodeIndex];
774
+ for (const scene of episode?.scenes || []) {
775
+ const sceneId = scene.scene_id?.trim();
776
+ if (sceneId) sceneIds.add(sceneId);
777
+ }
778
+ return sceneIds;
779
+ }
780
+ function nextAvailableEpisodeSceneId(data, episodeIndex, suggestedId) {
781
+ const used = collectEpisodeSceneIds(data, episodeIndex);
782
+ let candidate = suggestedId.trim() || nextEpisodeSceneId(data, episodeIndex);
783
+ if (!used.has(candidate)) return candidate;
784
+ const match = candidate.match(/^(.*?)(\d+)$/);
785
+ if (!match) return nextEpisodeSceneId(data, episodeIndex);
786
+ let nextNum = Number.parseInt(match[2], 10) + 1;
787
+ while (used.has(candidate)) {
788
+ candidate = `${match[1]}${String(nextNum).padStart(match[2].length, "0")}`;
789
+ nextNum += 1;
790
+ }
791
+ return candidate;
792
+ }
793
+ function nextAvailableSceneId(prevSceneId, existingScenes, fallbackEpisodeIndex, fallbackSceneIndex) {
794
+ const used = /* @__PURE__ */ new Set();
795
+ for (const sc of existingScenes) {
796
+ const id = sc.scene_id?.trim();
797
+ if (id) used.add(id);
798
+ }
799
+ let candidate = incrementSceneId(prevSceneId, fallbackEpisodeIndex, fallbackSceneIndex);
800
+ let attempts = 0;
801
+ while (used.has(candidate) && attempts < 1e3) {
802
+ candidate = incrementSceneId(candidate, fallbackEpisodeIndex, fallbackSceneIndex);
803
+ attempts += 1;
804
+ }
805
+ return candidate;
806
+ }
807
+ function decrementSceneId(sceneId, fallbackEpisodeIndex, fallbackSceneIndex) {
808
+ const normalized = sceneId?.trim();
809
+ if (!normalized) return `${fallbackEpisodeIndex + 1}-${fallbackSceneIndex + 1}`;
810
+ const match = normalized.match(/^(.*?)(\d+)$/);
811
+ if (!match) return `${fallbackEpisodeIndex + 1}-${fallbackSceneIndex + 1}`;
812
+ const n = Number.parseInt(match[2], 10);
813
+ if (n <= 1) return normalized;
814
+ return `${match[1]}${String(n - 1)}`;
815
+ }
816
+ function createSceneFromDraft(draft) {
817
+ const location = draft.location.trim();
818
+ const actors = normalizeCommaList(draft.actors);
819
+ return {
820
+ scene_id: draft.sceneId.trim(),
821
+ environment: {
822
+ space: draft.space.trim() || "interior",
823
+ time: draft.time.trim() || "day"
824
+ },
825
+ location_ids: location ? [location] : [],
826
+ locations: location ? [{ location_id: location, state_id: null }] : [],
827
+ actors: actors.map((actorId) => ({ actor_id: actorId, state_id: null })),
828
+ actions: []
829
+ };
830
+ }
831
+ function createItemFromDraft(draft) {
832
+ if (draft.kind === "dialogue") {
833
+ return {
834
+ type: "dialogue" /* Dialogue */,
835
+ actor_id: draft.actorName.trim(),
836
+ emotion: draft.emotion.trim(),
837
+ content: draft.content.trim()
838
+ };
839
+ }
840
+ return {
841
+ type: draft.actionType.trim() || "action" /* Action */,
842
+ content: draft.content.trim()
843
+ };
844
+ }
845
+ function updateSceneField(data, episodeIndex, sceneIndex, field, value) {
846
+ return withSceneUpdated(data, episodeIndex, sceneIndex, (scene) => {
847
+ if (field === "scene_id") {
848
+ return { ...scene, scene_id: value.trim() };
849
+ }
850
+ if (field === "space" || field === "time") {
851
+ return {
852
+ ...scene,
853
+ environment: {
854
+ ...scene.environment,
855
+ [field]: value.trim()
856
+ }
857
+ };
858
+ }
859
+ if (field === "location") {
860
+ const location = value.trim();
861
+ return setSceneContext({
862
+ ...scene,
863
+ location_ids: location ? [location] : []
864
+ }, { locations: location ? [{ location_id: location, state_id: null }] : [] });
865
+ }
866
+ if (field === "props") {
867
+ const propIds = normalizeCommaList(value);
868
+ const existingPropStateMap = new Map(
869
+ (getSceneContext(scene).props || []).map((item) => [(item.prop_id || "").trim(), item.state_id ?? null])
870
+ );
871
+ return setSceneContext(scene, {
872
+ props: propIds.map((propId) => ({
873
+ prop_id: propId,
874
+ state_id: existingPropStateMap.get(propId) ?? null
875
+ }))
876
+ });
877
+ }
878
+ const actorIds = normalizeCommaList(value);
879
+ const existingStateMap = new Map(
880
+ (getSceneContext(scene).actors || []).map((item) => [(item.actor_id || "").trim(), item.state_id ?? null])
881
+ );
882
+ return setSceneContext(scene, {
883
+ actors: actorIds.map((actorId) => ({
884
+ actor_id: actorId,
885
+ state_id: existingStateMap.get(actorId) ?? null
886
+ }))
887
+ });
888
+ });
889
+ }
890
+ function updateSceneAssetRefState(data, episodeIndex, sceneIndex, field, idKey, targetId, stateId) {
891
+ const normalizedTarget = targetId.trim();
892
+ const normalizedState = stateId?.trim() || null;
893
+ if (!normalizedTarget) return data;
894
+ return withSceneUpdated(data, episodeIndex, sceneIndex, (scene) => {
895
+ const context = getSceneContext(scene);
896
+ const list = context[field] || [];
897
+ let matched = false;
898
+ const next = list.map((item) => {
899
+ if ((item[idKey] || "").toString().trim() !== normalizedTarget) return item;
900
+ matched = true;
901
+ return { ...item, state_id: normalizedState };
902
+ });
903
+ if (!matched) {
904
+ next.push({ [idKey]: normalizedTarget, state_id: normalizedState });
905
+ }
906
+ return setSceneContext(scene, { [field]: next });
907
+ });
908
+ }
909
+ function updateSceneActorState(data, episodeIndex, sceneIndex, actorId, stateId) {
910
+ return updateSceneAssetRefState(data, episodeIndex, sceneIndex, "actors", "actor_id", actorId, stateId);
911
+ }
912
+ function updateSceneLocationState(data, episodeIndex, sceneIndex, locationId, stateId) {
913
+ return updateSceneAssetRefState(data, episodeIndex, sceneIndex, "locations", "location_id", locationId, stateId);
914
+ }
915
+ function updateScenePropState(data, episodeIndex, sceneIndex, propId, stateId) {
916
+ return updateSceneAssetRefState(data, episodeIndex, sceneIndex, "props", "prop_id", propId, stateId);
917
+ }
918
+ function updateSceneItemContent(data, episodeIndex, sceneIndex, itemIndex, content) {
919
+ return withSceneUpdated(data, episodeIndex, sceneIndex, (scene) => {
920
+ const actions = [...scene.actions || []];
921
+ const item = actions[itemIndex];
922
+ if (!item) return scene;
923
+ actions[itemIndex] = { ...item, content: content.trim() };
924
+ return { ...scene, actions };
925
+ });
926
+ }
927
+ function updateSceneItemActor(data, episodeIndex, sceneIndex, itemIndex, actorId) {
928
+ return withSceneUpdated(data, episodeIndex, sceneIndex, (scene) => {
929
+ const actions = [...scene.actions || []];
930
+ const item = actions[itemIndex];
931
+ if (!item) return scene;
932
+ actions[itemIndex] = { ...item, actor_id: actorId.trim() };
933
+ return { ...scene, actions };
934
+ });
935
+ }
936
+ function updateSceneItemEmotion(data, episodeIndex, sceneIndex, itemIndex, emotion) {
937
+ return withSceneUpdated(data, episodeIndex, sceneIndex, (scene) => {
938
+ const actions = [...scene.actions || []];
939
+ const item = actions[itemIndex];
940
+ if (!item) return scene;
941
+ const trimmed = emotion.trim();
942
+ const next = { ...item };
943
+ if (trimmed) next.emotion = trimmed;
944
+ else delete next.emotion;
945
+ actions[itemIndex] = next;
946
+ return { ...scene, actions };
947
+ });
948
+ }
949
+ function replaceSceneItem(data, episodeIndex, sceneIndex, itemIndex, item) {
950
+ return withSceneUpdated(data, episodeIndex, sceneIndex, (scene) => {
951
+ const actions = [...scene.actions || []];
952
+ if (!actions[itemIndex]) return scene;
953
+ actions[itemIndex] = item;
954
+ return { ...scene, actions };
955
+ });
956
+ }
957
+ function addEpisode(data, atIndex) {
958
+ const episodes = [...data.episodes || []];
959
+ const index = atIndex == null ? episodes.length : Math.max(0, Math.min(atIndex, episodes.length));
960
+ const stamp = typeof Date !== "undefined" && typeof Date.now === "function" ? Date.now().toString(36) : String(episodes.length);
961
+ episodes.splice(index, 0, { episode_id: `ep_${stamp}`, title: "", scenes: [] });
962
+ return { ...data, episodes };
963
+ }
964
+ function addBlankScene(data, episodeIndex) {
965
+ const scenes = data.episodes?.[episodeIndex]?.scenes ?? [];
966
+ return insertScene(data, episodeIndex, scenes.length, {
967
+ environment: { space: "interior", time: "day" },
968
+ actions: []
969
+ });
970
+ }
971
+ function insertScene(data, episodeIndex, sceneIndex, scene) {
972
+ const episodes = cloneEpisodes(data);
973
+ const episode = episodes[episodeIndex];
974
+ if (!episode) return data;
975
+ const scenes = [...episode.scenes || []];
976
+ const finalScene = {
977
+ ...scene,
978
+ scene_id: nextAvailableEpisodeSceneId(data, episodeIndex, scene.scene_id || "")
979
+ };
980
+ scenes.splice(sceneIndex, 0, finalScene);
981
+ episodes[episodeIndex] = { ...episode, scenes };
982
+ return { ...data, episodes };
983
+ }
984
+ function splitScene(data, episodeIndex, sceneIndex, splitAtItemIndex) {
985
+ const episodes = cloneEpisodes(data);
986
+ const episode = episodes[episodeIndex];
987
+ if (!episode) return data;
988
+ const scenes = [...episode.scenes || []];
989
+ const scene = scenes[sceneIndex];
990
+ if (!scene) return data;
991
+ const actions = scene.actions || [];
992
+ if (splitAtItemIndex <= 0 || splitAtItemIndex >= actions.length) return data;
993
+ const frontScene = {
994
+ ...scene,
995
+ actions: actions.slice(0, splitAtItemIndex)
996
+ };
997
+ const backScene = {
998
+ ...scene,
999
+ scene_id: nextAvailableEpisodeSceneId(data, episodeIndex, ""),
1000
+ actions: actions.slice(splitAtItemIndex)
1001
+ };
1002
+ const renumberedTail = scenes.slice(sceneIndex + 1);
1003
+ episodes[episodeIndex] = {
1004
+ ...episode,
1005
+ scenes: [...scenes.slice(0, sceneIndex), frontScene, backScene, ...renumberedTail]
1006
+ };
1007
+ return { ...data, episodes };
1008
+ }
1009
+ function mergeSceneIntoPrev(data, episodeIndex, sceneIndex) {
1010
+ if (sceneIndex <= 0) return data;
1011
+ const episodes = cloneEpisodes(data);
1012
+ const episode = episodes[episodeIndex];
1013
+ if (!episode) return data;
1014
+ const scenes = [...episode.scenes || []];
1015
+ const prev = scenes[sceneIndex - 1];
1016
+ const cur = scenes[sceneIndex];
1017
+ if (!prev || !cur) return data;
1018
+ const mergeActors = () => {
1019
+ const seen = /* @__PURE__ */ new Set();
1020
+ const out = [];
1021
+ for (const ref of [...getSceneContext(prev).actors ?? [], ...getSceneContext(cur).actors ?? []]) {
1022
+ const id = ref.actor_id?.trim() || "";
1023
+ if (!id || seen.has(id)) continue;
1024
+ seen.add(id);
1025
+ out.push(ref);
1026
+ }
1027
+ return out;
1028
+ };
1029
+ const mergeLocations = () => {
1030
+ const seen = /* @__PURE__ */ new Set();
1031
+ const out = [];
1032
+ for (const ref of [...getSceneContext(prev).locations ?? [], ...getSceneContext(cur).locations ?? []]) {
1033
+ const id = ref.location_id?.trim() || "";
1034
+ if (!id || seen.has(id)) continue;
1035
+ seen.add(id);
1036
+ out.push(ref);
1037
+ }
1038
+ return out;
1039
+ };
1040
+ const mergeProps = () => {
1041
+ const seen = /* @__PURE__ */ new Set();
1042
+ const out = [];
1043
+ for (const ref of [...getSceneContext(prev).props ?? [], ...getSceneContext(cur).props ?? []]) {
1044
+ const id = ref.prop_id?.trim() || "";
1045
+ if (!id || seen.has(id)) continue;
1046
+ seen.add(id);
1047
+ out.push(ref);
1048
+ }
1049
+ return out;
1050
+ };
1051
+ const actors = mergeActors();
1052
+ const locations = mergeLocations();
1053
+ const props = mergeProps();
1054
+ const merged = setSceneContext({
1055
+ ...prev,
1056
+ actions: [...prev.actions ?? [], ...cur.actions ?? []],
1057
+ // drop the stale flat location_ids (copied from prev): it would disagree
1058
+ // with the merged context.locations, and getPrimarySceneLocationId reads
1059
+ // location_ids first → would silently lose cur's location. Reads fall back
1060
+ // to context.locations which now holds the full merged set.
1061
+ location_ids: void 0
1062
+ }, { actors, locations, props });
1063
+ const tail = scenes.slice(sceneIndex + 1);
1064
+ episodes[episodeIndex] = {
1065
+ ...episode,
1066
+ scenes: [...scenes.slice(0, sceneIndex - 1), merged, ...tail]
1067
+ };
1068
+ return { ...data, episodes };
1069
+ }
1070
+ function deleteScene(data, episodeIndex, sceneIndex) {
1071
+ const episodes = cloneEpisodes(data);
1072
+ const episode = episodes[episodeIndex];
1073
+ if (!episode) return data;
1074
+ const scenes = [...episode.scenes || []];
1075
+ scenes.splice(sceneIndex, 1);
1076
+ episodes[episodeIndex] = { ...episode, scenes };
1077
+ return { ...data, episodes };
1078
+ }
1079
+ function insertSceneItem(data, episodeIndex, sceneIndex, itemIndex, item) {
1080
+ return withSceneUpdated(data, episodeIndex, sceneIndex, (scene) => {
1081
+ const actions = [...scene.actions || []];
1082
+ actions.splice(itemIndex, 0, item);
1083
+ return { ...scene, actions };
1084
+ });
1085
+ }
1086
+ function deleteSceneItem(data, episodeIndex, sceneIndex, itemIndex) {
1087
+ return withSceneUpdated(data, episodeIndex, sceneIndex, (scene) => {
1088
+ const actions = [...scene.actions || []];
1089
+ actions.splice(itemIndex, 1);
1090
+ return { ...scene, actions };
1091
+ });
1092
+ }
1093
+ function moveSceneItem(data, episodeIndex, sceneIndex, fromIndex, toIndex) {
1094
+ return withSceneUpdated(data, episodeIndex, sceneIndex, (scene) => {
1095
+ const actions = [...scene.actions || []];
1096
+ if (fromIndex < 0 || fromIndex >= actions.length) return scene;
1097
+ const clampedTo = Math.max(0, Math.min(toIndex, actions.length));
1098
+ const dest = clampedTo > fromIndex ? clampedTo - 1 : clampedTo;
1099
+ if (dest === fromIndex) return scene;
1100
+ const [moved] = actions.splice(fromIndex, 1);
1101
+ actions.splice(dest, 0, moved);
1102
+ return { ...scene, actions };
1103
+ });
1104
+ }
1105
+ function moveScriptItem(data, from, to) {
1106
+ if (from.episodeIndex === to.episodeIndex && from.sceneIndex === to.sceneIndex) {
1107
+ return moveSceneItem(data, from.episodeIndex, from.sceneIndex, from.itemIndex, to.slot);
1108
+ }
1109
+ const episodes = cloneEpisodes(data);
1110
+ const srcEp = episodes[from.episodeIndex];
1111
+ const dstEp = episodes[to.episodeIndex];
1112
+ if (!srcEp || !dstEp) return data;
1113
+ const srcScenes = [...srcEp.scenes || []];
1114
+ const srcScene = srcScenes[from.sceneIndex];
1115
+ if (!srcScene) return data;
1116
+ const srcActions = [...srcScene.actions || []];
1117
+ if (from.itemIndex < 0 || from.itemIndex >= srcActions.length) return data;
1118
+ const [moved] = srcActions.splice(from.itemIndex, 1);
1119
+ srcScenes[from.sceneIndex] = { ...srcScene, actions: srcActions };
1120
+ episodes[from.episodeIndex] = { ...srcEp, scenes: srcScenes };
1121
+ const dstEp2 = episodes[to.episodeIndex];
1122
+ const dstScenes = [...dstEp2.scenes || []];
1123
+ const dstScene = dstScenes[to.sceneIndex];
1124
+ if (!dstScene) return data;
1125
+ const dstActions = [...dstScene.actions || []];
1126
+ const slot = Math.max(0, Math.min(to.slot, dstActions.length));
1127
+ dstActions.splice(slot, 0, moved);
1128
+ dstScenes[to.sceneIndex] = { ...dstScene, actions: dstActions };
1129
+ episodes[to.episodeIndex] = { ...dstEp2, scenes: dstScenes };
1130
+ return { ...data, episodes };
1131
+ }
1132
+ function deleteEpisode(data, episodeIndex) {
1133
+ const episodes = [...data.episodes || []];
1134
+ if (episodeIndex < 0 || episodeIndex >= episodes.length) return data;
1135
+ episodes.splice(episodeIndex, 1);
1136
+ return { ...data, episodes };
1137
+ }
1138
+
1139
+ // src/entity-distribution.ts
1140
+ function refFields(episode, ei, scene, si) {
1141
+ return {
1142
+ label: `${getEpisodeNumberLabel(episode, ei)} \xB7 ${getSceneNumberLabel(scene, si)}`,
1143
+ episodeIndex: ei,
1144
+ episodeNumber: getEpisodeNumber(episode, ei),
1145
+ sceneIndex: si,
1146
+ sceneNumber: getSceneNumber(scene, si)
1147
+ };
1148
+ }
1149
+ function buildActorDistribution(data, actorId) {
1150
+ const items = [];
1151
+ (data.episodes ?? []).forEach((episode, ei) => {
1152
+ (episode.scenes ?? []).forEach((scene, si) => {
1153
+ const ctx = getSceneContext(scene);
1154
+ const ref = (ctx.actors ?? []).find((i) => (i.actor_id ?? "").trim() === actorId);
1155
+ const inActions = !ref && (scene.actions ?? []).some((i) => (i.actor_id ?? "").trim() === actorId);
1156
+ if (ref || inActions) {
1157
+ items.push({
1158
+ key: `actor-${actorId}-${ei}-${si}`,
1159
+ ...refFields(episode, ei, scene, si),
1160
+ stateId: ref?.state_id
1161
+ });
1162
+ }
1163
+ });
1164
+ });
1165
+ return items;
1166
+ }
1167
+ function buildLocationDistribution(data, locationId) {
1168
+ const items = [];
1169
+ (data.episodes ?? []).forEach((episode, ei) => {
1170
+ (episode.scenes ?? []).forEach((scene, si) => {
1171
+ const inIds = (scene.location_ids ?? []).some((i) => (i ?? "").trim() === locationId);
1172
+ const ctx = getSceneContext(scene);
1173
+ const inCtx = (ctx.locations ?? []).some((i) => (i.location_id ?? "").trim() === locationId);
1174
+ if (inIds || inCtx) {
1175
+ items.push({
1176
+ key: `location-${locationId}-${ei}-${si}`,
1177
+ ...refFields(episode, ei, scene, si)
1178
+ });
1179
+ }
1180
+ });
1181
+ });
1182
+ return items;
1183
+ }
1184
+ function buildPropDistribution(data, propId, propName) {
1185
+ const items = [];
1186
+ const id = propId.trim();
1187
+ const needle = propName.trim().toLowerCase();
1188
+ (data.episodes ?? []).forEach((episode, ei) => {
1189
+ (episode.scenes ?? []).forEach((scene, si) => {
1190
+ const ctx = getSceneContext(scene);
1191
+ const byRef = (ctx.props ?? []).some((i) => (i.prop_id ?? "").trim() === id);
1192
+ const byId = !byRef && id ? (scene.actions ?? []).some((i) => (i.content ?? "").includes(id)) : false;
1193
+ const byName = !byRef && !byId && needle ? (scene.actions ?? []).some((i) => (i.content ?? "").toLowerCase().includes(needle)) : false;
1194
+ if (byRef || byId || byName) {
1195
+ items.push({
1196
+ key: `prop-${id || needle}-${ei}-${si}`,
1197
+ ...refFields(episode, ei, scene, si)
1198
+ });
1199
+ }
1200
+ });
1201
+ });
1202
+ return items;
1203
+ }
1204
+ function groupReferencesByEpisode(distribution) {
1205
+ const map = /* @__PURE__ */ new Map();
1206
+ for (const item of distribution) {
1207
+ if (!map.has(item.episodeIndex)) map.set(item.episodeIndex, []);
1208
+ map.get(item.episodeIndex).push(item);
1209
+ }
1210
+ return Array.from(map.entries()).sort(([a], [b]) => a - b).map(([episodeIndex, items]) => ({
1211
+ episodeIndex,
1212
+ episodeNumber: items[0]?.episodeNumber ?? episodeIndex + 1,
1213
+ items: items.sort((a, b) => a.sceneIndex - b.sceneIndex)
1214
+ }));
1215
+ }
1216
+ var EMPTY_REFS = [];
1217
+ function buildAssetRefs(data, group, assetId) {
1218
+ let groups;
1219
+ if (group === "actors") {
1220
+ groups = groupReferencesByEpisode(buildActorDistribution(data, assetId));
1221
+ } else if (group === "locations") {
1222
+ groups = groupReferencesByEpisode(buildLocationDistribution(data, assetId));
1223
+ } else {
1224
+ const prop = (data.props ?? []).find((p) => (p.prop_id ?? "").trim() === assetId);
1225
+ groups = groupReferencesByEpisode(buildPropDistribution(data, assetId, getPropDisplayName(prop)));
1226
+ }
1227
+ return groups.length === 0 ? EMPTY_REFS : groups;
1228
+ }
1229
+ function sameRefGroups(a, b) {
1230
+ if (a === b) return true;
1231
+ if (a.length !== b.length) return false;
1232
+ for (let i = 0; i < a.length; i++) {
1233
+ const ga = a[i];
1234
+ const gb = b[i];
1235
+ if (ga.episodeIndex !== gb.episodeIndex || ga.episodeNumber !== gb.episodeNumber || ga.items.length !== gb.items.length) {
1236
+ return false;
1237
+ }
1238
+ for (let j = 0; j < ga.items.length; j++) {
1239
+ const ia = ga.items[j];
1240
+ const ib = gb.items[j];
1241
+ if (ia.key !== ib.key || ia.label !== ib.label || ia.sceneIndex !== ib.sceneIndex || ia.sceneNumber !== ib.sceneNumber || ia.episodeNumber !== ib.episodeNumber || ia.stateId !== ib.stateId) {
1242
+ return false;
1243
+ }
1244
+ }
1245
+ }
1246
+ return true;
1247
+ }
1248
+ function buildDistributionIndex(data, prev) {
1249
+ const index = /* @__PURE__ */ new Map();
1250
+ const put = (key, next) => {
1251
+ const old = prev?.get(key);
1252
+ index.set(key, old && sameRefGroups(old, next) ? old : next);
1253
+ };
1254
+ for (const a of data.actors ?? []) {
1255
+ const id = (a.actor_id ?? "").trim();
1256
+ if (id) put(`actors:${id}`, buildAssetRefs(data, "actors", id));
1257
+ }
1258
+ for (const l of data.locations ?? []) {
1259
+ const id = (l.location_id ?? "").trim();
1260
+ if (id) put(`locations:${id}`, buildAssetRefs(data, "locations", id));
1261
+ }
1262
+ for (const p of data.props ?? []) {
1263
+ const id = (p.prop_id ?? "").trim();
1264
+ if (id) put(`props:${id}`, buildAssetRefs(data, "props", id));
1265
+ }
1266
+ return index;
1267
+ }
1268
+ function Popover({
1269
+ trigger,
1270
+ children,
1271
+ align = "start",
1272
+ side = "bottom",
1273
+ className,
1274
+ panelClassName
1275
+ }) {
1276
+ const [open, setOpen] = useState(false);
1277
+ const [coords, setCoords] = useState(null);
1278
+ const rootRef = useRef(null);
1279
+ const panelRef = useRef(null);
1280
+ const target = (() => {
1281
+ if (typeof document === "undefined") return null;
1282
+ return rootRef.current?.closest(".lj-se-root") ?? document.body;
1283
+ })();
1284
+ useLayoutEffect(() => {
1285
+ if (!open || !rootRef.current) return;
1286
+ const measure = () => {
1287
+ const el = rootRef.current;
1288
+ if (!el) return;
1289
+ const r = el.getBoundingClientRect();
1290
+ const vw = window.innerWidth;
1291
+ const vh = window.innerHeight;
1292
+ if (r.bottom <= 0 || r.top >= vh) {
1293
+ setOpen(false);
1294
+ return;
1295
+ }
1296
+ const panelW = panelRef.current?.offsetWidth ?? 232;
1297
+ const flipUp = side === "top" || r.bottom + 320 > vh;
1298
+ let left = r.left;
1299
+ let transform;
1300
+ if (align === "center") {
1301
+ left = r.left + r.width / 2;
1302
+ transform = "translateX(-50%)";
1303
+ } else if (align === "end") {
1304
+ left = r.right;
1305
+ transform = "translateX(-100%)";
1306
+ }
1307
+ if (!transform) left = Math.min(left, vw - panelW - 8);
1308
+ left = Math.max(8, left);
1309
+ setCoords(
1310
+ flipUp ? { bottom: vh - r.top + 4, left, transform } : { top: r.bottom + 4, left, transform }
1311
+ );
1312
+ };
1313
+ measure();
1314
+ const raf = requestAnimationFrame(measure);
1315
+ let throttle = 0;
1316
+ const onReflow = () => {
1317
+ if (throttle) return;
1318
+ throttle = requestAnimationFrame(() => {
1319
+ throttle = 0;
1320
+ measure();
1321
+ });
1322
+ };
1323
+ window.addEventListener("scroll", onReflow, true);
1324
+ window.addEventListener("resize", onReflow);
1325
+ return () => {
1326
+ cancelAnimationFrame(raf);
1327
+ if (throttle) cancelAnimationFrame(throttle);
1328
+ window.removeEventListener("scroll", onReflow, true);
1329
+ window.removeEventListener("resize", onReflow);
1330
+ };
1331
+ }, [open, side, align]);
1332
+ useEffect(() => {
1333
+ if (!open) return;
1334
+ function onDocClick(e) {
1335
+ const t = e.target;
1336
+ if (rootRef.current?.contains(t) || panelRef.current?.contains(t)) return;
1337
+ setOpen(false);
1338
+ }
1339
+ function onKey(e) {
1340
+ if (e.key === "Escape") setOpen(false);
1341
+ }
1342
+ document.addEventListener("mousedown", onDocClick, true);
1343
+ document.addEventListener("keydown", onKey, true);
1344
+ return () => {
1345
+ document.removeEventListener("mousedown", onDocClick, true);
1346
+ document.removeEventListener("keydown", onKey, true);
1347
+ };
1348
+ }, [open]);
1349
+ return /* @__PURE__ */ jsxs("span", { ref: rootRef, className: cn("lj-se-pop-root", className), children: [
1350
+ trigger({ open, toggle: () => setOpen((v) => !v) }),
1351
+ open && target ? createPortal(
1352
+ /* @__PURE__ */ jsx(
1353
+ "div",
1354
+ {
1355
+ ref: panelRef,
1356
+ role: "menu",
1357
+ className: cn("lj-se-pop-panel", panelClassName),
1358
+ style: {
1359
+ position: "fixed",
1360
+ top: coords?.top,
1361
+ bottom: coords?.bottom,
1362
+ left: coords?.left ?? -9999,
1363
+ transform: coords?.transform
1364
+ },
1365
+ children: children({ close: () => setOpen(false) })
1366
+ }
1367
+ ),
1368
+ target
1369
+ ) : null
1370
+ ] });
1371
+ }
1372
+ var TYPES = [
1373
+ { value: "dialogue" /* Dialogue */, label: "\u5BF9\u767D", icon: MessageCircle },
1374
+ { value: "action" /* Action */, label: "\u52A8\u4F5C", icon: Film },
1375
+ { value: "inner_thought" /* InnerThought */, label: "\u5FC3\u58F0", icon: Brain },
1376
+ { value: "narration" /* Narration */, label: "\u65C1\u767D", icon: BookOpen }
1377
+ ];
1378
+ function RowMenuPanel({
1379
+ close,
1380
+ typeSeg,
1381
+ sections
1382
+ }) {
1383
+ return /* @__PURE__ */ jsxs("div", { className: "lj-se-rowmenu-inner", children: [
1384
+ typeSeg ? /* @__PURE__ */ jsxs(Fragment, { children: [
1385
+ /* @__PURE__ */ jsx("div", { className: "lj-se-rowmenu-cap", children: "\u6539\u4E3A" }),
1386
+ /* @__PURE__ */ jsx("div", { className: "lj-se-typeseg", children: TYPES.map((t) => {
1387
+ const Icon = t.icon;
1388
+ const on = t.value === typeSeg.current;
1389
+ return /* @__PURE__ */ jsxs(
1390
+ "button",
1391
+ {
1392
+ type: "button",
1393
+ className: cn("lj-se-typeseg-item", on && "is-on"),
1394
+ onClick: () => {
1395
+ close();
1396
+ if (!on) typeSeg.onChange(t.value);
1397
+ },
1398
+ children: [
1399
+ /* @__PURE__ */ jsx(Icon, { className: "lj-se-ic-4", strokeWidth: 1.5 }),
1400
+ /* @__PURE__ */ jsx("span", { children: t.label })
1401
+ ]
1402
+ },
1403
+ t.value
1404
+ );
1405
+ }) })
1406
+ ] }) : null,
1407
+ sections.map((items, si) => /* @__PURE__ */ jsxs(Fragment$1, { children: [
1408
+ si > 0 || typeSeg ? /* @__PURE__ */ jsx("div", { className: "lj-se-rowmenu-div" }) : null,
1409
+ items.map((item) => {
1410
+ const Icon = item.icon;
1411
+ return /* @__PURE__ */ jsxs(
1412
+ "button",
1413
+ {
1414
+ type: "button",
1415
+ className: cn("lj-se-rowmenu-item", item.danger && "lj-se-rowmenu-del"),
1416
+ onClick: () => {
1417
+ close();
1418
+ item.onSelect();
1419
+ },
1420
+ children: [
1421
+ /* @__PURE__ */ jsx(Icon, { className: cn("lj-se-ic-4 lj-se-rowmenu-glyph", item.glyphClassName), strokeWidth: 1.5 }),
1422
+ item.label
1423
+ ]
1424
+ },
1425
+ item.label
1426
+ );
1427
+ })
1428
+ ] }, si))
1429
+ ] });
1430
+ }
1431
+ function RowControlHandle({
1432
+ currentType,
1433
+ canSplit,
1434
+ disabled,
1435
+ dragHandleProps,
1436
+ onChangeType,
1437
+ onInsertAbove,
1438
+ onInsertBelow,
1439
+ onSplitHere,
1440
+ onDelete
1441
+ }) {
1442
+ if (disabled) return null;
1443
+ const insertSection = [
1444
+ { icon: ArrowUp, label: "\u5728\u4E0A\u65B9\u63D2\u5165", onSelect: onInsertAbove },
1445
+ { icon: ArrowDown, label: "\u5728\u4E0B\u65B9\u63D2\u5165", onSelect: onInsertBelow }
1446
+ ];
1447
+ if (canSplit && onSplitHere) {
1448
+ insertSection.push({
1449
+ icon: Scissors,
1450
+ label: "\u4ECE\u6B64\u53E6\u8D77\u4E00\u573A",
1451
+ onSelect: onSplitHere,
1452
+ glyphClassName: "lj-se-scissors-rot"
1453
+ });
1454
+ }
1455
+ return /* @__PURE__ */ jsx(
1456
+ Popover,
1457
+ {
1458
+ align: "start",
1459
+ side: "bottom",
1460
+ className: "lj-se-handle-pop",
1461
+ panelClassName: "lj-se-rowmenu",
1462
+ trigger: ({ open, toggle }) => /* @__PURE__ */ jsx(
1463
+ "button",
1464
+ {
1465
+ type: "button",
1466
+ ...dragHandleProps,
1467
+ className: cn("lj-se-handle", open && "is-open", dragHandleProps?.draggable && "is-draggable"),
1468
+ title: "\u62D6\u52A8\u6392\u5E8F \xB7 \u70B9\u51FB\u64CD\u4F5C",
1469
+ "aria-label": "\u884C\u64CD\u4F5C",
1470
+ onClick: toggle,
1471
+ children: /* @__PURE__ */ jsx(GripVertical, { className: "lj-se-handle-grip", strokeWidth: 1.5 })
1472
+ }
1473
+ ),
1474
+ children: ({ close }) => /* @__PURE__ */ jsx(
1475
+ RowMenuPanel,
1476
+ {
1477
+ close,
1478
+ typeSeg: { current: currentType, onChange: onChangeType },
1479
+ sections: [insertSection, [{ icon: Trash2, label: "\u5220\u9664\u8FD9\u884C", onSelect: onDelete, danger: true }]]
1480
+ }
1481
+ )
1482
+ }
1483
+ );
1484
+ }
1485
+ var DragReorderContext = createContext(null);
1486
+ function useDragReorder() {
1487
+ return useContext(DragReorderContext);
1488
+ }
1489
+
1490
+ // src/mutations/item-mutations.ts
1491
+ function withSceneUpdated2(data, episodeIndex, sceneIndex, updater) {
1492
+ const episodes = [...data.episodes || []];
1493
+ const episode = episodes[episodeIndex];
1494
+ if (!episode) return data;
1495
+ const scenes = [...episode.scenes || []];
1496
+ const scene = scenes[sceneIndex];
1497
+ if (!scene) return data;
1498
+ scenes[sceneIndex] = updater(scene);
1499
+ episodes[episodeIndex] = { ...episode, scenes };
1500
+ return { ...data, episodes };
1501
+ }
1502
+ function updateSceneItemType(data, episodeIndex, sceneIndex, itemIndex, type) {
1503
+ return withSceneUpdated2(data, episodeIndex, sceneIndex, (scene) => {
1504
+ const actions = [...scene.actions || []];
1505
+ const current = actions[itemIndex];
1506
+ if (!current) return scene;
1507
+ const next = { ...current, type };
1508
+ if (!isActorBearingContentType(type)) {
1509
+ delete next.actor_id;
1510
+ delete next.emotion;
1511
+ } else {
1512
+ next.actor_id = next.actor_id?.trim() || pickInheritedActorId(scene, itemIndex);
1513
+ if (type !== "dialogue" /* Dialogue */) {
1514
+ delete next.emotion;
1515
+ }
1516
+ }
1517
+ actions[itemIndex] = next;
1518
+ return { ...scene, actions };
1519
+ });
1520
+ }
1521
+ function buildNewScriptItem(type, inheritedActorId) {
1522
+ if (isActorBearingContentType(type)) {
1523
+ return {
1524
+ type,
1525
+ actor_id: inheritedActorId?.trim() || "",
1526
+ content: ""
1527
+ };
1528
+ }
1529
+ return { type, content: "" };
1530
+ }
1531
+ function pickInheritedActorId(scene, beforeIndex) {
1532
+ const actions = scene.actions || [];
1533
+ for (let i = beforeIndex - 1; i >= 0; i -= 1) {
1534
+ const item = actions[i];
1535
+ if (item && isActorBearingContentType(item.type) && item.actor_id?.trim()) {
1536
+ return item.actor_id.trim();
1537
+ }
1538
+ }
1539
+ return "";
1540
+ }
1541
+ function SelectField({
1542
+ value,
1543
+ options,
1544
+ onChange,
1545
+ disabled,
1546
+ title,
1547
+ placeholder,
1548
+ mono,
1549
+ className,
1550
+ style,
1551
+ emptyOptionLabel
1552
+ }) {
1553
+ return /* @__PURE__ */ jsxs(
1554
+ "select",
1555
+ {
1556
+ disabled,
1557
+ value,
1558
+ title,
1559
+ onChange: (e) => onChange(e.target.value),
1560
+ style,
1561
+ className: cn("lj-se-select", mono && "lj-se-mono", className),
1562
+ children: [
1563
+ !value || !options.some((o) => o.value === value) ? /* @__PURE__ */ jsx("option", { value, children: placeholder ?? emptyOptionLabel ?? "\u2014" }) : null,
1564
+ options.map((o) => /* @__PURE__ */ jsx("option", { value: o.value, children: o.label }, o.value))
1565
+ ]
1566
+ }
1567
+ );
1568
+ }
1569
+ function stateOptions(states) {
1570
+ return (states ?? []).map((s) => ({ value: (s.state_id ?? "").trim(), label: getStateDisplayName(s) })).filter((o) => o.value);
1571
+ }
1572
+ function AssetStateChip({
1573
+ name,
1574
+ orphan,
1575
+ states,
1576
+ currentStateId,
1577
+ disabled,
1578
+ onChangeState,
1579
+ onDelete,
1580
+ onNavigate
1581
+ }) {
1582
+ const opts = stateOptions(states);
1583
+ const trimmed = (currentStateId ?? "").trim();
1584
+ return /* @__PURE__ */ jsxs("span", { className: cn("lj-se-chip", orphan && "is-orphan"), title: orphan ? `${name} \u4E0D\u5B58\u5728` : void 0, children: [
1585
+ onNavigate ? /* @__PURE__ */ jsx("button", { type: "button", className: "lj-se-chip-name lj-se-chip-name--link", title: `\u8DF3\u5230 ${name}`, onClick: onNavigate, children: name }) : /* @__PURE__ */ jsx("span", { className: "lj-se-chip-name", children: name }),
1586
+ /* @__PURE__ */ jsx("span", { className: "lj-se-chip-sep", children: "\xB7" }),
1587
+ /* @__PURE__ */ jsx(
1588
+ SelectField,
1589
+ {
1590
+ value: trimmed,
1591
+ options: opts,
1592
+ placeholder: "\u9ED8\u8BA4",
1593
+ disabled,
1594
+ className: "lj-se-chip-state",
1595
+ onChange: (v) => onChangeState(v || null)
1596
+ }
1597
+ ),
1598
+ !disabled ? /* @__PURE__ */ jsx(
1599
+ "button",
1600
+ {
1601
+ type: "button",
1602
+ className: "lj-se-chip-del",
1603
+ title: `\u4ECE\u672C\u573A\u79FB\u9664 ${name}`,
1604
+ "aria-label": `\u4ECE\u672C\u573A\u79FB\u9664 ${name}`,
1605
+ onClick: (e) => {
1606
+ e.stopPropagation();
1607
+ onDelete();
1608
+ },
1609
+ children: /* @__PURE__ */ jsx(Eraser, { className: "lj-se-ic-3", strokeWidth: 1.5 })
1610
+ }
1611
+ ) : null
1612
+ ] });
1613
+ }
1614
+ function AddRefControl({
1615
+ options,
1616
+ existingIds,
1617
+ disabled,
1618
+ emptyHint,
1619
+ onAdd
1620
+ }) {
1621
+ if (disabled) return null;
1622
+ const available = options.filter((o) => !existingIds.includes(o.value));
1623
+ return /* @__PURE__ */ jsx(
1624
+ Popover,
1625
+ {
1626
+ trigger: ({ toggle }) => /* @__PURE__ */ jsx("button", { type: "button", className: "lj-se-chip-add", title: "\u6DFB\u52A0\u5F15\u7528", onClick: toggle, children: /* @__PURE__ */ jsx(Plus, { className: "lj-se-icon-sm", strokeWidth: 1.5 }) }),
1627
+ children: ({ close }) => /* @__PURE__ */ jsx("div", { className: "lj-se-addmenu", children: available.length === 0 ? /* @__PURE__ */ jsx("div", { className: "lj-se-addmenu-empty", children: emptyHint }) : available.map((o) => /* @__PURE__ */ jsx(
1628
+ "button",
1629
+ {
1630
+ type: "button",
1631
+ className: "lj-se-addmenu-item",
1632
+ onClick: () => {
1633
+ close();
1634
+ onAdd(o.value);
1635
+ },
1636
+ children: o.label
1637
+ },
1638
+ o.value
1639
+ )) })
1640
+ }
1641
+ );
1642
+ }
1643
+ function SceneCastPropRows({
1644
+ scene,
1645
+ episodeIndex,
1646
+ sceneIndex,
1647
+ actorMap,
1648
+ propMap,
1649
+ disabled,
1650
+ onSaveData,
1651
+ onRequestAssetDelete,
1652
+ onNavigateAsset
1653
+ }) {
1654
+ const context = getSceneContext(scene);
1655
+ const sceneActors = context.actors ?? [];
1656
+ const sceneProps = context.props ?? [];
1657
+ const currentActorIds = sceneActors.map((i) => (i.actor_id ?? "").trim()).filter(Boolean);
1658
+ const currentPropIds = sceneProps.map((i) => (i.prop_id ?? "").trim()).filter(Boolean);
1659
+ const actorOptions = Array.from(actorMap.values()).map((a) => ({ value: (a.actor_id ?? "").trim(), label: getActorDisplayName(a) })).filter((o) => o.value);
1660
+ const propOptions = Array.from(propMap.values()).map((p) => ({ value: (p.prop_id ?? "").trim(), label: getPropDisplayName(p) })).filter((o) => o.value);
1661
+ const writeRefs = (field, nextIds) => onSaveData((cur) => updateSceneField(cur, episodeIndex, sceneIndex, field, nextIds.join(",")));
1662
+ return /* @__PURE__ */ jsxs("div", { className: "lj-se-refs", children: [
1663
+ /* @__PURE__ */ jsxs("div", { className: "lj-se-ref-row", children: [
1664
+ /* @__PURE__ */ jsx("span", { className: "lj-se-ref-label", children: "CAST" }),
1665
+ /* @__PURE__ */ jsxs("div", { className: "lj-se-ref-chips", children: [
1666
+ sceneActors.length === 0 ? /* @__PURE__ */ jsx("span", { className: "lj-se-ref-empty", children: "\u2014" }) : null,
1667
+ sceneActors.map((ref, idx) => {
1668
+ const id = (ref.actor_id ?? "").trim();
1669
+ if (!id) return null;
1670
+ const actor = actorMap.get(id);
1671
+ const name = actor ? getActorDisplayName(actor) : id;
1672
+ return /* @__PURE__ */ jsx(
1673
+ AssetStateChip,
1674
+ {
1675
+ name,
1676
+ orphan: !actor,
1677
+ states: actor?.states,
1678
+ currentStateId: ref.state_id,
1679
+ disabled,
1680
+ onNavigate: onNavigateAsset ? () => onNavigateAsset({ kind: "actor", actorId: id, origin: "reference" }) : void 0,
1681
+ onChangeState: (next) => onSaveData(
1682
+ (cur) => updateSceneActorState(cur, episodeIndex, sceneIndex, id, next)
1683
+ ),
1684
+ onDelete: () => {
1685
+ if (onRequestAssetDelete) {
1686
+ onRequestAssetDelete({
1687
+ kind: "actor",
1688
+ id,
1689
+ name,
1690
+ description: actor?.description,
1691
+ episodeIndex,
1692
+ sceneIndex
1693
+ });
1694
+ return;
1695
+ }
1696
+ writeRefs("actors", currentActorIds.filter((x) => x !== id));
1697
+ }
1698
+ },
1699
+ `${id}-${idx}`
1700
+ );
1701
+ }),
1702
+ /* @__PURE__ */ jsx(
1703
+ AddRefControl,
1704
+ {
1705
+ options: actorOptions,
1706
+ existingIds: currentActorIds,
1707
+ disabled,
1708
+ emptyHint: "\u6CA1\u6709\u53EF\u6DFB\u52A0\u7684\u89D2\u8272",
1709
+ onAdd: (id) => currentActorIds.includes(id) ? void 0 : writeRefs("actors", [...currentActorIds, id])
1710
+ }
1711
+ )
1712
+ ] })
1713
+ ] }),
1714
+ /* @__PURE__ */ jsxs("div", { className: "lj-se-ref-row", children: [
1715
+ /* @__PURE__ */ jsx("span", { className: "lj-se-ref-label", children: "PROP" }),
1716
+ /* @__PURE__ */ jsxs("div", { className: "lj-se-ref-chips", children: [
1717
+ sceneProps.length === 0 ? /* @__PURE__ */ jsx("span", { className: "lj-se-ref-empty", children: "\u2014" }) : null,
1718
+ sceneProps.map((ref, idx) => {
1719
+ const id = (ref.prop_id ?? "").trim();
1720
+ if (!id) return null;
1721
+ const prop = propMap.get(id);
1722
+ const name = prop ? getPropDisplayName(prop) : id;
1723
+ return /* @__PURE__ */ jsx(
1724
+ AssetStateChip,
1725
+ {
1726
+ name,
1727
+ orphan: !prop,
1728
+ states: prop?.states,
1729
+ currentStateId: ref.state_id,
1730
+ disabled,
1731
+ onNavigate: onNavigateAsset ? () => onNavigateAsset({ kind: "prop", propId: id, origin: "reference" }) : void 0,
1732
+ onChangeState: (next) => onSaveData(
1733
+ (cur) => updateScenePropState(cur, episodeIndex, sceneIndex, id, next)
1734
+ ),
1735
+ onDelete: () => {
1736
+ if (onRequestAssetDelete) {
1737
+ onRequestAssetDelete({
1738
+ kind: "prop",
1739
+ id,
1740
+ name,
1741
+ description: prop?.description,
1742
+ episodeIndex,
1743
+ sceneIndex
1744
+ });
1745
+ return;
1746
+ }
1747
+ writeRefs("props", currentPropIds.filter((x) => x !== id));
1748
+ }
1749
+ },
1750
+ `${id}-${idx}`
1751
+ );
1752
+ }),
1753
+ /* @__PURE__ */ jsx(
1754
+ AddRefControl,
1755
+ {
1756
+ options: propOptions,
1757
+ existingIds: currentPropIds,
1758
+ disabled,
1759
+ emptyHint: "\u6CA1\u6709\u53EF\u6DFB\u52A0\u7684\u9053\u5177",
1760
+ onAdd: (id) => currentPropIds.includes(id) ? void 0 : writeRefs("props", [...currentPropIds, id])
1761
+ }
1762
+ )
1763
+ ] })
1764
+ ] })
1765
+ ] });
1766
+ }
1767
+ function SceneNumberMenu({
1768
+ label,
1769
+ triggerClassName,
1770
+ canMerge,
1771
+ disabled,
1772
+ onMerge,
1773
+ onDelete
1774
+ }) {
1775
+ if (disabled) {
1776
+ return /* @__PURE__ */ jsx("span", { className: triggerClassName, children: label });
1777
+ }
1778
+ const items = [];
1779
+ if (canMerge) items.push({ icon: ArrowUpToLine, label: "\u5E76\u5165\u4E0A\u4E00\u573A", onSelect: onMerge });
1780
+ items.push({ icon: Trash2, label: "\u5220\u9664\u6574\u573A", onSelect: onDelete, danger: true });
1781
+ return /* @__PURE__ */ jsx(
1782
+ Popover,
1783
+ {
1784
+ align: "start",
1785
+ side: "bottom",
1786
+ className: "lj-se-scenenum-pop",
1787
+ panelClassName: "lj-se-rowmenu",
1788
+ trigger: ({ open, toggle }) => /* @__PURE__ */ jsx(
1789
+ "button",
1790
+ {
1791
+ type: "button",
1792
+ className: cn(triggerClassName, "lj-se-scenenum-btn", open && "is-open"),
1793
+ title: "\u573A\u64CD\u4F5C",
1794
+ "aria-label": "\u573A\u64CD\u4F5C",
1795
+ onClick: toggle,
1796
+ children: label
1797
+ }
1798
+ ),
1799
+ children: ({ close }) => /* @__PURE__ */ jsx(RowMenuPanel, { close, sections: [items] })
1800
+ }
1801
+ );
1802
+ }
1803
+ var SPACE_OPTIONS = [
1804
+ { value: "interior", label: "INT." },
1805
+ { value: "exterior", label: "EXT." }
1806
+ ];
1807
+ var TIME_OPTIONS = [
1808
+ { value: "day", label: "DAY" },
1809
+ { value: "night", label: "NIGHT" },
1810
+ { value: "dawn", label: "DAWN" },
1811
+ { value: "dusk", label: "DUSK" }
1812
+ ];
1813
+ var ASIAN_SPACE_OPTIONS = [
1814
+ { value: "interior", label: "\u5185" },
1815
+ { value: "exterior", label: "\u5916" }
1816
+ ];
1817
+ var ASIAN_TIME_OPTIONS = [
1818
+ { value: "day", label: "\u65E5" },
1819
+ { value: "night", label: "\u591C" },
1820
+ { value: "dawn", label: "\u6668" },
1821
+ { value: "dusk", label: "\u66AE" }
1822
+ ];
1823
+ function SceneHeader({
1824
+ scene,
1825
+ episodeIndex,
1826
+ sceneIndex,
1827
+ actorMap,
1828
+ locationMap,
1829
+ propMap,
1830
+ disabled,
1831
+ focused = false,
1832
+ format = "standard",
1833
+ onSaveData,
1834
+ onRequestAssetDelete,
1835
+ onNavigateAsset
1836
+ }) {
1837
+ const primaryLocationId = getPrimarySceneLocationId(scene);
1838
+ const location = primaryLocationId ? locationMap.get(primaryLocationId) : void 0;
1839
+ const hasOrphanLocation = !!primaryLocationId && !location;
1840
+ const locationStateId = getSceneContext(scene).locations.find(
1841
+ (i) => (i.location_id ?? "").trim() === primaryLocationId
1842
+ )?.state_id;
1843
+ const locationOptions = useMemo(
1844
+ () => Array.from(locationMap.values()).map((loc) => ({ value: (loc.location_id ?? "").trim(), label: getLocationDisplayName(loc) })).filter((o) => o.value),
1845
+ [locationMap]
1846
+ );
1847
+ const locationStateOptions = useMemo(
1848
+ () => (location?.states ?? []).map((s) => ({ value: (s.state_id ?? "").trim(), label: getStateDisplayName(s) })).filter((o) => o.value),
1849
+ [location]
1850
+ );
1851
+ if (format === "asian") {
1852
+ return /* @__PURE__ */ jsxs("div", { className: "lj-se-asian-head group", children: [
1853
+ /* @__PURE__ */ jsx(
1854
+ SceneNumberMenu,
1855
+ {
1856
+ label: `${getSceneNumber(scene, sceneIndex)}.`,
1857
+ triggerClassName: "lj-se-asian-num",
1858
+ canMerge: sceneIndex > 0,
1859
+ disabled,
1860
+ onMerge: () => onSaveData((cur) => mergeSceneIntoPrev(cur, episodeIndex, sceneIndex)),
1861
+ onDelete: () => onSaveData((cur) => deleteScene(cur, episodeIndex, sceneIndex))
1862
+ }
1863
+ ),
1864
+ /* @__PURE__ */ jsx(
1865
+ SelectField,
1866
+ {
1867
+ value: primaryLocationId,
1868
+ options: locationOptions,
1869
+ disabled,
1870
+ placeholder: "\u5730\u70B9",
1871
+ className: cn("lj-se-asian-loc", hasOrphanLocation && "is-orphan"),
1872
+ onChange: (v) => onSaveData((cur) => updateSceneField(cur, episodeIndex, sceneIndex, "location", v))
1873
+ }
1874
+ ),
1875
+ /* @__PURE__ */ jsxs("span", { className: "lj-se-asian-slug", children: [
1876
+ /* @__PURE__ */ jsx(
1877
+ SelectField,
1878
+ {
1879
+ mono: true,
1880
+ value: scene.environment?.time || "day",
1881
+ options: ASIAN_TIME_OPTIONS,
1882
+ disabled,
1883
+ onChange: (v) => onSaveData((cur) => updateSceneField(cur, episodeIndex, sceneIndex, "time", v))
1884
+ }
1885
+ ),
1886
+ /* @__PURE__ */ jsx("span", { className: "lj-se-asian-slash", children: "/" }),
1887
+ /* @__PURE__ */ jsx(
1888
+ SelectField,
1889
+ {
1890
+ mono: true,
1891
+ value: scene.environment?.space || "interior",
1892
+ options: ASIAN_SPACE_OPTIONS,
1893
+ disabled,
1894
+ onChange: (v) => onSaveData((cur) => updateSceneField(cur, episodeIndex, sceneIndex, "space", v))
1895
+ }
1896
+ )
1897
+ ] })
1898
+ ] });
1899
+ }
1900
+ return /* @__PURE__ */ jsxs("div", { className: "lj-se-scene-head group", children: [
1901
+ /* @__PURE__ */ jsxs("div", { className: "lj-se-scene-headrow", children: [
1902
+ /* @__PURE__ */ jsx(
1903
+ SceneNumberMenu,
1904
+ {
1905
+ label: String(getSceneNumber(scene, sceneIndex)),
1906
+ triggerClassName: cn("lj-se-scene-num", focused && "is-focused"),
1907
+ canMerge: sceneIndex > 0,
1908
+ disabled,
1909
+ onMerge: () => onSaveData((cur) => mergeSceneIntoPrev(cur, episodeIndex, sceneIndex)),
1910
+ onDelete: () => onSaveData((cur) => deleteScene(cur, episodeIndex, sceneIndex))
1911
+ }
1912
+ ),
1913
+ /* @__PURE__ */ jsxs("div", { className: "lj-se-scene-slug", children: [
1914
+ /* @__PURE__ */ jsx(
1915
+ SelectField,
1916
+ {
1917
+ mono: true,
1918
+ value: scene.environment?.space || "interior",
1919
+ options: SPACE_OPTIONS,
1920
+ disabled,
1921
+ onChange: (v) => onSaveData((cur) => updateSceneField(cur, episodeIndex, sceneIndex, "space", v))
1922
+ }
1923
+ ),
1924
+ /* @__PURE__ */ jsx(
1925
+ SelectField,
1926
+ {
1927
+ value: primaryLocationId,
1928
+ options: locationOptions,
1929
+ disabled,
1930
+ placeholder: "\u9009\u62E9\u5730\u70B9",
1931
+ title: hasOrphanLocation ? `\u5730\u70B9 ${primaryLocationId} \u4E0D\u5B58\u5728` : void 0,
1932
+ className: cn("lj-se-slug-loc", hasOrphanLocation && "is-orphan"),
1933
+ onChange: (v) => onSaveData((cur) => updateSceneField(cur, episodeIndex, sceneIndex, "location", v))
1934
+ }
1935
+ ),
1936
+ /* @__PURE__ */ jsx("span", { className: "lj-se-slug-dash", children: "\u2014" }),
1937
+ /* @__PURE__ */ jsx(
1938
+ SelectField,
1939
+ {
1940
+ mono: true,
1941
+ value: scene.environment?.time || "day",
1942
+ options: TIME_OPTIONS,
1943
+ disabled,
1944
+ onChange: (v) => onSaveData((cur) => updateSceneField(cur, episodeIndex, sceneIndex, "time", v))
1945
+ }
1946
+ ),
1947
+ locationStateOptions.length > 0 ? /* @__PURE__ */ jsx(
1948
+ SelectField,
1949
+ {
1950
+ value: (locationStateId ?? "").trim(),
1951
+ options: locationStateOptions,
1952
+ placeholder: "\u65E0\u72B6\u6001",
1953
+ disabled,
1954
+ className: "lj-se-slug-state",
1955
+ onChange: (v) => onSaveData(
1956
+ (cur) => updateSceneLocationState(cur, episodeIndex, sceneIndex, primaryLocationId, v || null)
1957
+ )
1958
+ }
1959
+ ) : null
1960
+ ] })
1961
+ ] }),
1962
+ /* @__PURE__ */ jsx(
1963
+ SceneCastPropRows,
1964
+ {
1965
+ scene,
1966
+ episodeIndex,
1967
+ sceneIndex,
1968
+ actorMap,
1969
+ propMap,
1970
+ disabled,
1971
+ onSaveData,
1972
+ onRequestAssetDelete,
1973
+ onNavigateAsset
1974
+ }
1975
+ )
1976
+ ] });
1977
+ }
1978
+
1979
+ // src/actor-color.ts
1980
+ var PALETTE_SIZE = 8;
1981
+ function hashActorId(id) {
1982
+ let h = 5381;
1983
+ for (let i = 0; i < id.length; i++) {
1984
+ h = h * 33 ^ id.charCodeAt(i);
1985
+ }
1986
+ return Math.abs(h | 0);
1987
+ }
1988
+ function getActorCueVar(actorId, tone = "dialogue") {
1989
+ const id = actorId?.trim();
1990
+ if (!id) {
1991
+ return tone === "inner" ? "var(--lj-se-cue-inner)" : "var(--lj-se-cue-dialogue)";
1992
+ }
1993
+ const idx = hashActorId(id) % PALETTE_SIZE + 1;
1994
+ return `var(--lj-se-actor-cue-${idx})`;
1995
+ }
1996
+ var EditingSignalContext = createContext(null);
1997
+ function caretOffsetFromPoint(x, y) {
1998
+ const d = document;
1999
+ if (typeof d.caretRangeFromPoint === "function") {
2000
+ const r = d.caretRangeFromPoint(x, y);
2001
+ return r ? r.startOffset : null;
2002
+ }
2003
+ if (typeof d.caretPositionFromPoint === "function") {
2004
+ const p = d.caretPositionFromPoint(x, y);
2005
+ return p ? p.offset : null;
2006
+ }
2007
+ return null;
2008
+ }
2009
+ function EditableText({
2010
+ value,
2011
+ onSave,
2012
+ placeholder,
2013
+ multiline = false,
2014
+ disabled = false,
2015
+ className,
2016
+ autoEdit = false,
2017
+ onEditEnd,
2018
+ spanProps
2019
+ }) {
2020
+ const [editing, setEditing] = useState(autoEdit);
2021
+ const [draft, setDraft] = useState(value);
2022
+ const caretRef = useRef(null);
2023
+ const inputRef = useRef(null);
2024
+ const committedRef = useRef(value);
2025
+ const editingSignal = useContext(EditingSignalContext);
2026
+ useEffect(() => {
2027
+ if (!editing || !editingSignal) return;
2028
+ editingSignal.inc();
2029
+ return () => editingSignal.dec();
2030
+ }, [editing, editingSignal]);
2031
+ useEffect(() => {
2032
+ if (value === committedRef.current) return;
2033
+ const userDiverged = editing && draft !== committedRef.current;
2034
+ if (userDiverged) return;
2035
+ setDraft(value);
2036
+ committedRef.current = value;
2037
+ }, [value, editing, draft]);
2038
+ useEffect(() => {
2039
+ if (autoEdit) setEditing(true);
2040
+ }, [autoEdit]);
2041
+ useLayoutEffect(() => {
2042
+ if (!editing) return;
2043
+ const el = inputRef.current;
2044
+ if (!el) return;
2045
+ if (multiline) autoSize(el);
2046
+ el.focus();
2047
+ const caret = caretRef.current;
2048
+ const pos = caret == null ? el.value.length : Math.min(caret, el.value.length);
2049
+ try {
2050
+ el.setSelectionRange(pos, pos);
2051
+ } catch {
2052
+ }
2053
+ caretRef.current = null;
2054
+ }, [editing, multiline]);
2055
+ function beginEdit(e) {
2056
+ if (disabled) return;
2057
+ const sel = window.getSelection();
2058
+ if (sel && !sel.isCollapsed) return;
2059
+ caretRef.current = caretOffsetFromPoint(e.clientX, e.clientY);
2060
+ setDraft(committedRef.current);
2061
+ setEditing(true);
2062
+ }
2063
+ function commit() {
2064
+ setEditing(false);
2065
+ onEditEnd?.();
2066
+ if (draft !== committedRef.current) {
2067
+ committedRef.current = draft;
2068
+ onSave(draft);
2069
+ }
2070
+ }
2071
+ function cancel() {
2072
+ setDraft(committedRef.current);
2073
+ setEditing(false);
2074
+ onEditEnd?.();
2075
+ }
2076
+ if (!editing) {
2077
+ return /* @__PURE__ */ jsx(
2078
+ "span",
2079
+ {
2080
+ ...spanProps,
2081
+ className: "lj-se-editable" + (className ? " " + className : "") + (disabled ? " lj-se-editable--ro" : ""),
2082
+ onClick: beginEdit,
2083
+ children: value || /* @__PURE__ */ jsx("span", { className: "lj-se-ph", children: placeholder ?? "" })
2084
+ }
2085
+ );
2086
+ }
2087
+ const common = {
2088
+ ref: inputRef,
2089
+ value: draft,
2090
+ className: "lj-se-editing" + (className ? " " + className : ""),
2091
+ onChange: (e) => {
2092
+ setDraft(e.target.value);
2093
+ if (multiline) autoSize(e.target);
2094
+ },
2095
+ onBlur: commit,
2096
+ onKeyDown: (e) => {
2097
+ if (e.key === "Escape") {
2098
+ e.preventDefault();
2099
+ cancel();
2100
+ } else if (e.key === "Enter") {
2101
+ if (!multiline || e.metaKey || e.ctrlKey) {
2102
+ e.preventDefault();
2103
+ commit();
2104
+ }
2105
+ }
2106
+ }
2107
+ };
2108
+ return multiline ? /* @__PURE__ */ jsx("textarea", { ...common, rows: 1 }) : /* @__PURE__ */ jsx("input", { ...common, type: "text" });
2109
+ }
2110
+ function autoSize(el) {
2111
+ el.style.height = "auto";
2112
+ el.style.height = el.scrollHeight + "px";
2113
+ }
2114
+ function formatStateChangeLabels(item, actorMap, locationMap, propMap) {
2115
+ const labels = (item.state_changes ?? []).map((change) => {
2116
+ const kind = change.target_kind?.trim() || "actor";
2117
+ const id = change.target_id?.trim() || "";
2118
+ if (!id) return "";
2119
+ const states = kind === "location" ? locationMap.get(id)?.states : kind === "prop" ? propMap.get(id)?.states : actorMap.get(id)?.states;
2120
+ const name = kind === "location" ? getLocationDisplayName(locationMap.get(id)) || id : kind === "prop" ? getPropDisplayName(propMap.get(id)) || id : getActorDisplayName(actorMap.get(id)) || id;
2121
+ const from = getStateDisplayName(findState(states, change.from_state_id)) || change.from_state_id?.trim() || "\u672A\u8BBE";
2122
+ const to = getStateDisplayName(findState(states, change.to_state_id)) || change.to_state_id?.trim() || "\u672A\u8BBE";
2123
+ return `${name}\uFF1A${from} \u2192 ${to}`;
2124
+ }).filter(Boolean);
2125
+ if (item.transition_prompt?.process || item.transition_prompt?.contrast) {
2126
+ labels.push("\u753B\u9762\u53D8\u5316\u5DF2\u8BBE\u7F6E");
2127
+ }
2128
+ return labels;
2129
+ }
2130
+ function StateBadges({ labels }) {
2131
+ if (labels.length === 0) return null;
2132
+ return /* @__PURE__ */ jsx("div", { className: "lj-se-state-badges", children: labels.map((label, i) => /* @__PURE__ */ jsx("span", { className: "lj-se-state-badge", children: label }, `${label}-${i}`)) });
2133
+ }
2134
+ function SceneItemRowImpl({
2135
+ item,
2136
+ actorMap,
2137
+ speakerMap = /* @__PURE__ */ new Map(),
2138
+ locationMap = /* @__PURE__ */ new Map(),
2139
+ propMap = /* @__PURE__ */ new Map(),
2140
+ actorOptions,
2141
+ format,
2142
+ disabled,
2143
+ autoEdit,
2144
+ onEditEnd,
2145
+ canSplit,
2146
+ dragHandleProps,
2147
+ onSave,
2148
+ onSaveActor,
2149
+ onSaveEmotion,
2150
+ onChangeType,
2151
+ onInsertAbove,
2152
+ onInsertBelow,
2153
+ onSplitHere,
2154
+ onDelete
2155
+ }) {
2156
+ const type = item.type ?? "dialogue" /* Dialogue */;
2157
+ const content = item.content ?? "";
2158
+ const actorId = item.actor_id?.trim() || "";
2159
+ const actor = actorMap.get(actorId);
2160
+ const actorName = actor ? getActorDisplayName(actor) : actorId || "\u672A\u77E5\u89D2\u8272";
2161
+ const hasOrphanActor = !!actorId && !actor;
2162
+ const emotion = translateEmotionLabel(item.emotion);
2163
+ const stateLabels = formatStateChangeLabels(item, actorMap, locationMap, propMap);
2164
+ const structured = type === "dialogue" /* Dialogue */ && (!!item.speaker_id || !!item.delivery || (item.speakers?.length ?? 0) > 0 || (item.lines?.length ?? 0) > 0);
2165
+ const handle = /* @__PURE__ */ jsx(
2166
+ RowControlHandle,
2167
+ {
2168
+ currentType: type,
2169
+ canSplit,
2170
+ disabled,
2171
+ dragHandleProps,
2172
+ onChangeType,
2173
+ onInsertAbove,
2174
+ onInsertBelow,
2175
+ onSplitHere,
2176
+ onDelete
2177
+ }
2178
+ );
2179
+ const actorCue = (tone) => structured ? /* @__PURE__ */ jsxs(
2180
+ "span",
2181
+ {
2182
+ className: "lj-se-cue lj-se-cue--structured",
2183
+ style: !hasOrphanActor ? { color: getActorCueVar(actorId, tone) } : void 0,
2184
+ children: [
2185
+ getDialogueSpeakerLabel(item, actorMap, speakerMap) || "\u672A\u77E5\u53D1\u58F0\u6E90",
2186
+ getDialogueDeliveryLabel(item.delivery) ? /* @__PURE__ */ jsxs("span", { className: "lj-se-cue-delivery", children: [
2187
+ "\uFF08",
2188
+ getDialogueDeliveryLabel(item.delivery),
2189
+ "\uFF09"
2190
+ ] }) : null
2191
+ ]
2192
+ }
2193
+ ) : /* @__PURE__ */ jsx(
2194
+ SelectField,
2195
+ {
2196
+ value: actorId,
2197
+ options: actorOptions,
2198
+ disabled,
2199
+ placeholder: "\u672A\u77E5\u89D2\u8272",
2200
+ title: hasOrphanActor ? `\u89D2\u8272 ${actorName} \u4E0D\u5B58\u5728` : "\u5207\u6362\u89D2\u8272",
2201
+ className: cn("lj-se-cue lj-se-cue--select", hasOrphanActor && "is-orphan"),
2202
+ style: !hasOrphanActor ? { color: getActorCueVar(actorId, tone) } : void 0,
2203
+ onChange: onSaveActor
2204
+ }
2205
+ );
2206
+ if (format === "standard") {
2207
+ let body2;
2208
+ if (type === "dialogue" /* Dialogue */) {
2209
+ body2 = /* @__PURE__ */ jsxs("div", { className: "lj-se-std-dialogue", children: [
2210
+ actorCue("dialogue"),
2211
+ !structured ? /* @__PURE__ */ jsx("div", { className: "lj-se-std-emotion", children: /* @__PURE__ */ jsx(EditableText, { value: item.emotion ?? "", disabled, placeholder: "\u60C5\u7EEA\u2026", onSave: onSaveEmotion }) }) : emotion ? /* @__PURE__ */ jsx("div", { className: "lj-se-std-emotion", children: emotion }) : null,
2212
+ /* @__PURE__ */ jsx(
2213
+ EditableText,
2214
+ {
2215
+ multiline: true,
2216
+ value: content,
2217
+ disabled,
2218
+ autoEdit,
2219
+ onEditEnd,
2220
+ placeholder: "\u8F93\u5165\u5BF9\u767D\u2026",
2221
+ className: "lj-se-std-line",
2222
+ onSave
2223
+ }
2224
+ )
2225
+ ] });
2226
+ } else if (type === "inner_thought" /* InnerThought */) {
2227
+ body2 = /* @__PURE__ */ jsxs("div", { className: "lj-se-std-dialogue", children: [
2228
+ actorCue("inner"),
2229
+ /* @__PURE__ */ jsx(
2230
+ EditableText,
2231
+ {
2232
+ multiline: true,
2233
+ value: content,
2234
+ disabled,
2235
+ autoEdit,
2236
+ onEditEnd,
2237
+ placeholder: "\u8F93\u5165\u5FC3\u7406\u6D3B\u52A8\u2026",
2238
+ className: "lj-se-std-inner",
2239
+ onSave
2240
+ }
2241
+ )
2242
+ ] });
2243
+ } else {
2244
+ body2 = /* @__PURE__ */ jsx(
2245
+ EditableText,
2246
+ {
2247
+ multiline: true,
2248
+ value: content,
2249
+ disabled,
2250
+ autoEdit,
2251
+ onEditEnd,
2252
+ placeholder: type === "narration" /* Narration */ ? "\u8F93\u5165\u65C1\u767D\u2026" : "\u8F93\u5165\u52A8\u4F5C\u2026",
2253
+ className: cn("lj-se-std-action", type === "narration" /* Narration */ && "is-narration"),
2254
+ onSave
2255
+ }
2256
+ );
2257
+ }
2258
+ return /* @__PURE__ */ jsxs("div", { className: "lj-se-row group", children: [
2259
+ handle,
2260
+ body2,
2261
+ /* @__PURE__ */ jsx(StateBadges, { labels: stateLabels })
2262
+ ] });
2263
+ }
2264
+ let body;
2265
+ if (type === "action" /* Action */) {
2266
+ body = /* @__PURE__ */ jsxs("p", { className: "lj-se-asian-action", children: [
2267
+ /* @__PURE__ */ jsx("span", { className: "lj-se-asian-action-mark", children: "\u25B3" }),
2268
+ /* @__PURE__ */ jsx(EditableText, { multiline: true, value: content, disabled, autoEdit, onEditEnd, placeholder: "\u8F93\u5165\u52A8\u4F5C\u2026", onSave })
2269
+ ] });
2270
+ } else if (type === "narration" /* Narration */) {
2271
+ body = /* @__PURE__ */ jsx("p", { className: "lj-se-asian-narration", children: /* @__PURE__ */ jsx(EditableText, { multiline: true, value: content, disabled, autoEdit, onEditEnd, placeholder: "\u8F93\u5165\u65C1\u767D\u2026", onSave }) });
2272
+ } else {
2273
+ const isInner = type === "inner_thought" /* InnerThought */;
2274
+ const tone = isInner ? "inner" : "dialogue";
2275
+ const tag = isInner ? emotion ? `OS\xB7${emotion}` : "OS" : emotion;
2276
+ body = /* @__PURE__ */ jsxs("p", { className: cn("lj-se-asian-cueline", isInner && "is-inner"), children: [
2277
+ structured ? /* @__PURE__ */ jsx("strong", { className: "lj-se-asian-speaker", style: !hasOrphanActor ? { color: getActorCueVar(actorId, tone) } : void 0, children: getDialogueSpeakerLabel(item, actorMap, speakerMap) }) : /* @__PURE__ */ jsx(
2278
+ SelectField,
2279
+ {
2280
+ value: actorId,
2281
+ options: actorOptions,
2282
+ disabled,
2283
+ placeholder: "\u672A\u77E5\u89D2\u8272",
2284
+ className: cn("lj-se-asian-speaker lj-se-asian-speaker--select", hasOrphanActor && "is-orphan"),
2285
+ style: !hasOrphanActor ? { color: getActorCueVar(actorId, tone) } : void 0,
2286
+ onChange: onSaveActor
2287
+ }
2288
+ ),
2289
+ !structured ? /* @__PURE__ */ jsxs("span", { className: "lj-se-asian-tag", children: [
2290
+ "\uFF08",
2291
+ /* @__PURE__ */ jsx(EditableText, { value: item.emotion ?? "", disabled, placeholder: isInner ? "OS" : "\u60C5\u7EEA", onSave: onSaveEmotion }),
2292
+ "\uFF09"
2293
+ ] }) : tag ? /* @__PURE__ */ jsxs("span", { className: "lj-se-asian-tag", children: [
2294
+ "\uFF08",
2295
+ tag,
2296
+ "\uFF09"
2297
+ ] }) : null,
2298
+ /* @__PURE__ */ jsx("span", { className: "lj-se-asian-colon", children: "\uFF1A" }),
2299
+ /* @__PURE__ */ jsx(EditableText, { multiline: true, value: content, disabled, autoEdit, onEditEnd, placeholder: "\u8F93\u5165\u5BF9\u767D\u2026", onSave })
2300
+ ] });
2301
+ }
2302
+ return /* @__PURE__ */ jsxs("div", { className: "lj-se-row lj-se-row--asian group", children: [
2303
+ handle,
2304
+ body,
2305
+ /* @__PURE__ */ jsx(StateBadges, { labels: stateLabels })
2306
+ ] });
2307
+ }
2308
+ var SceneItemRow = memo(SceneItemRowImpl);
2309
+ function SceneEditorImpl({
2310
+ episodeIndex,
2311
+ sceneIndex,
2312
+ scene,
2313
+ maps,
2314
+ format,
2315
+ disabled,
2316
+ focused = false,
2317
+ onEdit,
2318
+ onNavigateAsset,
2319
+ onRequestAssetDelete
2320
+ }) {
2321
+ const [pendingEdit, setPendingEdit] = useState(null);
2322
+ const { actorMap, speakerMap, locationMap, propMap } = maps;
2323
+ const actions = scene.actions ?? [];
2324
+ const actorOptions = useMemo(
2325
+ () => Array.from(actorMap.values()).map((a) => ({ value: (a.actor_id ?? "").trim(), label: getActorDisplayName(a) })).filter((o) => o.value),
2326
+ [actorMap]
2327
+ );
2328
+ const insertAt = useCallback(
2329
+ (index, type) => {
2330
+ const inherited = pickInheritedActorId(scene, index);
2331
+ onEdit((cur) => insertSceneItem(cur, episodeIndex, sceneIndex, index, buildNewScriptItem(type, inherited)));
2332
+ setPendingEdit(index);
2333
+ },
2334
+ [onEdit, episodeIndex, sceneIndex, scene]
2335
+ );
2336
+ const clearPending = useCallback(() => setPendingEdit(null), []);
2337
+ const insertSame = (atIndex, refIndex) => insertAt(atIndex, actions[refIndex]?.type ?? "dialogue" /* Dialogue */);
2338
+ const dnd = useDragReorder();
2339
+ const dragIndex = dnd?.source && dnd.source.ep === episodeIndex && dnd.source.sc === sceneIndex ? dnd.source.idx : null;
2340
+ const dropSlot = dnd?.dropAt && dnd.dropAt.ep === episodeIndex && dnd.dropAt.sc === sceneIndex ? dnd.dropAt.slot : null;
2341
+ const onDragStart = (e, idx) => {
2342
+ dnd?.begin({ ep: episodeIndex, sc: sceneIndex, idx });
2343
+ e.dataTransfer.effectAllowed = "move";
2344
+ try {
2345
+ e.dataTransfer.setData("text/plain", String(idx));
2346
+ } catch {
2347
+ }
2348
+ const slot = e.currentTarget.closest(".lj-se-action-slot");
2349
+ if (slot) e.dataTransfer.setDragImage(slot, 16, 16);
2350
+ };
2351
+ const onSlotDragOver = (e, idx) => {
2352
+ if (!dnd?.source) return;
2353
+ e.preventDefault();
2354
+ e.dataTransfer.dropEffect = "move";
2355
+ const rect = e.currentTarget.getBoundingClientRect();
2356
+ const after = e.clientY > rect.top + rect.height / 2;
2357
+ dnd.hover({ ep: episodeIndex, sc: sceneIndex, slot: after ? idx + 1 : idx });
2358
+ };
2359
+ const onSlotDrop = (e) => {
2360
+ e.preventDefault();
2361
+ dnd?.drop();
2362
+ };
2363
+ return /* @__PURE__ */ jsxs("div", { className: "lj-se-scene", children: [
2364
+ /* @__PURE__ */ jsx(
2365
+ SceneHeader,
2366
+ {
2367
+ scene,
2368
+ episodeIndex,
2369
+ sceneIndex,
2370
+ actorMap,
2371
+ locationMap,
2372
+ propMap,
2373
+ disabled,
2374
+ focused,
2375
+ format,
2376
+ onSaveData: onEdit,
2377
+ onRequestAssetDelete,
2378
+ onNavigateAsset
2379
+ }
2380
+ ),
2381
+ /* @__PURE__ */ jsxs("div", { className: "lj-se-actions", children: [
2382
+ actions.map((item, idx) => /* @__PURE__ */ jsx(
2383
+ "div",
2384
+ {
2385
+ className: cn("lj-se-action-slot", dragIndex === idx && "is-dragging", dropSlot === idx && "drop-before"),
2386
+ onDragOver: (e) => onSlotDragOver(e, idx),
2387
+ onDrop: onSlotDrop,
2388
+ children: /* @__PURE__ */ jsx(
2389
+ SceneItemRow,
2390
+ {
2391
+ item,
2392
+ actorMap,
2393
+ speakerMap,
2394
+ locationMap,
2395
+ propMap,
2396
+ actorOptions,
2397
+ format,
2398
+ disabled,
2399
+ autoEdit: pendingEdit === idx,
2400
+ onEditEnd: pendingEdit === idx ? clearPending : void 0,
2401
+ canSplit: idx > 0,
2402
+ dragHandleProps: disabled ? void 0 : { draggable: true, onDragStart: (e) => onDragStart(e, idx), onDragEnd: () => dnd?.end() },
2403
+ onSave: (content) => {
2404
+ clearPending();
2405
+ onEdit((cur) => updateSceneItemContent(cur, episodeIndex, sceneIndex, idx, content));
2406
+ },
2407
+ onSaveActor: (actorId) => onEdit((cur) => updateSceneItemActor(cur, episodeIndex, sceneIndex, idx, actorId)),
2408
+ onSaveEmotion: (emotion) => onEdit((cur) => updateSceneItemEmotion(cur, episodeIndex, sceneIndex, idx, emotion)),
2409
+ onChangeType: (type) => {
2410
+ clearPending();
2411
+ onEdit((cur) => updateSceneItemType(cur, episodeIndex, sceneIndex, idx, type));
2412
+ },
2413
+ onInsertAbove: () => insertSame(idx, idx),
2414
+ onInsertBelow: () => insertSame(idx + 1, idx),
2415
+ onSplitHere: idx > 0 ? () => {
2416
+ clearPending();
2417
+ onEdit((cur) => splitScene(cur, episodeIndex, sceneIndex, idx));
2418
+ } : void 0,
2419
+ onDelete: () => {
2420
+ clearPending();
2421
+ onEdit((cur) => deleteSceneItem(cur, episodeIndex, sceneIndex, idx));
2422
+ }
2423
+ }
2424
+ )
2425
+ },
2426
+ `item-${idx}`
2427
+ )),
2428
+ dropSlot === actions.length && actions.length > 0 ? /* @__PURE__ */ jsx("div", { className: "lj-se-dropline-end" }) : null,
2429
+ actions.length === 0 && !disabled ? /* @__PURE__ */ jsxs(
2430
+ "button",
2431
+ {
2432
+ type: "button",
2433
+ className: "lj-se-scene-add-first",
2434
+ onClick: () => insertAt(0, "dialogue" /* Dialogue */),
2435
+ children: [
2436
+ /* @__PURE__ */ jsx(Plus, { className: "lj-se-ic-3", strokeWidth: 2 }),
2437
+ " \u6DFB\u52A0\u5185\u5BB9"
2438
+ ]
2439
+ }
2440
+ ) : null,
2441
+ actions.length === 0 && disabled ? /* @__PURE__ */ jsx("p", { className: "lj-se-scene-empty", children: "\u672C\u573A\u5C1A\u65E0\u5185\u5BB9" }) : null
2442
+ ] })
2443
+ ] });
2444
+ }
2445
+ var SceneEditor = memo(SceneEditorImpl);
2446
+ function Stat({ label, value }) {
2447
+ return /* @__PURE__ */ jsxs("div", { className: "lj-se-stat", children: [
2448
+ /* @__PURE__ */ jsx("span", { className: "lj-se-stat-label", children: label }),
2449
+ /* @__PURE__ */ jsx("span", { className: "lj-se-stat-value", children: value })
2450
+ ] });
2451
+ }
2452
+ function WorldviewEditor({
2453
+ data,
2454
+ disabled = false,
2455
+ onEdit
2456
+ }) {
2457
+ const episodes = data.episodes ?? [];
2458
+ const totalScenes = episodes.reduce((sum, ep) => sum + (ep.scenes?.length ?? 0), 0);
2459
+ const totalLines = episodes.reduce(
2460
+ (sum, ep) => sum + (ep.scenes ?? []).reduce((s, sc) => s + (sc.actions?.length ?? 0), 0),
2461
+ 0
2462
+ );
2463
+ return /* @__PURE__ */ jsxs("div", { className: "lj-se-worldview lj-se-scroll", children: [
2464
+ /* @__PURE__ */ jsxs("header", { className: "lj-se-wv-header", children: [
2465
+ /* @__PURE__ */ jsx("div", { className: "lj-se-wv-kicker", children: "\u5267\u672C \xB7 \u9876\u5C42" }),
2466
+ /* @__PURE__ */ jsx("h2", { className: "lj-se-wv-title", children: "\u4E16\u754C\u89C2 & \u98CE\u683C" }),
2467
+ /* @__PURE__ */ jsx("p", { className: "lj-se-wv-sub", children: "\u6539\u52A8\u4F1A\u5F71\u54CD\u4E0B\u6E38\u8D44\u4EA7\u751F\u6210\u7684 prompt\uFF1B\u6539\u5B8C\u540E\u53EF\u8BA9 AI \u540C\u6B65\u66F4\u65B0\u4EBA\u8BBE / \u573A\u666F\u3002" })
2468
+ ] }),
2469
+ /* @__PURE__ */ jsxs("section", { className: "lj-se-stats", children: [
2470
+ /* @__PURE__ */ jsx(Stat, { label: "\u96C6\u6570", value: episodes.length }),
2471
+ /* @__PURE__ */ jsx(Stat, { label: "\u573A\u666F", value: totalScenes }),
2472
+ /* @__PURE__ */ jsx(Stat, { label: "\u53F0\u8BCD", value: totalLines }),
2473
+ /* @__PURE__ */ jsx(Stat, { label: "\u89D2\u8272", value: data.actors?.length ?? 0 }),
2474
+ /* @__PURE__ */ jsx(Stat, { label: "\u9053\u5177", value: data.props?.length ?? 0 })
2475
+ ] }),
2476
+ /* @__PURE__ */ jsxs("div", { className: "lj-se-field", children: [
2477
+ /* @__PURE__ */ jsx("div", { className: "lj-se-field-label", children: "\u6807\u9898" }),
2478
+ /* @__PURE__ */ jsx("div", { className: "lj-se-field-box", children: /* @__PURE__ */ jsx(
2479
+ EditableText,
2480
+ {
2481
+ value: data.title ?? "",
2482
+ disabled,
2483
+ placeholder: "\u672A\u547D\u540D\u5267\u672C",
2484
+ onSave: (v) => onEdit((p) => ({ ...p, title: v }))
2485
+ }
2486
+ ) })
2487
+ ] }),
2488
+ /* @__PURE__ */ jsxs("div", { className: "lj-se-field", children: [
2489
+ /* @__PURE__ */ jsxs("div", { className: "lj-se-field-label", children: [
2490
+ "\u4E16\u754C\u89C2 ",
2491
+ /* @__PURE__ */ jsx("span", { className: "lj-se-field-hint", children: "\u591A\u884C\uFF1BAI \u5199\u5267\u672C\u65F6\u4F1A\u5F15\u7528" })
2492
+ ] }),
2493
+ /* @__PURE__ */ jsx("div", { className: "lj-se-field-box", children: /* @__PURE__ */ jsx(
2494
+ EditableText,
2495
+ {
2496
+ multiline: true,
2497
+ value: data.worldview ?? "",
2498
+ disabled,
2499
+ placeholder: "\u63CF\u8FF0\u6545\u4E8B\u53D1\u751F\u7684\u4E16\u754C\u3001\u80CC\u666F\u8BBE\u5B9A\u2026",
2500
+ onSave: (v) => onEdit((p) => ({ ...p, worldview: v }))
2501
+ }
2502
+ ) })
2503
+ ] }),
2504
+ /* @__PURE__ */ jsxs("div", { className: "lj-se-field", children: [
2505
+ /* @__PURE__ */ jsxs("div", { className: "lj-se-field-label", children: [
2506
+ "\u98CE\u683C ",
2507
+ /* @__PURE__ */ jsx("span", { className: "lj-se-field-hint", children: "\u8BED\u8A00\u8C03\u6027 / \u955C\u5934\u8BED\u8A00\u503E\u5411" })
2508
+ ] }),
2509
+ /* @__PURE__ */ jsx("div", { className: "lj-se-field-box", children: /* @__PURE__ */ jsx(
2510
+ EditableText,
2511
+ {
2512
+ multiline: true,
2513
+ value: data.style ?? "",
2514
+ disabled,
2515
+ placeholder: "\u4F8B\u5982\uFF1A\u5199\u5B9E\u3001\u51B7\u5CFB\u3001\u957F\u955C\u5934\u2026",
2516
+ onSave: (v) => onEdit((p) => ({ ...p, style: v }))
2517
+ }
2518
+ ) })
2519
+ ] })
2520
+ ] });
2521
+ }
2522
+
2523
+ // src/stream/flat-items.ts
2524
+ var SCENE_KEY = (e, s) => `ep:${e}:sc:${s}`;
2525
+ var SCENE_ID_KEY = (e, id) => `ep:${e}:id:${id}`;
2526
+ var ASSET_KEY = (g, id) => `${g}:${id}`;
2527
+ function safeId(raw, fallback) {
2528
+ const t = (raw ?? "").toString().trim();
2529
+ return t.length > 0 ? t : fallback;
2530
+ }
2531
+ var GROUP_LABEL = { actors: "\u89D2\u8272", locations: "\u573A\u666F", props: "\u9053\u5177" };
2532
+ var assetGroupLabel = (g) => GROUP_LABEL[g];
2533
+ function buildFlatStream(data) {
2534
+ const items = [];
2535
+ const indexByKey = /* @__PURE__ */ new Map();
2536
+ const sceneIndexLookup = /* @__PURE__ */ new Map();
2537
+ const sceneIdLookup = /* @__PURE__ */ new Map();
2538
+ const assetIndexLookup = /* @__PURE__ */ new Map();
2539
+ const push = (item) => {
2540
+ indexByKey.set(item.key, items.length);
2541
+ if (item.kind === "scene") {
2542
+ sceneIndexLookup.set(SCENE_KEY(item.episodeIdx, item.sceneIdx), items.length);
2543
+ sceneIdLookup.set(SCENE_ID_KEY(item.episodeIdx, item.sceneId), items.length);
2544
+ }
2545
+ if (item.kind === "asset-card") assetIndexLookup.set(ASSET_KEY(item.group, item.assetId), items.length);
2546
+ items.push(item);
2547
+ };
2548
+ push({ key: "worldview", kind: "worldview" });
2549
+ if (!data) {
2550
+ return { items, indexByKey, sceneIndexLookup, sceneIdLookup, assetIndexLookup, paperStartKey: null, paperEndKey: null };
2551
+ }
2552
+ let paperStartKey = null;
2553
+ let paperEndKey = null;
2554
+ (data.episodes ?? []).forEach((episode, ei) => {
2555
+ const headerKey = `ep:${ei}:header`;
2556
+ push({ key: headerKey, kind: "episode-header", episodeIdx: ei });
2557
+ if (paperStartKey === null) paperStartKey = headerKey;
2558
+ paperEndKey = headerKey;
2559
+ const scenes = episode.scenes ?? [];
2560
+ if (scenes.length === 0) {
2561
+ const emptyKey = `ep:${ei}:empty`;
2562
+ push({ key: emptyKey, kind: "episode-empty", episodeIdx: ei });
2563
+ paperEndKey = emptyKey;
2564
+ } else {
2565
+ scenes.forEach((scene, si) => {
2566
+ const sceneId = safeId(scene.scene_id, `__idx:${si}`);
2567
+ const key = `sc:ep${ei}:sc${si}:${sceneId}`;
2568
+ push({
2569
+ key,
2570
+ kind: "scene",
2571
+ episodeIdx: ei,
2572
+ sceneIdx: si,
2573
+ sceneId,
2574
+ isStart: si === 0,
2575
+ isEnd: si === scenes.length - 1
2576
+ });
2577
+ paperEndKey = key;
2578
+ });
2579
+ }
2580
+ });
2581
+ push({ key: "add-episode", kind: "add-episode" });
2582
+ const specs = [
2583
+ { group: "actors", ids: (data.actors ?? []).map((a, i) => safeId(a.actor_id, `__idx:${i}`)) },
2584
+ { group: "locations", ids: (data.locations ?? []).map((l, i) => safeId(l.location_id, `__idx:${i}`)) },
2585
+ { group: "props", ids: (data.props ?? []).map((p, i) => safeId(p.prop_id, `__idx:${i}`)) }
2586
+ ];
2587
+ for (const { group, ids } of specs) {
2588
+ push({ key: `asset-group:${group}`, kind: "asset-group-header", group });
2589
+ ids.forEach((assetId) => push({ key: `asset:${group}:${assetId}`, kind: "asset-card", group, assetId }));
2590
+ }
2591
+ return { items, indexByKey, sceneIndexLookup, sceneIdLookup, assetIndexLookup, paperStartKey, paperEndKey };
2592
+ }
2593
+ function selectionToFlatIndex(stream, sel) {
2594
+ switch (sel.kind) {
2595
+ case "worldview":
2596
+ return stream.indexByKey.get("worldview") ?? null;
2597
+ case "episode":
2598
+ return stream.indexByKey.get(`ep:${sel.episodeIdx}:header`) ?? null;
2599
+ case "scene":
2600
+ return stream.sceneIndexLookup.get(SCENE_KEY(sel.episodeIdx, sel.sceneIdx)) ?? null;
2601
+ case "clip": {
2602
+ if (sel.sceneIdx !== void 0) {
2603
+ const byIdx = stream.sceneIndexLookup.get(SCENE_KEY(sel.episodeIdx, sel.sceneIdx));
2604
+ if (byIdx !== void 0) return byIdx;
2605
+ }
2606
+ const id = (sel.sceneId ?? "").trim();
2607
+ return id ? stream.sceneIdLookup.get(SCENE_ID_KEY(sel.episodeIdx, id)) ?? null : null;
2608
+ }
2609
+ case "actor":
2610
+ return stream.assetIndexLookup.get(ASSET_KEY("actors", sel.actorId)) ?? null;
2611
+ case "location":
2612
+ return stream.assetIndexLookup.get(ASSET_KEY("locations", sel.locationId)) ?? null;
2613
+ case "prop":
2614
+ return stream.assetIndexLookup.get(ASSET_KEY("props", sel.propId)) ?? null;
2615
+ default:
2616
+ return null;
2617
+ }
2618
+ }
2619
+ function flatIndexToSelection(stream, idx) {
2620
+ const item = stream.items[idx];
2621
+ if (!item) return null;
2622
+ switch (item.kind) {
2623
+ case "worldview":
2624
+ return { kind: "worldview" };
2625
+ case "episode-header":
2626
+ return { kind: "episode", episodeIdx: item.episodeIdx };
2627
+ case "scene":
2628
+ return { kind: "scene", episodeIdx: item.episodeIdx, sceneIdx: item.sceneIdx };
2629
+ case "asset-card":
2630
+ if (item.group === "actors") return { kind: "actor", actorId: item.assetId };
2631
+ if (item.group === "locations") return { kind: "location", locationId: item.assetId };
2632
+ return { kind: "prop", propId: item.assetId };
2633
+ default:
2634
+ return null;
2635
+ }
2636
+ }
2637
+
2638
+ // src/mutations/entity-mutations.ts
2639
+ var ID_FIELD = {
2640
+ actor: "actor_id",
2641
+ location: "location_id",
2642
+ prop: "prop_id"
2643
+ };
2644
+ var NAME_FIELD = {
2645
+ actor: "actor_name",
2646
+ location: "location_name",
2647
+ prop: "prop_name"
2648
+ };
2649
+ var LIST_FIELD = {
2650
+ actor: "actors",
2651
+ location: "locations",
2652
+ prop: "props"
2653
+ };
2654
+ function mapEntity(data, kind, id, fn) {
2655
+ const listKey = LIST_FIELD[kind];
2656
+ const idField = ID_FIELD[kind];
2657
+ const list = data[listKey] ?? [];
2658
+ let changed = false;
2659
+ const next = list.map((e) => {
2660
+ if ((e[idField] ?? "").trim() !== id) return e;
2661
+ changed = true;
2662
+ return fn(e);
2663
+ });
2664
+ if (!changed) return data;
2665
+ return { ...data, [listKey]: next };
2666
+ }
2667
+ function updateEntityName(data, kind, id, name) {
2668
+ return mapEntity(data, kind, id, (e) => ({ ...e, [NAME_FIELD[kind]]: name }));
2669
+ }
2670
+ function updateEntityDescription(data, kind, id, description) {
2671
+ return mapEntity(data, kind, id, (e) => ({ ...e, description }));
2672
+ }
2673
+ function updateEntityStateField(data, kind, id, stateId, field, value) {
2674
+ return mapEntity(data, kind, id, (e) => ({
2675
+ ...e,
2676
+ states: (e.states ?? []).map(
2677
+ (s) => (s.state_id ?? "").trim() === stateId ? { ...s, [field]: value } : s
2678
+ )
2679
+ }));
2680
+ }
2681
+ function addEntityState(data, kind, id, state) {
2682
+ return mapEntity(data, kind, id, (e) => ({ ...e, states: [...e.states ?? [], state] }));
2683
+ }
2684
+ function removeEntityState(data, kind, id, stateId) {
2685
+ return mapEntity(data, kind, id, (e) => ({
2686
+ ...e,
2687
+ states: (e.states ?? []).filter((s) => (s.state_id ?? "").trim() !== stateId)
2688
+ }));
2689
+ }
2690
+ var CONFIRM_TTL_MS = 2500;
2691
+ function DeleteConfirmButton({
2692
+ onConfirm,
2693
+ disabled,
2694
+ targetScope,
2695
+ title = "\u5220\u9664\uFF1F"
2696
+ }) {
2697
+ const [confirming, setConfirming] = useState(false);
2698
+ const timerRef = useRef(null);
2699
+ useEffect(
2700
+ () => () => {
2701
+ if (timerRef.current) clearTimeout(timerRef.current);
2702
+ },
2703
+ []
2704
+ );
2705
+ const cancel = () => {
2706
+ if (timerRef.current) {
2707
+ clearTimeout(timerRef.current);
2708
+ timerRef.current = null;
2709
+ }
2710
+ setConfirming(false);
2711
+ };
2712
+ const handleClick = () => {
2713
+ if (confirming) {
2714
+ cancel();
2715
+ void onConfirm();
2716
+ return;
2717
+ }
2718
+ setConfirming(true);
2719
+ if (timerRef.current) clearTimeout(timerRef.current);
2720
+ timerRef.current = setTimeout(() => {
2721
+ setConfirming(false);
2722
+ timerRef.current = null;
2723
+ }, CONFIRM_TTL_MS);
2724
+ };
2725
+ return /* @__PURE__ */ jsx(
2726
+ "button",
2727
+ {
2728
+ type: "button",
2729
+ disabled,
2730
+ "data-delete-target": targetScope,
2731
+ "data-confirming": confirming || void 0,
2732
+ onClick: handleClick,
2733
+ "aria-label": confirming ? `\u518D\u70B9\u4E00\u6B21\u786E\u8BA4\uFF1A${title}` : title,
2734
+ title: confirming ? "\u518D\u70B9\u4E00\u6B21\u786E\u8BA4\uFF082.5 \u79D2\u540E\u81EA\u52A8\u53D6\u6D88\uFF09" : title,
2735
+ className: cn("lj-se-del", confirming && "is-confirming"),
2736
+ children: confirming ? /* @__PURE__ */ jsx(Check, { className: "lj-se-icon", strokeWidth: 2.2 }) : /* @__PURE__ */ jsx(Eraser, { className: "lj-se-icon", strokeWidth: 1.5 })
2737
+ }
2738
+ );
2739
+ }
2740
+ function AssetReferenceGrid({
2741
+ refs,
2742
+ highlight,
2743
+ onJump
2744
+ }) {
2745
+ if (refs.length === 0) return null;
2746
+ return /* @__PURE__ */ jsx("div", { className: "lj-se-refgrid", children: refs.map(({ episodeIndex, episodeNumber, items }) => /* @__PURE__ */ jsxs("span", { className: "lj-se-refgrid-chunk", children: [
2747
+ /* @__PURE__ */ jsxs("span", { className: "lj-se-refgrid-ep", children: [
2748
+ "\u7B2C ",
2749
+ String(episodeNumber).padStart(2, "0"),
2750
+ " \u96C6"
2751
+ ] }),
2752
+ /* @__PURE__ */ jsx("span", { className: "lj-se-refgrid-scenes", children: items.map((ref) => {
2753
+ const isCurrent = !!highlight && highlight.episodeIdx === ref.episodeIndex && highlight.sceneIdx === ref.sceneIndex;
2754
+ return /* @__PURE__ */ jsx(
2755
+ "button",
2756
+ {
2757
+ type: "button",
2758
+ className: cn("lj-se-refgrid-scene", isCurrent && "is-current"),
2759
+ title: ref.label,
2760
+ onClick: () => onJump(ref.episodeIndex, ref.sceneIndex),
2761
+ children: ref.sceneNumber
2762
+ },
2763
+ ref.key
2764
+ );
2765
+ }) })
2766
+ ] }, episodeIndex)) });
2767
+ }
2768
+ var META = {
2769
+ actors: { kind: "actor" },
2770
+ locations: { kind: "location" },
2771
+ props: { kind: "prop" }
2772
+ };
2773
+ function newStateId(id) {
2774
+ return `st_${id}_${Date.now().toString(36)}`;
2775
+ }
2776
+ function AssetCardImpl({
2777
+ data,
2778
+ group,
2779
+ assetId,
2780
+ refs,
2781
+ disabled = false,
2782
+ onEdit,
2783
+ onSelect,
2784
+ onRequestAssetDelete
2785
+ }) {
2786
+ const kind = META[group].kind;
2787
+ const entity = useMemo(() => {
2788
+ if (group === "actors") return (data.actors ?? []).find((a) => (a.actor_id ?? "").trim() === assetId);
2789
+ if (group === "locations") return (data.locations ?? []).find((l) => (l.location_id ?? "").trim() === assetId);
2790
+ return (data.props ?? []).find((p) => (p.prop_id ?? "").trim() === assetId);
2791
+ }, [data.actors, data.locations, data.props, group, assetId]);
2792
+ const sceneCount = refs.reduce((n, g) => n + g.items.length, 0);
2793
+ if (!entity) {
2794
+ return /* @__PURE__ */ jsx("div", { className: "lj-se-assetcard-row", children: /* @__PURE__ */ jsxs("div", { className: "lj-se-assetcard-missing", children: [
2795
+ "\u8BE5\u8D44\u4EA7\u5DF2\u4E0D\u5B58\u5728\uFF08",
2796
+ group,
2797
+ ":",
2798
+ assetId,
2799
+ "\uFF09"
2800
+ ] }) });
2801
+ }
2802
+ const name = group === "actors" ? getActorDisplayName(entity) : group === "locations" ? getLocationDisplayName(entity) : getPropDisplayName(entity);
2803
+ const rawDescription = entity.description ?? "";
2804
+ const description = rawDescription.trim();
2805
+ const states = entity.states ?? [];
2806
+ const fromRef = buildSelectionFrom(kind, assetId);
2807
+ return /* @__PURE__ */ jsx("div", { className: "lj-se-assetcard-row", children: /* @__PURE__ */ jsxs("article", { className: "lj-se-assetcard", "data-no-quote-selection": true, children: [
2808
+ /* @__PURE__ */ jsxs("div", { className: "lj-se-assetcard-head", children: [
2809
+ /* @__PURE__ */ jsx("span", { className: "lj-se-assetcard-name", children: /* @__PURE__ */ jsx(
2810
+ EditableText,
2811
+ {
2812
+ value: name,
2813
+ disabled,
2814
+ placeholder: `\u672A\u547D\u540D${assetGroupLabel(group)}`,
2815
+ onSave: (v) => onEdit((p) => updateEntityName(p, kind, assetId, v))
2816
+ }
2817
+ ) }),
2818
+ /* @__PURE__ */ jsx("span", { className: "lj-se-assetcard-id", children: assetId }),
2819
+ /* @__PURE__ */ jsxs("span", { className: "lj-se-assetcard-actions", children: [
2820
+ /* @__PURE__ */ jsx("span", { className: "lj-se-assetcard-badge", children: assetGroupLabel(group) }),
2821
+ /* @__PURE__ */ jsxs("span", { className: "lj-se-assetcard-badge", children: [
2822
+ sceneCount,
2823
+ " \u573A \xB7 ",
2824
+ refs.length,
2825
+ " \u96C6"
2826
+ ] }),
2827
+ onRequestAssetDelete && !disabled ? /* @__PURE__ */ jsx(
2828
+ "button",
2829
+ {
2830
+ type: "button",
2831
+ className: "lj-se-assetcard-del",
2832
+ title: "\u5220\u9664\u8BE5\u8D44\u4EA7",
2833
+ onClick: () => onRequestAssetDelete({ kind, id: assetId, name: name || assetId, description: description || void 0, episodeIndex: -1, sceneIndex: -1 }),
2834
+ children: /* @__PURE__ */ jsx(Trash2, { className: "lj-se-ic-3", strokeWidth: 1.5 })
2835
+ }
2836
+ ) : null
2837
+ ] })
2838
+ ] }),
2839
+ /* @__PURE__ */ jsx("div", { className: "lj-se-assetcard-desc", children: /* @__PURE__ */ jsx(
2840
+ EditableText,
2841
+ {
2842
+ multiline: true,
2843
+ value: rawDescription,
2844
+ disabled,
2845
+ placeholder: `\u63CF\u8FF0\u8FD9\u4E2A${assetGroupLabel(group)}\u7684\u5916\u89C2 / \u8BBE\u5B9A\u2026`,
2846
+ onSave: (v) => onEdit((p) => updateEntityDescription(p, kind, assetId, v))
2847
+ }
2848
+ ) }),
2849
+ /* @__PURE__ */ jsxs("div", { className: "lj-se-assetcard-states", children: [
2850
+ /* @__PURE__ */ jsxs("div", { className: "lj-se-assetcard-states-head", children: [
2851
+ /* @__PURE__ */ jsxs("span", { className: "lj-se-assetcard-states-label", children: [
2852
+ "\u72B6\u6001 ",
2853
+ /* @__PURE__ */ jsx("span", { className: "lj-se-field-hint", children: states.length })
2854
+ ] }),
2855
+ !disabled ? /* @__PURE__ */ jsxs(
2856
+ "button",
2857
+ {
2858
+ type: "button",
2859
+ className: "lj-se-assetcard-state-add",
2860
+ onClick: () => onEdit((p) => addEntityState(p, kind, assetId, { state_id: newStateId(assetId), state_name: "\u65B0\u72B6\u6001" })),
2861
+ children: [
2862
+ /* @__PURE__ */ jsx(Plus, { className: "lj-se-ic-3", strokeWidth: 1.5 }),
2863
+ " \u72B6\u6001"
2864
+ ]
2865
+ }
2866
+ ) : null
2867
+ ] }),
2868
+ states.length > 0 ? /* @__PURE__ */ jsx("div", { className: "lj-se-assetcard-state-cards", children: states.map((state, idx) => {
2869
+ const sid = (state.state_id ?? "").trim() || `idx-${idx}`;
2870
+ return /* @__PURE__ */ jsxs("div", { className: "lj-se-assetcard-state", children: [
2871
+ /* @__PURE__ */ jsxs("div", { className: "lj-se-assetcard-state-top", children: [
2872
+ /* @__PURE__ */ jsx(
2873
+ EditableText,
2874
+ {
2875
+ value: state.state_name ?? state.name ?? "",
2876
+ disabled,
2877
+ placeholder: "\u72B6\u6001\u540D",
2878
+ className: "lj-se-assetcard-state-name",
2879
+ onSave: (v) => onEdit((p) => updateEntityStateField(p, kind, assetId, sid, "state_name", v))
2880
+ }
2881
+ ),
2882
+ !disabled && (state.state_id ?? "").trim() ? /* @__PURE__ */ jsx(DeleteConfirmButton, { targetScope: "row", title: "\u5220\u9664\u8BE5\u72B6\u6001\uFF1F", onConfirm: () => onEdit((p) => removeEntityState(p, kind, assetId, sid)) }) : null
2883
+ ] }),
2884
+ /* @__PURE__ */ jsx(
2885
+ EditableText,
2886
+ {
2887
+ multiline: true,
2888
+ value: state.description ?? "",
2889
+ disabled,
2890
+ placeholder: "\u72B6\u6001\u63CF\u8FF0\u2026",
2891
+ className: "lj-se-assetcard-state-desc",
2892
+ onSave: (v) => onEdit((p) => updateEntityStateField(p, kind, assetId, sid, "description", v))
2893
+ }
2894
+ )
2895
+ ] }, sid);
2896
+ }) }) : null
2897
+ ] }),
2898
+ refs.length > 0 ? /* @__PURE__ */ jsx(
2899
+ AssetReferenceGrid,
2900
+ {
2901
+ refs,
2902
+ onJump: (episodeIdx, sceneIdx) => onSelect({ kind: "scene", episodeIdx, sceneIdx, from: fromRef, origin: "sidebar" })
2903
+ }
2904
+ ) : null
2905
+ ] }) });
2906
+ }
2907
+ function areEqual(prev, next) {
2908
+ if (prev.group !== next.group || prev.assetId !== next.assetId || prev.disabled !== next.disabled) return false;
2909
+ if (prev.onEdit !== next.onEdit || prev.onSelect !== next.onSelect || prev.onRequestAssetDelete !== next.onRequestAssetDelete) return false;
2910
+ if (prev.refs !== next.refs) return false;
2911
+ if (prev.group === "actors") return prev.data.actors === next.data.actors;
2912
+ if (prev.group === "locations") return prev.data.locations === next.data.locations;
2913
+ return prev.data.props === next.data.props;
2914
+ }
2915
+ var AssetCard = memo(AssetCardImpl, areEqual);
2916
+ function ScriptStream({
2917
+ value,
2918
+ format,
2919
+ disabled,
2920
+ selection,
2921
+ changedSceneKeys,
2922
+ onEdit,
2923
+ onSelectionChange,
2924
+ onRequestAssetDelete
2925
+ }) {
2926
+ const stream = useMemo(() => buildFlatStream(value), [value]);
2927
+ const maps = useMemo(
2928
+ () => ({
2929
+ actorMap: getActorMap(value.actors ?? []),
2930
+ speakerMap: getSpeakerMap(value.speakers ?? []),
2931
+ locationMap: getLocationMap(value.locations ?? []),
2932
+ propMap: getPropMap(value.props ?? [])
2933
+ }),
2934
+ [value.actors, value.speakers, value.locations, value.props]
2935
+ );
2936
+ const deferredValue = useDeferredValue(value);
2937
+ const prevDistributions = useRef(void 0);
2938
+ const distributions = useMemo(() => {
2939
+ const next = buildDistributionIndex(deferredValue, prevDistributions.current);
2940
+ prevDistributions.current = next;
2941
+ return next;
2942
+ }, [deferredValue]);
2943
+ const handleRef = useRef(null);
2944
+ const programmaticUntil = useRef(0);
2945
+ const lastEmitted = useRef(-1);
2946
+ const lastScrolledKey = useRef("");
2947
+ const select = useCallback(
2948
+ (sel) => onSelectionChange?.(sel),
2949
+ [onSelectionChange]
2950
+ );
2951
+ const [dragSource, setDragSource] = useState(null);
2952
+ const [dropTarget, setDropTarget] = useState(null);
2953
+ const dragSourceRef = useRef(null);
2954
+ const dropTargetRef = useRef(null);
2955
+ dragSourceRef.current = dragSource;
2956
+ dropTargetRef.current = dropTarget;
2957
+ const dnd = useMemo(
2958
+ () => ({
2959
+ source: dragSource,
2960
+ dropAt: dropTarget,
2961
+ begin: (s) => {
2962
+ setDragSource(s);
2963
+ setDropTarget(null);
2964
+ },
2965
+ hover: (t) => setDropTarget(t),
2966
+ drop: () => {
2967
+ const s = dragSourceRef.current;
2968
+ const d = dropTargetRef.current;
2969
+ if (s && d) {
2970
+ onEdit(
2971
+ (cur) => moveScriptItem(
2972
+ cur,
2973
+ { episodeIndex: s.ep, sceneIndex: s.sc, itemIndex: s.idx },
2974
+ { episodeIndex: d.ep, sceneIndex: d.sc, slot: d.slot }
2975
+ )
2976
+ );
2977
+ }
2978
+ setDragSource(null);
2979
+ setDropTarget(null);
2980
+ },
2981
+ end: () => {
2982
+ setDragSource(null);
2983
+ setDropTarget(null);
2984
+ }
2985
+ }),
2986
+ [dragSource, dropTarget, onEdit]
2987
+ );
2988
+ const paperEdge = (key) => {
2989
+ const start = key === stream.paperStartKey;
2990
+ const end = key === stream.paperEndKey;
2991
+ return start && end ? "is-sole" : start ? "is-start" : end ? "is-end" : "is-mid";
2992
+ };
2993
+ useEffect(() => {
2994
+ const key = selectionTargetKey(selection);
2995
+ if (selection.origin === "scroll") {
2996
+ lastScrolledKey.current = key;
2997
+ return;
2998
+ }
2999
+ if (key === lastScrolledKey.current) return;
3000
+ const idx = selectionToFlatIndex(stream, selection);
3001
+ if (idx == null) return;
3002
+ lastScrolledKey.current = key;
3003
+ programmaticUntil.current = Date.now() + 700;
3004
+ handleRef.current?.scrollToIndex({ index: idx, align: "start", behavior: "smooth" });
3005
+ }, [selection, stream]);
3006
+ const emitScrollSelection = (topIndex) => {
3007
+ if (Date.now() < programmaticUntil.current) return;
3008
+ if (topIndex === lastEmitted.current) return;
3009
+ lastEmitted.current = topIndex;
3010
+ const next = flatIndexToSelection(stream, topIndex);
3011
+ if (!next) return;
3012
+ if (isSameSelectionTarget(next, selection)) return;
3013
+ onSelectionChange?.(withOrigin(next, "scroll"));
3014
+ };
3015
+ return /* @__PURE__ */ jsx(DragReorderContext.Provider, { value: dnd, children: /* @__PURE__ */ jsx(
3016
+ Virtuoso,
3017
+ {
3018
+ ref: handleRef,
3019
+ className: "lj-se-stream lj-se-scroll",
3020
+ data: stream.items,
3021
+ increaseViewportBy: 1500,
3022
+ rangeChanged: (range) => emitScrollSelection(range.startIndex),
3023
+ itemContent: (_index, item) => {
3024
+ if (!item) return null;
3025
+ switch (item.kind) {
3026
+ case "worldview":
3027
+ return /* @__PURE__ */ jsx("div", { className: "lj-se-stream-worldview", children: /* @__PURE__ */ jsx(WorldviewEditor, { data: value, disabled, onEdit }) });
3028
+ case "episode-header": {
3029
+ const episode = value.episodes?.[item.episodeIdx];
3030
+ if (!episode) return null;
3031
+ return /* @__PURE__ */ jsx("div", { className: "lj-se-stream-row", children: /* @__PURE__ */ jsx("div", { className: cn("lj-se-paper-seg", paperEdge(item.key), "lj-se-ep-seg"), children: /* @__PURE__ */ jsxs("div", { className: "lj-se-epbanner-wrap", children: [
3032
+ !disabled ? /* @__PURE__ */ jsx(EpisodeMenu, { episodeIdx: item.episodeIdx, onEdit }) : null,
3033
+ /* @__PURE__ */ jsx(
3034
+ EpisodeBanner,
3035
+ {
3036
+ episodeIdx: item.episodeIdx,
3037
+ episodeTitle: getEpisodeTitle(episode, item.episodeIdx),
3038
+ projectTitle: item.episodeIdx === 0 ? value.title ?? "" : void 0,
3039
+ disabled,
3040
+ onEdit
3041
+ }
3042
+ )
3043
+ ] }) }) });
3044
+ }
3045
+ case "episode-empty":
3046
+ if (disabled) return null;
3047
+ return /* @__PURE__ */ jsx("div", { className: "lj-se-stream-row", children: /* @__PURE__ */ jsx("div", { className: cn("lj-se-paper-seg", paperEdge(item.key)), children: /* @__PURE__ */ jsxs(
3048
+ "button",
3049
+ {
3050
+ type: "button",
3051
+ className: "lj-se-scene-add-first",
3052
+ onClick: () => onEdit((p) => addBlankScene(p, item.episodeIdx)),
3053
+ children: [
3054
+ /* @__PURE__ */ jsx(Plus, { className: "lj-se-ic-3", strokeWidth: 2 }),
3055
+ " \u6DFB\u52A0\u573A\u6B21"
3056
+ ]
3057
+ }
3058
+ ) }) });
3059
+ case "scene": {
3060
+ const scene = value.episodes?.[item.episodeIdx]?.scenes?.[item.sceneIdx];
3061
+ if (!scene) return null;
3062
+ const focused = selection.kind === "scene" && selection.episodeIdx === item.episodeIdx && selection.sceneIdx === item.sceneIdx;
3063
+ const changed = changedSceneKeys?.has(`${item.episodeIdx}:${item.sceneId}`) ?? false;
3064
+ return /* @__PURE__ */ jsx("div", { className: "lj-se-stream-row", "data-quote-source": true, children: /* @__PURE__ */ jsx("div", { className: cn("lj-se-paper-seg", paperEdge(item.key), changed && "is-changed"), children: /* @__PURE__ */ jsx(
3065
+ SceneEditor,
3066
+ {
3067
+ episodeIndex: item.episodeIdx,
3068
+ sceneIndex: item.sceneIdx,
3069
+ scene,
3070
+ maps,
3071
+ format,
3072
+ disabled,
3073
+ focused,
3074
+ onEdit,
3075
+ onNavigateAsset: select,
3076
+ onRequestAssetDelete
3077
+ }
3078
+ ) }) });
3079
+ }
3080
+ case "add-episode":
3081
+ if (disabled) return null;
3082
+ return /* @__PURE__ */ jsx("div", { className: "lj-se-addep-row", children: /* @__PURE__ */ jsxs("button", { type: "button", className: "lj-se-addep-btn", onClick: () => onEdit((p) => addEpisode(p)), children: [
3083
+ /* @__PURE__ */ jsx(Plus, { className: "lj-se-ic-4", strokeWidth: 2 }),
3084
+ " \u65B0\u589E\u4E00\u96C6"
3085
+ ] }) });
3086
+ case "asset-group-header":
3087
+ return /* @__PURE__ */ jsx("div", { className: "lj-se-assetgroup-head", children: /* @__PURE__ */ jsx("span", { className: "lj-se-assetgroup-title", children: assetGroupLabel(item.group) }) });
3088
+ case "asset-card":
3089
+ return /* @__PURE__ */ jsx(
3090
+ AssetCard,
3091
+ {
3092
+ data: value,
3093
+ group: item.group,
3094
+ assetId: item.assetId,
3095
+ refs: distributions.get(`${item.group}:${item.assetId}`) ?? buildAssetRefs(value, item.group, item.assetId),
3096
+ disabled,
3097
+ onEdit,
3098
+ onSelect: select,
3099
+ onRequestAssetDelete
3100
+ }
3101
+ );
3102
+ default:
3103
+ return null;
3104
+ }
3105
+ }
3106
+ }
3107
+ ) });
3108
+ }
3109
+ function EpisodeMenu({
3110
+ episodeIdx,
3111
+ onEdit
3112
+ }) {
3113
+ return /* @__PURE__ */ jsx(
3114
+ Popover,
3115
+ {
3116
+ align: "start",
3117
+ side: "bottom",
3118
+ className: "lj-se-ephandle-pop",
3119
+ panelClassName: "lj-se-rowmenu",
3120
+ trigger: ({ open, toggle }) => /* @__PURE__ */ jsx("button", { type: "button", className: cn("lj-se-handle lj-se-ephandle", open && "is-open"), title: "\u96C6\u64CD\u4F5C", "aria-label": "\u96C6\u64CD\u4F5C", onClick: toggle, children: /* @__PURE__ */ jsx(GripVertical, { className: "lj-se-handle-grip", strokeWidth: 1.5 }) }),
3121
+ children: ({ close }) => /* @__PURE__ */ jsx(
3122
+ RowMenuPanel,
3123
+ {
3124
+ close,
3125
+ sections: [
3126
+ [{ icon: Plus, label: "\u5728\u4E0B\u65B9\u65B0\u589E\u4E00\u96C6", onSelect: () => onEdit((p) => addEpisode(p, episodeIdx + 1)) }],
3127
+ [{ icon: Trash2, label: "\u5220\u9664\u6574\u96C6", onSelect: () => onEdit((p) => deleteEpisode(p, episodeIdx)), danger: true }]
3128
+ ]
3129
+ }
3130
+ )
3131
+ }
3132
+ );
3133
+ }
3134
+ function EpisodeBanner({
3135
+ episodeIdx,
3136
+ episodeTitle,
3137
+ projectTitle,
3138
+ disabled,
3139
+ onEdit
3140
+ }) {
3141
+ return /* @__PURE__ */ jsxs("div", { className: "lj-se-epbanner", children: [
3142
+ projectTitle !== void 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
3143
+ /* @__PURE__ */ jsx("div", { className: "lj-se-epbanner-kicker", children: "\u5267\u672C" }),
3144
+ /* @__PURE__ */ jsx("h1", { className: "lj-se-epbanner-script", children: /* @__PURE__ */ jsx(
3145
+ EditableText,
3146
+ {
3147
+ value: projectTitle,
3148
+ disabled,
3149
+ placeholder: "\u672A\u547D\u540D",
3150
+ onSave: (v) => onEdit((p) => ({ ...p, title: v }))
3151
+ }
3152
+ ) })
3153
+ ] }) : null,
3154
+ /* @__PURE__ */ jsx("h2", { className: "lj-se-epbanner-ep", children: /* @__PURE__ */ jsx(
3155
+ EditableText,
3156
+ {
3157
+ value: episodeTitle,
3158
+ disabled,
3159
+ placeholder: "\u672A\u547D\u540D\u96C6",
3160
+ onSave: (v) => onEdit((p) => ({
3161
+ ...p,
3162
+ episodes: p.episodes?.map((ep, i) => i === episodeIdx ? { ...ep, title: v } : ep) ?? p.episodes
3163
+ }))
3164
+ }
3165
+ ) }),
3166
+ /* @__PURE__ */ jsx("span", { className: "lj-se-epbanner-divider", "aria-hidden": true })
3167
+ ] });
3168
+ }
3169
+
3170
+ // src/diff/diff-engine.ts
3171
+ function sceneChangeKey(episodeIdx, sceneId) {
3172
+ return `${episodeIdx}:${(sceneId ?? "").trim()}`;
3173
+ }
3174
+ function fieldChange(before, after) {
3175
+ const b = (before ?? "").trim();
3176
+ const a = (after ?? "").trim();
3177
+ return b === a ? void 0 : { before: b, after: a };
3178
+ }
3179
+ function serializeScene(scene) {
3180
+ const ctx = getSceneContext(scene);
3181
+ return JSON.stringify({
3182
+ environment: scene.environment ?? null,
3183
+ actors: ctx.actors,
3184
+ locations: ctx.locations,
3185
+ props: ctx.props,
3186
+ actions: scene.actions ?? []
3187
+ });
3188
+ }
3189
+ function sceneById(episode) {
3190
+ const map = /* @__PURE__ */ new Map();
3191
+ for (const scene of episode?.scenes ?? []) {
3192
+ const id = (scene.scene_id ?? "").trim();
3193
+ if (id) map.set(id, scene);
3194
+ }
3195
+ return map;
3196
+ }
3197
+ function matchEpisode(episodes, ref, index) {
3198
+ const list = episodes ?? [];
3199
+ const id = ref.episode_id?.trim();
3200
+ if (id) {
3201
+ const byId = list.find((e) => e.episode_id?.trim() === id);
3202
+ if (byId) return byId;
3203
+ }
3204
+ return list[index];
3205
+ }
3206
+ function computeChangeSet(baseline, current) {
3207
+ const changedScenes = /* @__PURE__ */ new Map();
3208
+ let removedCount = 0;
3209
+ if (!baseline) {
3210
+ return { changedScenes, removedCount, count: 0 };
3211
+ }
3212
+ const title = fieldChange(baseline.title, current.title);
3213
+ const worldview = fieldChange(baseline.worldview, current.worldview);
3214
+ const style = fieldChange(baseline.style, current.style);
3215
+ const curEpisodes = current.episodes ?? [];
3216
+ curEpisodes.forEach((episode, ei) => {
3217
+ const baseMap = sceneById(matchEpisode(baseline.episodes, episode, ei));
3218
+ for (const scene of episode.scenes ?? []) {
3219
+ const id = (scene.scene_id ?? "").trim();
3220
+ if (!id) continue;
3221
+ const base = baseMap.get(id);
3222
+ if (!base) {
3223
+ changedScenes.set(sceneChangeKey(ei, id), "added");
3224
+ } else if (base === scene) {
3225
+ continue;
3226
+ } else if (serializeScene(base) !== serializeScene(scene)) {
3227
+ changedScenes.set(sceneChangeKey(ei, id), "modified");
3228
+ }
3229
+ }
3230
+ });
3231
+ (baseline.episodes ?? []).forEach((episode, ei) => {
3232
+ const curMap = sceneById(matchEpisode(curEpisodes, episode, ei));
3233
+ for (const scene of episode.scenes ?? []) {
3234
+ const id = (scene.scene_id ?? "").trim();
3235
+ if (id && !curMap.has(id)) removedCount += 1;
3236
+ }
3237
+ });
3238
+ const count = (title ? 1 : 0) + (worldview ? 1 : 0) + (style ? 1 : 0) + changedScenes.size + removedCount;
3239
+ return { title, worldview, style, changedScenes, removedCount, count };
3240
+ }
3241
+ function QuoteLayer({
3242
+ containerRef,
3243
+ onAddChatReferences
3244
+ }) {
3245
+ const [pos, setPos] = useState(null);
3246
+ const seqRef = useRef(0);
3247
+ useEffect(() => {
3248
+ const container = containerRef.current;
3249
+ if (!container) return;
3250
+ function update(pointerX, pointerY) {
3251
+ const sel = window.getSelection();
3252
+ if (!sel || sel.isCollapsed || sel.rangeCount === 0) {
3253
+ setPos(null);
3254
+ return;
3255
+ }
3256
+ const range = sel.getRangeAt(0);
3257
+ const anchor = sel.anchorNode ?? range.commonAncestorContainer;
3258
+ const anchorEl = anchor instanceof Element ? anchor : anchor.parentElement;
3259
+ const host = containerRef.current;
3260
+ if (!host || !anchorEl || !host.contains(anchorEl)) {
3261
+ setPos(null);
3262
+ return;
3263
+ }
3264
+ if (!anchorEl.closest("[data-quote-source]")) {
3265
+ setPos(null);
3266
+ return;
3267
+ }
3268
+ const text = sel.toString().trim();
3269
+ if (!text) {
3270
+ setPos(null);
3271
+ return;
3272
+ }
3273
+ const hostRect = host.getBoundingClientRect();
3274
+ const left = Math.min(Math.max(8, pointerX - hostRect.left + 6), hostRect.width - 150);
3275
+ const top = Math.min(Math.max(36, pointerY - hostRect.top - 6), hostRect.height - 8);
3276
+ setPos({ top, left, text });
3277
+ }
3278
+ function onUp(e) {
3279
+ const x = e.clientX;
3280
+ const y = e.clientY;
3281
+ requestAnimationFrame(() => update(x, y));
3282
+ }
3283
+ function onDown(e) {
3284
+ if (e.target?.closest?.(".lj-se-quote-btn")) return;
3285
+ setPos(null);
3286
+ }
3287
+ document.addEventListener("mouseup", onUp);
3288
+ document.addEventListener("mousedown", onDown, true);
3289
+ return () => {
3290
+ document.removeEventListener("mouseup", onUp);
3291
+ document.removeEventListener("mousedown", onDown, true);
3292
+ };
3293
+ }, [containerRef]);
3294
+ if (!pos) return null;
3295
+ return /* @__PURE__ */ jsxs(
3296
+ "button",
3297
+ {
3298
+ type: "button",
3299
+ className: "lj-se-quote-btn",
3300
+ style: { top: pos.top, left: pos.left },
3301
+ onClick: () => {
3302
+ seqRef.current += 1;
3303
+ onAddChatReferences([
3304
+ {
3305
+ id: `quote-${seqRef.current}-${pos.text.length}`,
3306
+ label: pos.text.length > 24 ? pos.text.slice(0, 24) + "\u2026" : pos.text,
3307
+ text: pos.text
3308
+ }
3309
+ ]);
3310
+ window.getSelection()?.removeAllRanges();
3311
+ setPos(null);
3312
+ },
3313
+ children: [
3314
+ /* @__PURE__ */ jsx(MessageSquareQuote, { className: "lj-se-icon-sm", strokeWidth: 1.5 }),
3315
+ "\u5F15\u7528\u5230\u5BF9\u8BDD"
3316
+ ]
3317
+ }
3318
+ );
3319
+ }
3320
+ function ScriptEditor({
3321
+ value,
3322
+ onEdit,
3323
+ selection: controlledSelection,
3324
+ onSelectionChange,
3325
+ format: controlledFormat,
3326
+ onFormatChange,
3327
+ disabled = false,
3328
+ className,
3329
+ dark,
3330
+ saveStatus = "idle",
3331
+ canUndo,
3332
+ canRedo,
3333
+ onUndo,
3334
+ onRedo,
3335
+ onEditingChange,
3336
+ diff,
3337
+ asset,
3338
+ agent,
3339
+ onAddChatReferences,
3340
+ onRequestAssetDelete
3341
+ }) {
3342
+ const editingCount = useRef(0);
3343
+ const onEditingChangeRef = useRef(onEditingChange);
3344
+ onEditingChangeRef.current = onEditingChange;
3345
+ const editingSignal = useMemo(
3346
+ () => ({
3347
+ inc: () => {
3348
+ if ((editingCount.current += 1) === 1) onEditingChangeRef.current?.(true);
3349
+ },
3350
+ dec: () => {
3351
+ editingCount.current = Math.max(0, editingCount.current - 1);
3352
+ if (editingCount.current === 0) onEditingChangeRef.current?.(false);
3353
+ }
3354
+ }),
3355
+ []
3356
+ );
3357
+ const [innerSelection, setInnerSelection] = useState(EMPTY_SELECTION);
3358
+ const selection = controlledSelection ?? innerSelection;
3359
+ const setSelection = useCallback(
3360
+ (next) => {
3361
+ setInnerSelection(next);
3362
+ onSelectionChange?.(next);
3363
+ },
3364
+ [onSelectionChange]
3365
+ );
3366
+ const [innerFormat, setInnerFormat] = useState("standard");
3367
+ const format = controlledFormat ?? innerFormat;
3368
+ const setFormat = useCallback(
3369
+ (next) => {
3370
+ setInnerFormat(next);
3371
+ onFormatChange?.(next);
3372
+ },
3373
+ [onFormatChange]
3374
+ );
3375
+ const changeSet = useMemo(
3376
+ () => computeChangeSet(diff?.baseline ?? null, value),
3377
+ [diff?.baseline, value]
3378
+ );
3379
+ const changedSceneKeys = useMemo(
3380
+ () => new Set(changeSet.changedScenes.keys()),
3381
+ [changeSet]
3382
+ );
3383
+ const changedSelections = useMemo(() => {
3384
+ if (changedSceneKeys.size === 0) return [];
3385
+ const out = [];
3386
+ (value.episodes ?? []).forEach((episode, ei) => {
3387
+ (episode.scenes ?? []).forEach((scene, si) => {
3388
+ if (changedSceneKeys.has(sceneChangeKey(ei, scene.scene_id))) {
3389
+ out.push({ kind: "scene", episodeIdx: ei, sceneIdx: si });
3390
+ }
3391
+ });
3392
+ });
3393
+ return out;
3394
+ }, [value, changedSceneKeys]);
3395
+ const canvasRef = useRef(null);
3396
+ const [diffCursor, setDiffCursor] = useState(0);
3397
+ const navDiff = useCallback(
3398
+ (dir) => {
3399
+ const list = changedSelections;
3400
+ if (list.length === 0) return;
3401
+ const next = (diffCursor + dir + list.length) % list.length;
3402
+ setDiffCursor(next);
3403
+ setSelection(withOrigin(list[next], "diff"));
3404
+ },
3405
+ [changedSelections, diffCursor, setSelection]
3406
+ );
3407
+ const contextLabel = buildContextLabel(value, selection);
3408
+ const returnTarget = resolveReturnTarget(value, selection);
3409
+ const rootClass = ["lj-se-root", dark ? "dark" : "", className].filter(Boolean).join(" ");
3410
+ const center = /* @__PURE__ */ jsx(
3411
+ ScriptStream,
3412
+ {
3413
+ value,
3414
+ format,
3415
+ disabled,
3416
+ selection,
3417
+ changedSceneKeys,
3418
+ onEdit,
3419
+ onSelectionChange: setSelection,
3420
+ onRequestAssetDelete
3421
+ }
3422
+ );
3423
+ return /* @__PURE__ */ jsx(EditingSignalContext.Provider, { value: editingSignal, children: /* @__PURE__ */ jsxs("div", { className: rootClass, children: [
3424
+ /* @__PURE__ */ jsx(
3425
+ StudioToc,
3426
+ {
3427
+ data: value,
3428
+ selection,
3429
+ onSelect: setSelection,
3430
+ onAddEpisode: disabled ? void 0 : () => onEdit((p) => addEpisode(p))
3431
+ }
3432
+ ),
3433
+ /* @__PURE__ */ jsxs("div", { className: "lj-se-main", children: [
3434
+ /* @__PURE__ */ jsx(
3435
+ CanvasTopBar,
3436
+ {
3437
+ contextLabel,
3438
+ format,
3439
+ onFormatChange: setFormat,
3440
+ canUndo,
3441
+ canRedo,
3442
+ onUndo,
3443
+ onRedo
3444
+ }
3445
+ ),
3446
+ /* @__PURE__ */ jsxs("div", { className: "lj-se-canvas", ref: canvasRef, children: [
3447
+ center,
3448
+ onAddChatReferences ? /* @__PURE__ */ jsx(QuoteLayer, { containerRef: canvasRef, onAddChatReferences }) : null,
3449
+ /* @__PURE__ */ jsx(
3450
+ SaveCapsule,
3451
+ {
3452
+ saveStatus,
3453
+ diffCount: diff ? changeSet.count : 0,
3454
+ canApplyDiff: !!(diff?.onAcceptAll || diff?.onRejectAll),
3455
+ onDiffPrev: changedSelections.length > 0 ? () => navDiff(-1) : void 0,
3456
+ onDiffNext: changedSelections.length > 0 ? () => navDiff(1) : void 0,
3457
+ onAcceptAll: diff?.onAcceptAll,
3458
+ onRejectAll: diff?.onRejectAll,
3459
+ returnLabel: returnTarget?.label,
3460
+ onReturn: returnTarget ? () => setSelection(returnTarget.selection) : void 0
3461
+ }
3462
+ )
3463
+ ] })
3464
+ ] })
3465
+ ] }) });
3466
+ }
3467
+ function buildContextLabel(value, selection) {
3468
+ if (selection.kind === "worldview") {
3469
+ return /* @__PURE__ */ jsx("span", { className: "lj-se-ctx-kicker", children: "\u4E16\u754C\u89C2 & \u98CE\u683C" });
3470
+ }
3471
+ if (selection.kind === "actor") {
3472
+ const a = getActorMap(value.actors ?? []).get(selection.actorId);
3473
+ return /* @__PURE__ */ jsx(Ctx, { kicker: "\u89D2\u8272", name: a ? getActorDisplayName(a) : selection.actorId });
3474
+ }
3475
+ if (selection.kind === "location") {
3476
+ const l = getLocationMap(value.locations ?? []).get(selection.locationId);
3477
+ return /* @__PURE__ */ jsx(Ctx, { kicker: "\u573A\u666F", name: l ? getLocationDisplayName(l) : selection.locationId });
3478
+ }
3479
+ if (selection.kind === "prop") {
3480
+ const p = getPropMap(value.props ?? []).get(selection.propId);
3481
+ return /* @__PURE__ */ jsx(Ctx, { kicker: "\u9053\u5177", name: p ? getPropDisplayName(p) : selection.propId });
3482
+ }
3483
+ const epIdx = selection.kind === "episode" || selection.kind === "scene" || selection.kind === "clip" ? selection.episodeIdx : null;
3484
+ if (epIdx != null) {
3485
+ const ep = value.episodes?.[epIdx];
3486
+ if (ep) {
3487
+ return /* @__PURE__ */ jsxs("span", { className: "lj-se-ctx", children: [
3488
+ /* @__PURE__ */ jsxs("span", { className: "lj-se-ctx-kicker", children: [
3489
+ "\u7B2C ",
3490
+ getEpisodeNumberLabel(ep, epIdx),
3491
+ " \u96C6"
3492
+ ] }),
3493
+ /* @__PURE__ */ jsxs("span", { className: "lj-se-ctx-meta", children: [
3494
+ ep.scenes?.length ?? 0,
3495
+ " \u573A"
3496
+ ] })
3497
+ ] });
3498
+ }
3499
+ }
3500
+ return /* @__PURE__ */ jsx("span", { className: "lj-se-ctx-kicker", children: "\u5267\u672C" });
3501
+ }
3502
+ function Ctx({ kicker, name }) {
3503
+ return /* @__PURE__ */ jsxs("span", { className: "lj-se-ctx", children: [
3504
+ /* @__PURE__ */ jsx("span", { className: "lj-se-ctx-kicker", children: kicker }),
3505
+ /* @__PURE__ */ jsx("span", { className: "lj-se-ctx-name", children: name })
3506
+ ] });
3507
+ }
3508
+ function resolveReturnTarget(value, selection) {
3509
+ const from = selection.kind === "episode" || selection.kind === "scene" || selection.kind === "clip" ? selection.from : void 0;
3510
+ if (!from) return null;
3511
+ let label;
3512
+ if (from.kind === "actor") {
3513
+ const a = getActorMap(value.actors ?? []).get(from.actorId);
3514
+ label = a ? getActorDisplayName(a) : from.actorId;
3515
+ } else if (from.kind === "location") {
3516
+ const l = getLocationMap(value.locations ?? []).get(from.locationId);
3517
+ label = l ? getLocationDisplayName(l) : from.locationId;
3518
+ } else {
3519
+ const p = getPropMap(value.props ?? []).get(from.propId);
3520
+ label = p ? getPropDisplayName(p) : from.propId;
3521
+ }
3522
+ return { label, selection: withOrigin(fromToSelection(from), "reference") };
3523
+ }
3524
+
3525
+ // src/mutations/index.ts
3526
+ var mutations_exports = {};
3527
+ __export(mutations_exports, {
3528
+ addBlankScene: () => addBlankScene,
3529
+ addEntityState: () => addEntityState,
3530
+ addEpisode: () => addEpisode,
3531
+ buildNewScriptItem: () => buildNewScriptItem,
3532
+ buildSceneDraft: () => buildSceneDraft,
3533
+ createItemFromDraft: () => createItemFromDraft,
3534
+ createSceneFromDraft: () => createSceneFromDraft,
3535
+ decrementSceneId: () => decrementSceneId,
3536
+ deleteEpisode: () => deleteEpisode,
3537
+ deleteScene: () => deleteScene,
3538
+ deleteSceneItem: () => deleteSceneItem,
3539
+ getPrimarySceneLocationId: () => getPrimarySceneLocationId,
3540
+ getSceneActorsText: () => getSceneActorsText,
3541
+ incrementSceneId: () => incrementSceneId,
3542
+ insertScene: () => insertScene,
3543
+ insertSceneItem: () => insertSceneItem,
3544
+ mergeSceneIntoPrev: () => mergeSceneIntoPrev,
3545
+ moveSceneItem: () => moveSceneItem,
3546
+ moveScriptItem: () => moveScriptItem,
3547
+ nextAvailableSceneId: () => nextAvailableSceneId,
3548
+ nextEpisodeSceneId: () => nextEpisodeSceneId,
3549
+ pickInheritedActorId: () => pickInheritedActorId,
3550
+ removeEntityState: () => removeEntityState,
3551
+ replaceSceneItem: () => replaceSceneItem,
3552
+ splitScene: () => splitScene,
3553
+ updateEntityDescription: () => updateEntityDescription,
3554
+ updateEntityName: () => updateEntityName,
3555
+ updateEntityStateField: () => updateEntityStateField,
3556
+ updateSceneActorState: () => updateSceneActorState,
3557
+ updateSceneField: () => updateSceneField,
3558
+ updateSceneItemActor: () => updateSceneItemActor,
3559
+ updateSceneItemContent: () => updateSceneItemContent,
3560
+ updateSceneItemEmotion: () => updateSceneItemEmotion,
3561
+ updateSceneItemType: () => updateSceneItemType,
3562
+ updateSceneLocationState: () => updateSceneLocationState,
3563
+ updateScenePropState: () => updateScenePropState
3564
+ });
3565
+
3566
+ export { DeleteConfirmButton, EMPTY_SELECTION, EditableText, Popover, ScriptContentType, ScriptEditor, SelectField, buildSelectionFrom, computeChangeSet, fromToSelection, getActorCueVar, getActorMap, getEpisodeTitle, getLocationMap, getPropMap, getSceneContext, getSelectionEpisodeIdx, getSpeakerMap, isClipSelected, isEpisodeOpen, isSameSelectionTarget, isSceneSelected, mutations_exports as mutations, sceneChangeKey, selectionTargetKey, withOrigin };
3567
+ //# sourceMappingURL=index.js.map
3568
+ //# sourceMappingURL=index.js.map