@lingjingai/script-editor 0.1.0 → 0.1.13

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 CHANGED
@@ -1,6 +1,6 @@
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';
1
+ import { createContext, memo, useState, useMemo, useCallback, Fragment, useRef, useContext, useEffect, useLayoutEffect, useDeferredValue } from 'react';
2
+ import { Plus, Trash2, X, 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 as Fragment$1 } from 'react/jsx-runtime';
4
4
  import { Virtuoso } from 'react-virtuoso';
5
5
  import { createPortal } from 'react-dom';
6
6
 
@@ -286,10 +286,10 @@ function getSceneContext(scene) {
286
286
  props: (scene.context?.props?.length ? scene.context.props : scene.props) ?? []
287
287
  };
288
288
  }
289
- function findState(states, stateId) {
289
+ function findState(states2, stateId) {
290
290
  const id = stateId?.trim();
291
- if (!id || !states) return void 0;
292
- return states.find((state) => (state.state_id || "").trim() === id);
291
+ if (!id || !states2) return void 0;
292
+ return states2.find((state) => (state.state_id || "").trim() === id);
293
293
  }
294
294
  var ZH_DIGIT_MAP = {
295
295
  "\u96F6": 0,
@@ -443,7 +443,7 @@ function GroupHeader({
443
443
  highlighted,
444
444
  onToggle
445
445
  }) {
446
- const inner = /* @__PURE__ */ jsxs(Fragment, { children: [
446
+ const inner = /* @__PURE__ */ jsxs(Fragment$1, { children: [
447
447
  onToggle ? /* @__PURE__ */ jsx(
448
448
  ChevronRight,
449
449
  {
@@ -591,7 +591,7 @@ function EntityGroup({
591
591
  }) {
592
592
  const [open, setOpen] = useState(true);
593
593
  const valid = items.filter((i) => i.id);
594
- return /* @__PURE__ */ jsxs(Fragment, { children: [
594
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [
595
595
  /* @__PURE__ */ jsx(GroupHeader, { icon: Icon, label, count: valid.length, open, highlighted, onToggle: () => setOpen((v) => !v) }),
596
596
  open ? valid.length === 0 ? /* @__PURE__ */ jsx("div", { className: "lj-se-toc-empty", children: emptyText }) : valid.map((i) => /* @__PURE__ */ jsxs(
597
597
  "button",
@@ -931,7 +931,11 @@ function updateSceneItemActor(data, episodeIndex, sceneIndex, itemIndex, actorId
931
931
  const actions = [...scene.actions || []];
932
932
  const item = actions[itemIndex];
933
933
  if (!item) return scene;
934
- actions[itemIndex] = { ...item, actor_id: actorId.trim() };
934
+ const next = { ...item, actor_id: actorId.trim() };
935
+ delete next.speaker_id;
936
+ delete next.speakers;
937
+ delete next.lines;
938
+ actions[itemIndex] = next;
935
939
  return { ...scene, actions };
936
940
  });
937
941
  }
@@ -1267,6 +1271,21 @@ function buildDistributionIndex(data, prev) {
1267
1271
  }
1268
1272
  return index;
1269
1273
  }
1274
+ var states = /* @__PURE__ */ new Map();
1275
+ function readScrollState(scope) {
1276
+ return scope ? states.get(scope) : void 0;
1277
+ }
1278
+ function useScrollStateMemo(scope, virtuosoRef) {
1279
+ useEffect(() => {
1280
+ if (!scope) return;
1281
+ const ref = virtuosoRef;
1282
+ return () => {
1283
+ ref.current?.getState((snapshot) => {
1284
+ states.set(scope, snapshot);
1285
+ });
1286
+ };
1287
+ }, [scope, virtuosoRef]);
1288
+ }
1270
1289
  function Popover({
1271
1290
  trigger,
1272
1291
  children,
@@ -1383,7 +1402,7 @@ function RowMenuPanel({
1383
1402
  sections
1384
1403
  }) {
1385
1404
  return /* @__PURE__ */ jsxs("div", { className: "lj-se-rowmenu-inner", children: [
1386
- typeSeg ? /* @__PURE__ */ jsxs(Fragment, { children: [
1405
+ typeSeg ? /* @__PURE__ */ jsxs(Fragment$1, { children: [
1387
1406
  /* @__PURE__ */ jsx("div", { className: "lj-se-rowmenu-cap", children: "\u6539\u4E3A" }),
1388
1407
  /* @__PURE__ */ jsx("div", { className: "lj-se-typeseg", children: TYPES.map((t) => {
1389
1408
  const Icon = t.icon;
@@ -1406,7 +1425,7 @@ function RowMenuPanel({
1406
1425
  );
1407
1426
  }) })
1408
1427
  ] }) : null,
1409
- sections.map((items, si) => /* @__PURE__ */ jsxs(Fragment$1, { children: [
1428
+ sections.map((items, si) => /* @__PURE__ */ jsxs(Fragment, { children: [
1410
1429
  si > 0 || typeSeg ? /* @__PURE__ */ jsx("div", { className: "lj-se-rowmenu-div" }) : null,
1411
1430
  items.map((item) => {
1412
1431
  const Icon = item.icon;
@@ -1568,20 +1587,20 @@ function SelectField({
1568
1587
  }
1569
1588
  );
1570
1589
  }
1571
- function stateOptions(states) {
1572
- return (states ?? []).map((s) => ({ value: (s.state_id ?? "").trim(), label: getStateDisplayName(s) })).filter((o) => o.value);
1590
+ function stateOptions(states2) {
1591
+ return (states2 ?? []).map((s) => ({ value: (s.state_id ?? "").trim(), label: getStateDisplayName(s) })).filter((o) => o.value);
1573
1592
  }
1574
1593
  function AssetStateChip({
1575
1594
  name,
1576
1595
  orphan,
1577
- states,
1596
+ states: states2,
1578
1597
  currentStateId,
1579
1598
  disabled,
1580
1599
  onChangeState,
1581
1600
  onDelete,
1582
1601
  onNavigate
1583
1602
  }) {
1584
- const opts = stateOptions(states);
1603
+ const opts = stateOptions(states2);
1585
1604
  const trimmed = (currentStateId ?? "").trim();
1586
1605
  return /* @__PURE__ */ jsxs("span", { className: cn("lj-se-chip", orphan && "is-orphan"), title: orphan ? `${name} \u4E0D\u5B58\u5728` : void 0, children: [
1587
1606
  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 }),
@@ -2118,10 +2137,10 @@ function formatStateChangeLabels(item, actorMap, locationMap, propMap) {
2118
2137
  const kind = change.target_kind?.trim() || "actor";
2119
2138
  const id = change.target_id?.trim() || "";
2120
2139
  if (!id) return "";
2121
- const states = kind === "location" ? locationMap.get(id)?.states : kind === "prop" ? propMap.get(id)?.states : actorMap.get(id)?.states;
2140
+ const states2 = kind === "location" ? locationMap.get(id)?.states : kind === "prop" ? propMap.get(id)?.states : actorMap.get(id)?.states;
2122
2141
  const name = kind === "location" ? getLocationDisplayName(locationMap.get(id)) || id : kind === "prop" ? getPropDisplayName(propMap.get(id)) || id : getActorDisplayName(actorMap.get(id)) || id;
2123
- const from = getStateDisplayName(findState(states, change.from_state_id)) || change.from_state_id?.trim() || "\u672A\u8BBE";
2124
- const to = getStateDisplayName(findState(states, change.to_state_id)) || change.to_state_id?.trim() || "\u672A\u8BBE";
2142
+ const from = getStateDisplayName(findState(states2, change.from_state_id)) || change.from_state_id?.trim() || "\u672A\u8BBE";
2143
+ const to = getStateDisplayName(findState(states2, change.to_state_id)) || change.to_state_id?.trim() || "\u672A\u8BBE";
2125
2144
  return `${name}\uFF1A${from} \u2192 ${to}`;
2126
2145
  }).filter(Boolean);
2127
2146
  if (item.transition_prompt?.process || item.transition_prompt?.contrast) {
@@ -2158,12 +2177,19 @@ function SceneItemRowImpl({
2158
2177
  const type = item.type ?? "dialogue" /* Dialogue */;
2159
2178
  const content = item.content ?? "";
2160
2179
  const actorId = item.actor_id?.trim() || "";
2161
- const actor = actorMap.get(actorId);
2162
- const actorName = actor ? getActorDisplayName(actor) : actorId || "\u672A\u77E5\u89D2\u8272";
2163
- const hasOrphanActor = !!actorId && !actor;
2164
2180
  const emotion = translateEmotionLabel(item.emotion);
2165
2181
  const stateLabels = formatStateChangeLabels(item, actorMap, locationMap, propMap);
2166
- const structured = type === "dialogue" /* Dialogue */ && (!!item.speaker_id || !!item.delivery || (item.speakers?.length ?? 0) > 0 || (item.lines?.length ?? 0) > 0);
2182
+ const multiSpeaker = type === "dialogue" /* Dialogue */ && (item.delivery === "overlap" || (item.lines?.length ?? 0) > 0 || (item.speakers?.length ?? 0) > 1);
2183
+ const resolvedActorId = actorId || (() => {
2184
+ const spkId = item.speaker_id?.trim() || item.speakers?.[0]?.speaker_id?.trim() || "";
2185
+ if (!spkId) return "";
2186
+ const spk = speakerMap.get(spkId);
2187
+ const kind = spk?.source_kind?.trim();
2188
+ if ((kind === "actor" || kind === "prop") && spk?.source_id?.trim()) return spk.source_id.trim();
2189
+ if (spkId.startsWith("spk_") && actorMap.has(spkId.slice(4))) return spkId.slice(4);
2190
+ return "";
2191
+ })();
2192
+ const resolvedActor = actorMap.get(resolvedActorId) ?? propMap.get(resolvedActorId);
2167
2193
  const handle = /* @__PURE__ */ jsx(
2168
2194
  RowControlHandle,
2169
2195
  {
@@ -2178,11 +2204,11 @@ function SceneItemRowImpl({
2178
2204
  onDelete
2179
2205
  }
2180
2206
  );
2181
- const actorCue = (tone) => structured ? /* @__PURE__ */ jsxs(
2207
+ const actorCue = (tone) => multiSpeaker ? /* @__PURE__ */ jsxs(
2182
2208
  "span",
2183
2209
  {
2184
2210
  className: "lj-se-cue lj-se-cue--structured",
2185
- style: !hasOrphanActor ? { color: getActorCueVar(actorId, tone) } : void 0,
2211
+ style: resolvedActor ? { color: getActorCueVar(resolvedActorId, tone) } : void 0,
2186
2212
  children: [
2187
2213
  getDialogueSpeakerLabel(item, actorMap, speakerMap) || "\u672A\u77E5\u53D1\u58F0\u6E90",
2188
2214
  getDialogueDeliveryLabel(item.delivery) ? /* @__PURE__ */ jsxs("span", { className: "lj-se-cue-delivery", children: [
@@ -2195,13 +2221,13 @@ function SceneItemRowImpl({
2195
2221
  ) : /* @__PURE__ */ jsx(
2196
2222
  SelectField,
2197
2223
  {
2198
- value: actorId,
2224
+ value: resolvedActorId,
2199
2225
  options: actorOptions,
2200
2226
  disabled,
2201
2227
  placeholder: "\u672A\u77E5\u89D2\u8272",
2202
- title: hasOrphanActor ? `\u89D2\u8272 ${actorName} \u4E0D\u5B58\u5728` : "\u5207\u6362\u89D2\u8272",
2203
- className: cn("lj-se-cue lj-se-cue--select", hasOrphanActor && "is-orphan"),
2204
- style: !hasOrphanActor ? { color: getActorCueVar(actorId, tone) } : void 0,
2228
+ title: "\u5207\u6362\u89D2\u8272",
2229
+ className: cn("lj-se-cue lj-se-cue--select", resolvedActorId && !resolvedActor && "is-orphan"),
2230
+ style: resolvedActor ? { color: getActorCueVar(resolvedActorId, tone) } : void 0,
2205
2231
  onChange: onSaveActor
2206
2232
  }
2207
2233
  );
@@ -2210,7 +2236,7 @@ function SceneItemRowImpl({
2210
2236
  if (type === "dialogue" /* Dialogue */) {
2211
2237
  body2 = /* @__PURE__ */ jsxs("div", { className: "lj-se-std-dialogue", children: [
2212
2238
  actorCue("dialogue"),
2213
- !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,
2239
+ /* @__PURE__ */ jsx("div", { className: "lj-se-std-emotion", children: /* @__PURE__ */ jsx(EditableText, { value: item.emotion ?? "", disabled, placeholder: "\u60C5\u7EEA\u2026", onSave: onSaveEmotion }) }),
2214
2240
  /* @__PURE__ */ jsx(
2215
2241
  EditableText,
2216
2242
  {
@@ -2276,19 +2302,19 @@ function SceneItemRowImpl({
2276
2302
  const tone = isInner ? "inner" : "dialogue";
2277
2303
  const tag = isInner ? emotion ? `OS\xB7${emotion}` : "OS" : emotion;
2278
2304
  body = /* @__PURE__ */ jsxs("p", { className: cn("lj-se-asian-cueline", isInner && "is-inner"), children: [
2279
- structured ? /* @__PURE__ */ jsx("strong", { className: "lj-se-asian-speaker", style: !hasOrphanActor ? { color: getActorCueVar(actorId, tone) } : void 0, children: getDialogueSpeakerLabel(item, actorMap, speakerMap) }) : /* @__PURE__ */ jsx(
2305
+ multiSpeaker ? /* @__PURE__ */ jsx("strong", { className: "lj-se-asian-speaker", style: resolvedActor ? { color: getActorCueVar(resolvedActorId, tone) } : void 0, children: getDialogueSpeakerLabel(item, actorMap, speakerMap) }) : /* @__PURE__ */ jsx(
2280
2306
  SelectField,
2281
2307
  {
2282
- value: actorId,
2308
+ value: resolvedActorId,
2283
2309
  options: actorOptions,
2284
2310
  disabled,
2285
2311
  placeholder: "\u672A\u77E5\u89D2\u8272",
2286
- className: cn("lj-se-asian-speaker lj-se-asian-speaker--select", hasOrphanActor && "is-orphan"),
2287
- style: !hasOrphanActor ? { color: getActorCueVar(actorId, tone) } : void 0,
2312
+ className: cn("lj-se-asian-speaker lj-se-asian-speaker--select", resolvedActorId && !resolvedActor && "is-orphan"),
2313
+ style: resolvedActor ? { color: getActorCueVar(resolvedActorId, tone) } : void 0,
2288
2314
  onChange: onSaveActor
2289
2315
  }
2290
2316
  ),
2291
- !structured ? /* @__PURE__ */ jsxs("span", { className: "lj-se-asian-tag", children: [
2317
+ !multiSpeaker ? /* @__PURE__ */ jsxs("span", { className: "lj-se-asian-tag", children: [
2292
2318
  "\uFF08",
2293
2319
  /* @__PURE__ */ jsx(EditableText, { value: item.emotion ?? "", disabled, placeholder: isInner ? "OS" : "\u60C5\u7EEA", onSave: onSaveEmotion }),
2294
2320
  "\uFF09"
@@ -2308,6 +2334,12 @@ function SceneItemRowImpl({
2308
2334
  ] });
2309
2335
  }
2310
2336
  var SceneItemRow = memo(SceneItemRowImpl);
2337
+ function DiffActionControls({ onAccept, onReject }) {
2338
+ return /* @__PURE__ */ jsxs("span", { className: "lj-se-actiondiff-ctrl", children: [
2339
+ /* @__PURE__ */ jsx("button", { type: "button", className: "lj-se-actiondiff-reject", title: "\u62D2\u7EDD\u8FD9\u884C", onClick: onReject, children: /* @__PURE__ */ jsx(X, { className: "lj-se-ic-3", strokeWidth: 2.75 }) }),
2340
+ /* @__PURE__ */ jsx("button", { type: "button", className: "lj-se-actiondiff-accept", title: "\u63A5\u53D7\u8FD9\u884C", onClick: onAccept, children: /* @__PURE__ */ jsx(Check, { className: "lj-se-ic-3", strokeWidth: 2.75 }) })
2341
+ ] });
2342
+ }
2311
2343
  function SceneEditorImpl({
2312
2344
  episodeIndex,
2313
2345
  sceneIndex,
@@ -2316,6 +2348,7 @@ function SceneEditorImpl({
2316
2348
  format,
2317
2349
  disabled,
2318
2350
  focused = false,
2351
+ actionDiff,
2319
2352
  onEdit,
2320
2353
  onNavigateAsset,
2321
2354
  onRequestAssetDelete
@@ -2323,10 +2356,18 @@ function SceneEditorImpl({
2323
2356
  const [pendingEdit, setPendingEdit] = useState(null);
2324
2357
  const { actorMap, speakerMap, locationMap, propMap } = maps;
2325
2358
  const actions = scene.actions ?? [];
2326
- const actorOptions = useMemo(
2327
- () => Array.from(actorMap.values()).map((a) => ({ value: (a.actor_id ?? "").trim(), label: getActorDisplayName(a) })).filter((o) => o.value),
2328
- [actorMap]
2329
- );
2359
+ const actorOptions = useMemo(() => {
2360
+ const out = [];
2361
+ for (const a of actorMap.values()) {
2362
+ const v = (a.actor_id ?? "").trim();
2363
+ if (v) out.push({ value: v, label: getActorDisplayName(a) });
2364
+ }
2365
+ for (const p of propMap.values()) {
2366
+ const v = (p.prop_id ?? "").trim();
2367
+ if (v) out.push({ value: v, label: `${getPropDisplayName(p)}\uFF08\u9053\u5177\uFF09` });
2368
+ }
2369
+ return out;
2370
+ }, [actorMap, propMap]);
2330
2371
  const insertAt = useCallback(
2331
2372
  (index, type) => {
2332
2373
  const inherited = pickInheritedActorId(scene, index);
@@ -2362,6 +2403,32 @@ function SceneEditorImpl({
2362
2403
  e.preventDefault();
2363
2404
  dnd?.drop();
2364
2405
  };
2406
+ const noop = () => {
2407
+ };
2408
+ const renderOldRow = (item) => /* @__PURE__ */ jsx(
2409
+ SceneItemRow,
2410
+ {
2411
+ item,
2412
+ actorMap,
2413
+ speakerMap,
2414
+ locationMap,
2415
+ propMap,
2416
+ actorOptions,
2417
+ format,
2418
+ disabled: true,
2419
+ onSave: noop,
2420
+ onSaveActor: noop,
2421
+ onSaveEmotion: noop,
2422
+ onChangeType: noop,
2423
+ onInsertAbove: noop,
2424
+ onInsertBelow: noop,
2425
+ onDelete: noop
2426
+ }
2427
+ );
2428
+ const renderGhosts = (beforeIdx) => actionDiff?.removedBefore.get(beforeIdx)?.map((m, gi) => /* @__PURE__ */ jsxs("div", { className: "lj-se-action-slot lj-se-diff-old lj-se-ghost-row", children: [
2429
+ /* @__PURE__ */ jsx(DiffActionControls, { onAccept: () => actionDiff.onAccept(m), onReject: () => actionDiff.onReject(m) }),
2430
+ m.item ? renderOldRow(m.item) : /* @__PURE__ */ jsx("p", { className: "lj-se-ghost-empty", children: "\uFF08\u5DF2\u5220\u9664\u7A7A\u884C\uFF09" })
2431
+ ] }, `ghost-${beforeIdx}-${gi}`)) ?? null;
2365
2432
  return /* @__PURE__ */ jsxs("div", { className: "lj-se-scene", children: [
2366
2433
  /* @__PURE__ */ jsx(
2367
2434
  SceneHeader,
@@ -2381,52 +2448,73 @@ function SceneEditorImpl({
2381
2448
  }
2382
2449
  ),
2383
2450
  /* @__PURE__ */ jsxs("div", { className: "lj-se-actions", children: [
2384
- actions.map((item, idx) => /* @__PURE__ */ jsx(
2385
- "div",
2386
- {
2387
- className: cn("lj-se-action-slot", dragIndex === idx && "is-dragging", dropSlot === idx && "drop-before"),
2388
- onDragOver: (e) => onSlotDragOver(e, idx),
2389
- onDrop: onSlotDrop,
2390
- children: /* @__PURE__ */ jsx(
2391
- SceneItemRow,
2451
+ actions.map((item, idx) => {
2452
+ const mark = actionDiff?.byIndex.get(idx);
2453
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
2454
+ renderGhosts(idx),
2455
+ /* @__PURE__ */ jsxs(
2456
+ "div",
2392
2457
  {
2393
- item,
2394
- actorMap,
2395
- speakerMap,
2396
- locationMap,
2397
- propMap,
2398
- actorOptions,
2399
- format,
2400
- disabled,
2401
- autoEdit: pendingEdit === idx,
2402
- onEditEnd: pendingEdit === idx ? clearPending : void 0,
2403
- canSplit: idx > 0,
2404
- dragHandleProps: disabled ? void 0 : { draggable: true, onDragStart: (e) => onDragStart(e, idx), onDragEnd: () => dnd?.end() },
2405
- onSave: (content) => {
2406
- clearPending();
2407
- onEdit((cur) => updateSceneItemContent(cur, episodeIndex, sceneIndex, idx, content));
2408
- },
2409
- onSaveActor: (actorId) => onEdit((cur) => updateSceneItemActor(cur, episodeIndex, sceneIndex, idx, actorId)),
2410
- onSaveEmotion: (emotion) => onEdit((cur) => updateSceneItemEmotion(cur, episodeIndex, sceneIndex, idx, emotion)),
2411
- onChangeType: (type) => {
2412
- clearPending();
2413
- onEdit((cur) => updateSceneItemType(cur, episodeIndex, sceneIndex, idx, type));
2414
- },
2415
- onInsertAbove: () => insertSame(idx, idx),
2416
- onInsertBelow: () => insertSame(idx + 1, idx),
2417
- onSplitHere: idx > 0 ? () => {
2418
- clearPending();
2419
- onEdit((cur) => splitScene(cur, episodeIndex, sceneIndex, idx));
2420
- } : void 0,
2421
- onDelete: () => {
2422
- clearPending();
2423
- onEdit((cur) => deleteSceneItem(cur, episodeIndex, sceneIndex, idx));
2424
- }
2458
+ className: cn(
2459
+ "lj-se-action-slot",
2460
+ dragIndex === idx && "is-dragging",
2461
+ dropSlot === idx && "drop-before",
2462
+ mark && (mark.kind === "added" ? "is-diff-added" : "is-diff-modified")
2463
+ ),
2464
+ onDragOver: (e) => onSlotDragOver(e, idx),
2465
+ onDrop: onSlotDrop,
2466
+ children: [
2467
+ mark ? /* @__PURE__ */ jsx(
2468
+ DiffActionControls,
2469
+ {
2470
+ onAccept: () => actionDiff.onAccept(mark),
2471
+ onReject: () => actionDiff.onReject(mark)
2472
+ }
2473
+ ) : null,
2474
+ mark?.kind === "modified" && mark.item ? /* @__PURE__ */ jsx("div", { className: "lj-se-diff-old", children: renderOldRow(mark.item) }) : null,
2475
+ /* @__PURE__ */ jsx(
2476
+ SceneItemRow,
2477
+ {
2478
+ item,
2479
+ actorMap,
2480
+ speakerMap,
2481
+ locationMap,
2482
+ propMap,
2483
+ actorOptions,
2484
+ format,
2485
+ disabled,
2486
+ autoEdit: pendingEdit === idx,
2487
+ onEditEnd: pendingEdit === idx ? clearPending : void 0,
2488
+ canSplit: idx > 0,
2489
+ dragHandleProps: disabled ? void 0 : { draggable: true, onDragStart: (e) => onDragStart(e, idx), onDragEnd: () => dnd?.end() },
2490
+ onSave: (content) => {
2491
+ clearPending();
2492
+ onEdit((cur) => updateSceneItemContent(cur, episodeIndex, sceneIndex, idx, content));
2493
+ },
2494
+ onSaveActor: (actorId) => onEdit((cur) => updateSceneItemActor(cur, episodeIndex, sceneIndex, idx, actorId)),
2495
+ onSaveEmotion: (emotion) => onEdit((cur) => updateSceneItemEmotion(cur, episodeIndex, sceneIndex, idx, emotion)),
2496
+ onChangeType: (type) => {
2497
+ clearPending();
2498
+ onEdit((cur) => updateSceneItemType(cur, episodeIndex, sceneIndex, idx, type));
2499
+ },
2500
+ onInsertAbove: () => insertSame(idx, idx),
2501
+ onInsertBelow: () => insertSame(idx + 1, idx),
2502
+ onSplitHere: idx > 0 ? () => {
2503
+ clearPending();
2504
+ onEdit((cur) => splitScene(cur, episodeIndex, sceneIndex, idx));
2505
+ } : void 0,
2506
+ onDelete: () => {
2507
+ clearPending();
2508
+ onEdit((cur) => deleteSceneItem(cur, episodeIndex, sceneIndex, idx));
2509
+ }
2510
+ }
2511
+ )
2512
+ ]
2425
2513
  }
2426
2514
  )
2427
- },
2428
- `item-${idx}`
2429
- )),
2515
+ ] }, `item-${idx}`);
2516
+ }),
2517
+ renderGhosts(actions.length),
2430
2518
  dropSlot === actions.length && actions.length > 0 ? /* @__PURE__ */ jsx("div", { className: "lj-se-dropline-end" }) : null,
2431
2519
  actions.length === 0 && !disabled ? /* @__PURE__ */ jsxs(
2432
2520
  "button",
@@ -2804,7 +2892,7 @@ function AssetCardImpl({
2804
2892
  const name = group === "actors" ? getActorDisplayName(entity) : group === "locations" ? getLocationDisplayName(entity) : getPropDisplayName(entity);
2805
2893
  const rawDescription = entity.description ?? "";
2806
2894
  const description = rawDescription.trim();
2807
- const states = entity.states ?? [];
2895
+ const states2 = entity.states ?? [];
2808
2896
  const fromRef = buildSelectionFrom(kind, assetId);
2809
2897
  return /* @__PURE__ */ jsx("div", { className: "lj-se-assetcard-row", children: /* @__PURE__ */ jsxs("article", { className: "lj-se-assetcard", "data-no-quote-selection": true, children: [
2810
2898
  /* @__PURE__ */ jsxs("div", { className: "lj-se-assetcard-head", children: [
@@ -2852,7 +2940,7 @@ function AssetCardImpl({
2852
2940
  /* @__PURE__ */ jsxs("div", { className: "lj-se-assetcard-states-head", children: [
2853
2941
  /* @__PURE__ */ jsxs("span", { className: "lj-se-assetcard-states-label", children: [
2854
2942
  "\u72B6\u6001 ",
2855
- /* @__PURE__ */ jsx("span", { className: "lj-se-field-hint", children: states.length })
2943
+ /* @__PURE__ */ jsx("span", { className: "lj-se-field-hint", children: states2.length })
2856
2944
  ] }),
2857
2945
  !disabled ? /* @__PURE__ */ jsxs(
2858
2946
  "button",
@@ -2867,7 +2955,7 @@ function AssetCardImpl({
2867
2955
  }
2868
2956
  ) : null
2869
2957
  ] }),
2870
- states.length > 0 ? /* @__PURE__ */ jsx("div", { className: "lj-se-assetcard-state-cards", children: states.map((state, idx) => {
2958
+ states2.length > 0 ? /* @__PURE__ */ jsx("div", { className: "lj-se-assetcard-state-cards", children: states2.map((state, idx) => {
2871
2959
  const sid = (state.state_id ?? "").trim() || `idx-${idx}`;
2872
2960
  return /* @__PURE__ */ jsxs("div", { className: "lj-se-assetcard-state", children: [
2873
2961
  /* @__PURE__ */ jsxs("div", { className: "lj-se-assetcard-state-top", children: [
@@ -2922,6 +3010,10 @@ function ScriptStream({
2922
3010
  selection,
2923
3011
  changedSceneKeys,
2924
3012
  sceneDiff,
3013
+ actionDiffByScene,
3014
+ onAcceptAction,
3015
+ onRejectAction,
3016
+ scrollScopeKey,
2925
3017
  onEdit,
2926
3018
  onSelectionChange,
2927
3019
  onRequestAssetDelete
@@ -2944,6 +3036,7 @@ function ScriptStream({
2944
3036
  return next;
2945
3037
  }, [deferredValue]);
2946
3038
  const handleRef = useRef(null);
3039
+ useScrollStateMemo(scrollScopeKey, handleRef);
2947
3040
  const programmaticUntil = useRef(0);
2948
3041
  const lastEmitted = useRef(-1);
2949
3042
  const lastScrolledKey = useRef("");
@@ -3003,8 +3096,8 @@ function ScriptStream({
3003
3096
  const idx = selectionToFlatIndex(stream, selection);
3004
3097
  if (idx == null) return;
3005
3098
  lastScrolledKey.current = key;
3006
- programmaticUntil.current = Date.now() + 700;
3007
- handleRef.current?.scrollToIndex({ index: idx, align: "start", behavior: "smooth" });
3099
+ programmaticUntil.current = Date.now() + 200;
3100
+ handleRef.current?.scrollToIndex({ index: idx, align: "start", behavior: "auto" });
3008
3101
  }, [selection, stream]);
3009
3102
  const emitScrollSelection = (topIndex) => {
3010
3103
  if (Date.now() < programmaticUntil.current) return;
@@ -3021,6 +3114,7 @@ function ScriptStream({
3021
3114
  ref: handleRef,
3022
3115
  className: "lj-se-stream lj-se-scroll",
3023
3116
  data: stream.items,
3117
+ restoreStateFrom: readScrollState(scrollScopeKey),
3024
3118
  increaseViewportBy: 1500,
3025
3119
  rangeChanged: (range) => emitScrollSelection(range.startIndex),
3026
3120
  itemContent: (_index, item) => {
@@ -3064,19 +3158,26 @@ function ScriptStream({
3064
3158
  if (!scene) return null;
3065
3159
  const focused = selection.kind === "scene" && selection.episodeIdx === item.episodeIdx && selection.sceneIdx === item.sceneIdx;
3066
3160
  const diffKey = `${item.episodeIdx}:${item.sceneId}`;
3067
- const changed = changedSceneKeys?.has(diffKey) ?? false;
3068
- const diffKind = !disabled ? sceneDiff?.kindByKey.get(diffKey) : void 0;
3069
- return /* @__PURE__ */ jsx("div", { className: "lj-se-stream-row", "data-quote-source": true, children: /* @__PURE__ */ jsxs("div", { className: cn("lj-se-paper-seg", paperEdge(item.key), changed && "is-changed"), children: [
3070
- diffKind ? /* @__PURE__ */ jsxs("div", { className: "lj-se-scenediff", children: [
3071
- /* @__PURE__ */ jsx("span", { className: cn("lj-se-scenediff-tag", diffKind === "added" && "is-added"), children: diffKind === "added" ? "AI \u65B0\u589E" : "AI \u6539\u52A8" }),
3161
+ const inDiff = !disabled && (changedSceneKeys?.has(diffKey) ?? false);
3162
+ const wholeKind = !disabled ? sceneDiff?.kindByKey.get(diffKey) : void 0;
3163
+ const sceneActions = !disabled ? actionDiffByScene?.get(diffKey) : void 0;
3164
+ const actionDiff = sceneActions && onAcceptAction && onRejectAction ? {
3165
+ byIndex: sceneActions.byIndex,
3166
+ removedBefore: sceneActions.removedBefore,
3167
+ onAccept: onAcceptAction,
3168
+ onReject: onRejectAction
3169
+ } : void 0;
3170
+ return /* @__PURE__ */ jsx("div", { className: "lj-se-stream-row", "data-quote-source": true, children: /* @__PURE__ */ jsxs("div", { className: cn("lj-se-paper-seg", paperEdge(item.key), wholeKind && "is-changed"), children: [
3171
+ sceneDiff && inDiff ? /* @__PURE__ */ jsxs("div", { className: "lj-se-scenediff", children: [
3172
+ /* @__PURE__ */ jsx("span", { className: cn("lj-se-scenediff-tag", wholeKind === "added" && "is-added"), children: wholeKind === "added" ? "AI \u65B0\u589E" : "AI \u6539\u52A8" }),
3072
3173
  /* @__PURE__ */ jsx("span", { className: "lj-se-scenediff-spacer" }),
3073
3174
  /* @__PURE__ */ jsx(
3074
3175
  "button",
3075
3176
  {
3076
3177
  type: "button",
3077
3178
  className: "lj-se-scenediff-reject",
3078
- onClick: () => sceneDiff.onReject(item.episodeIdx, item.sceneIdx, item.sceneId),
3079
- children: "\u62D2\u7EDD"
3179
+ onClick: () => sceneDiff.onReject(item.episodeIdx, item.sceneId),
3180
+ children: "\u6574\u573A\u62D2\u7EDD"
3080
3181
  }
3081
3182
  ),
3082
3183
  /* @__PURE__ */ jsx(
@@ -3085,13 +3186,14 @@ function ScriptStream({
3085
3186
  type: "button",
3086
3187
  className: "lj-se-scenediff-accept",
3087
3188
  onClick: () => sceneDiff.onAccept(item.episodeIdx, item.sceneId),
3088
- children: "\u63A5\u53D7"
3189
+ children: "\u6574\u573A\u63A5\u53D7"
3089
3190
  }
3090
3191
  )
3091
3192
  ] }) : null,
3092
3193
  /* @__PURE__ */ jsx(
3093
3194
  SceneEditor,
3094
3195
  {
3196
+ actionDiff,
3095
3197
  episodeIndex: item.episodeIdx,
3096
3198
  sceneIndex: item.sceneIdx,
3097
3199
  scene,
@@ -3168,7 +3270,7 @@ function EpisodeBanner({
3168
3270
  onEdit
3169
3271
  }) {
3170
3272
  return /* @__PURE__ */ jsxs("div", { className: "lj-se-epbanner", children: [
3171
- projectTitle !== void 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
3273
+ projectTitle !== void 0 ? /* @__PURE__ */ jsxs(Fragment$1, { children: [
3172
3274
  /* @__PURE__ */ jsx("div", { className: "lj-se-epbanner-kicker", children: "\u5267\u672C" }),
3173
3275
  /* @__PURE__ */ jsx("h1", { className: "lj-se-epbanner-script", children: /* @__PURE__ */ jsx(
3174
3276
  EditableText,
@@ -3267,6 +3369,21 @@ function computeChangeSet(baseline, current) {
3267
3369
  const count = (title ? 1 : 0) + (worldview ? 1 : 0) + (style ? 1 : 0) + changedScenes.size + removedCount;
3268
3370
  return { title, worldview, style, changedScenes, removedCount, count };
3269
3371
  }
3372
+ function changeSetToDiffScenes(changeSet) {
3373
+ const out = [];
3374
+ for (const [key, kind] of changeSet.changedScenes) {
3375
+ const i = key.indexOf(":");
3376
+ if (i < 0) continue;
3377
+ const episodeIdx = Number(key.slice(0, i));
3378
+ const sceneId = key.slice(i + 1);
3379
+ if (Number.isFinite(episodeIdx)) out.push({ episodeIdx, sceneId, kind });
3380
+ }
3381
+ return out;
3382
+ }
3383
+ function buildDiffFromBaseline(baseline, current) {
3384
+ const cs = computeChangeSet(baseline, current);
3385
+ return { scenes: changeSetToDiffScenes(cs), removedCount: cs.removedCount, count: cs.count };
3386
+ }
3270
3387
  function acceptSceneIntoBaseline(baseline, episodeId, episodeIdx, scene) {
3271
3388
  const episodes = (baseline.episodes ?? []).slice();
3272
3389
  const id = episodeId?.trim();
@@ -3304,6 +3421,170 @@ function findBaselineScene(baseline, episodeId, episodeIdx, sceneId) {
3304
3421
  if (!sid) return null;
3305
3422
  return (ep.scenes ?? []).find((s) => (s.scene_id ?? "").trim() === sid) ?? null;
3306
3423
  }
3424
+ function actionSig(a) {
3425
+ return [a.type ?? "", (a.content ?? "").trim(), a.actor_id ?? "", a.emotion ?? "", a.delivery ?? ""].join("\0");
3426
+ }
3427
+ function computeSceneActionDiff(baseline, current) {
3428
+ const m = baseline.length;
3429
+ const n = current.length;
3430
+ const bs = baseline.map(actionSig);
3431
+ const cs = current.map(actionSig);
3432
+ const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
3433
+ for (let i2 = m - 1; i2 >= 0; i2--) {
3434
+ for (let j2 = n - 1; j2 >= 0; j2--) {
3435
+ dp[i2][j2] = bs[i2] === cs[j2] ? dp[i2 + 1][j2 + 1] + 1 : Math.max(dp[i2 + 1][j2], dp[i2][j2 + 1]);
3436
+ }
3437
+ }
3438
+ const ops = [];
3439
+ let i = 0;
3440
+ let j = 0;
3441
+ while (i < m && j < n) {
3442
+ if (bs[i] === cs[j]) {
3443
+ ops.push({ t: "match", j });
3444
+ i++;
3445
+ j++;
3446
+ } else if (dp[i + 1][j] >= dp[i][j + 1]) {
3447
+ ops.push({ t: "del", i });
3448
+ i++;
3449
+ } else {
3450
+ ops.push({ t: "ins", j });
3451
+ j++;
3452
+ }
3453
+ }
3454
+ while (i < m) {
3455
+ ops.push({ t: "del", i });
3456
+ i++;
3457
+ }
3458
+ while (j < n) {
3459
+ ops.push({ t: "ins", j });
3460
+ j++;
3461
+ }
3462
+ const changed = [];
3463
+ const removed = [];
3464
+ let curJ = 0;
3465
+ let p = 0;
3466
+ while (p < ops.length) {
3467
+ const op = ops[p];
3468
+ if (op.t === "match") {
3469
+ curJ = op.j + 1;
3470
+ p++;
3471
+ continue;
3472
+ }
3473
+ if (op.t === "ins") {
3474
+ changed.push({ itemIdx: op.j, kind: "added" });
3475
+ curJ = op.j + 1;
3476
+ p++;
3477
+ continue;
3478
+ }
3479
+ const dels = [];
3480
+ while (p < ops.length && ops[p].t === "del") {
3481
+ dels.push(ops[p].i);
3482
+ p++;
3483
+ }
3484
+ const inss = [];
3485
+ while (p < ops.length && ops[p].t === "ins") {
3486
+ inss.push(ops[p].j);
3487
+ p++;
3488
+ }
3489
+ const pair = Math.min(dels.length, inss.length);
3490
+ for (let q = 0; q < pair; q++) changed.push({ itemIdx: inss[q], kind: "modified", baselineItemIdx: dels[q] });
3491
+ for (let q = pair; q < inss.length; q++) changed.push({ itemIdx: inss[q], kind: "added" });
3492
+ const ghostBefore = inss.length ? inss[inss.length - 1] + 1 : curJ;
3493
+ for (let q = pair; q < dels.length; q++) removed.push({ beforeItemIdx: ghostBefore, baselineItemIdx: dels[q] });
3494
+ if (inss.length) curJ = inss[inss.length - 1] + 1;
3495
+ }
3496
+ return { changed, removed };
3497
+ }
3498
+ function applyActionAccept(baselineScene, currentScene, mark) {
3499
+ const bActions = (baselineScene.actions ?? []).slice();
3500
+ const cActions = currentScene.actions ?? [];
3501
+ if (mark.kind === "modified" && mark.baselineItemIdx != null) {
3502
+ const item = cActions[mark.itemIdx];
3503
+ if (item && mark.baselineItemIdx < bActions.length) bActions[mark.baselineItemIdx] = item;
3504
+ } else if (mark.kind === "added") {
3505
+ const item = cActions[mark.itemIdx];
3506
+ if (item) bActions.splice(Math.min(mark.itemIdx, bActions.length), 0, item);
3507
+ } else if (mark.kind === "removed" && mark.baselineItemIdx != null) {
3508
+ if (mark.baselineItemIdx < bActions.length) bActions.splice(mark.baselineItemIdx, 1);
3509
+ }
3510
+ return { ...baselineScene, actions: bActions };
3511
+ }
3512
+ function applyActionReject(currentScene, baselineScene, mark) {
3513
+ const cActions = (currentScene.actions ?? []).slice();
3514
+ const bActions = baselineScene?.actions ?? [];
3515
+ if (mark.kind === "modified" && mark.baselineItemIdx != null) {
3516
+ const item = bActions[mark.baselineItemIdx];
3517
+ if (item && mark.itemIdx < cActions.length) cActions[mark.itemIdx] = item;
3518
+ } else if (mark.kind === "added") {
3519
+ if (mark.itemIdx < cActions.length) cActions.splice(mark.itemIdx, 1);
3520
+ } else if (mark.kind === "removed") {
3521
+ const item = mark.item ?? (mark.baselineItemIdx != null ? bActions[mark.baselineItemIdx] : void 0);
3522
+ if (item) cActions.splice(Math.min(mark.itemIdx, cActions.length), 0, item);
3523
+ }
3524
+ return { ...currentScene, actions: cActions };
3525
+ }
3526
+ function acceptActionIntoBaseline(baseline, current, mark) {
3527
+ const cEp = current.episodes?.[mark.episodeIdx];
3528
+ const curScene = cEp?.scenes?.find((s) => (s.scene_id ?? "").trim() === mark.sceneId.trim());
3529
+ const baseScene = findBaselineScene(baseline, cEp?.episode_id, mark.episodeIdx, mark.sceneId);
3530
+ if (!baseScene) {
3531
+ return curScene ? acceptSceneIntoBaseline(baseline, cEp?.episode_id, mark.episodeIdx, curScene) : baseline;
3532
+ }
3533
+ const next = applyActionAccept(baseScene, curScene ?? baseScene, mark);
3534
+ return acceptSceneIntoBaseline(baseline, cEp?.episode_id, mark.episodeIdx, next);
3535
+ }
3536
+ function rejectActionToBaseline(current, baseline, mark) {
3537
+ const cEp = current.episodes?.[mark.episodeIdx];
3538
+ const sceneIdx = cEp?.scenes?.findIndex((s) => (s.scene_id ?? "").trim() === mark.sceneId.trim()) ?? -1;
3539
+ if (sceneIdx < 0 || !cEp?.scenes) return current;
3540
+ const curScene = cEp.scenes[sceneIdx];
3541
+ const baseScene = findBaselineScene(baseline, cEp.episode_id, mark.episodeIdx, mark.sceneId);
3542
+ const next = applyActionReject(curScene, baseScene, mark);
3543
+ return rejectSceneToBaseline(current, mark.episodeIdx, sceneIdx, next);
3544
+ }
3545
+ function buildActionDiff(baseline, current) {
3546
+ const cs = computeChangeSet(baseline, current);
3547
+ const scenes = [];
3548
+ const actions = [];
3549
+ for (const [key, kind] of cs.changedScenes) {
3550
+ const ci = key.indexOf(":");
3551
+ const episodeIdx = Number(key.slice(0, ci));
3552
+ const sceneId = key.slice(ci + 1);
3553
+ if (!Number.isFinite(episodeIdx)) continue;
3554
+ if (kind === "added") {
3555
+ scenes.push({ episodeIdx, sceneId, kind: "added" });
3556
+ continue;
3557
+ }
3558
+ const ep = current.episodes?.[episodeIdx];
3559
+ const curScene = ep?.scenes?.find((s) => (s.scene_id ?? "").trim() === sceneId);
3560
+ const baseScene = findBaselineScene(baseline, ep?.episode_id, episodeIdx, sceneId);
3561
+ if (!curScene) continue;
3562
+ const sd = computeSceneActionDiff(baseScene?.actions ?? [], curScene.actions ?? []);
3563
+ for (const c of sd.changed) {
3564
+ actions.push({
3565
+ episodeIdx,
3566
+ sceneId,
3567
+ kind: c.kind,
3568
+ itemIdx: c.itemIdx,
3569
+ baselineItemIdx: c.baselineItemIdx,
3570
+ // modified carries the OLD baseline action so the editor can render the
3571
+ // deleted line above the new one (unified-diff view)
3572
+ item: c.kind === "modified" && c.baselineItemIdx != null ? baseScene?.actions?.[c.baselineItemIdx] : void 0
3573
+ });
3574
+ }
3575
+ for (const r of sd.removed) {
3576
+ actions.push({
3577
+ episodeIdx,
3578
+ sceneId,
3579
+ kind: "removed",
3580
+ itemIdx: r.beforeItemIdx,
3581
+ baselineItemIdx: r.baselineItemIdx,
3582
+ item: baseScene?.actions?.[r.baselineItemIdx]
3583
+ });
3584
+ }
3585
+ }
3586
+ return { scenes, actions, removedCount: cs.removedCount, count: scenes.length + actions.length + cs.removedCount };
3587
+ }
3307
3588
 
3308
3589
  // src/reference.ts
3309
3590
  function asNumber(value) {
@@ -3433,6 +3714,7 @@ function ScriptEditor({
3433
3714
  onRedo,
3434
3715
  onEditingChange,
3435
3716
  referenceFocusRequest,
3717
+ scrollScopeKey,
3436
3718
  diff,
3437
3719
  asset,
3438
3720
  onAddChatReferences,
@@ -3453,6 +3735,31 @@ function ScriptEditor({
3453
3735
  }),
3454
3736
  []
3455
3737
  );
3738
+ const undoKeysRef = useRef({ onUndo, onRedo, canUndo, canRedo });
3739
+ undoKeysRef.current = { onUndo, onRedo, canUndo, canRedo };
3740
+ useEffect(() => {
3741
+ if (disabled) return;
3742
+ const onKeyDown = (e) => {
3743
+ if (!(e.metaKey || e.ctrlKey)) return;
3744
+ const t = e.target;
3745
+ if (t instanceof HTMLElement && (t.tagName === "INPUT" || t.tagName === "TEXTAREA" || t.tagName === "SELECT" || t.isContentEditable)) {
3746
+ return;
3747
+ }
3748
+ const key = e.key.toLowerCase();
3749
+ const { onUndo: u, onRedo: r, canUndo: cu, canRedo: cr } = undoKeysRef.current;
3750
+ if (key === "z" && !e.shiftKey) {
3751
+ if (!u || !cu) return;
3752
+ e.preventDefault();
3753
+ u();
3754
+ } else if (key === "z" && e.shiftKey || key === "y") {
3755
+ if (!r || !cr) return;
3756
+ e.preventDefault();
3757
+ r();
3758
+ }
3759
+ };
3760
+ document.addEventListener("keydown", onKeyDown);
3761
+ return () => document.removeEventListener("keydown", onKeyDown);
3762
+ }, [disabled]);
3456
3763
  const [innerSelection, setInnerSelection] = useState(EMPTY_SELECTION);
3457
3764
  const selection = controlledSelection ?? innerSelection;
3458
3765
  const setSelection = useCallback(
@@ -3471,14 +3778,36 @@ function ScriptEditor({
3471
3778
  },
3472
3779
  [onFormatChange]
3473
3780
  );
3474
- const changeSet = useMemo(
3475
- () => computeChangeSet(diff?.baseline ?? null, value),
3476
- [diff?.baseline, value]
3477
- );
3478
- const changedSceneKeys = useMemo(
3479
- () => new Set(changeSet.changedScenes.keys()),
3480
- [changeSet]
3481
- );
3781
+ const sceneKindByKey = useMemo(() => {
3782
+ const m = /* @__PURE__ */ new Map();
3783
+ for (const s of diff?.scenes ?? []) m.set(sceneChangeKey(s.episodeIdx, s.sceneId), s.kind);
3784
+ return m;
3785
+ }, [diff?.scenes]);
3786
+ const actionDiffByScene = useMemo(() => {
3787
+ const map = /* @__PURE__ */ new Map();
3788
+ for (const m of diff?.actions ?? []) {
3789
+ const key = sceneChangeKey(m.episodeIdx, m.sceneId);
3790
+ let e = map.get(key);
3791
+ if (!e) {
3792
+ e = { byIndex: /* @__PURE__ */ new Map(), removedBefore: /* @__PURE__ */ new Map() };
3793
+ map.set(key, e);
3794
+ }
3795
+ if (m.kind === "removed") {
3796
+ const arr = e.removedBefore.get(m.itemIdx) ?? [];
3797
+ arr.push(m);
3798
+ e.removedBefore.set(m.itemIdx, arr);
3799
+ } else {
3800
+ e.byIndex.set(m.itemIdx, m);
3801
+ }
3802
+ }
3803
+ return map;
3804
+ }, [diff?.actions]);
3805
+ const changedSceneKeys = useMemo(() => {
3806
+ const s = new Set(sceneKindByKey.keys());
3807
+ for (const k of actionDiffByScene.keys()) s.add(k);
3808
+ return s;
3809
+ }, [sceneKindByKey, actionDiffByScene]);
3810
+ const diffCount = diff ? diff.count ?? diff.scenes.length + (diff.actions?.length ?? 0) + (diff.removedCount ?? 0) : 0;
3482
3811
  const changedSelections = useMemo(() => {
3483
3812
  if (changedSceneKeys.size === 0) return [];
3484
3813
  const out = [];
@@ -3503,28 +3832,15 @@ function ScriptEditor({
3503
3832
  },
3504
3833
  [changedSelections, diffCursor, setSelection]
3505
3834
  );
3506
- const onAcceptScene = useCallback(
3507
- (episodeIdx, sceneId) => {
3508
- if (!diff?.onPatchBaseline) return;
3509
- const ep = value.episodes?.[episodeIdx];
3510
- const scene = ep?.scenes?.find((s) => (s.scene_id ?? "").trim() === sceneId.trim());
3511
- if (!scene) return;
3512
- diff.onPatchBaseline((prev) => acceptSceneIntoBaseline(prev, ep?.episode_id, episodeIdx, scene));
3513
- },
3514
- [diff, value]
3515
- );
3516
- const onRejectScene = useCallback(
3517
- (episodeIdx, sceneIdx, sceneId) => {
3518
- const ep = value.episodes?.[episodeIdx];
3519
- const base = findBaselineScene(diff?.baseline ?? null, ep?.episode_id, episodeIdx, sceneId);
3520
- onEdit((cur) => rejectSceneToBaseline(cur, episodeIdx, sceneIdx, base));
3521
- },
3522
- [diff, value, onEdit]
3523
- );
3524
- const sceneDiff = useMemo(
3525
- () => diff?.onPatchBaseline && changeSet.changedScenes.size > 0 ? { kindByKey: changeSet.changedScenes, onAccept: onAcceptScene, onReject: onRejectScene } : void 0,
3526
- [diff?.onPatchBaseline, changeSet, onAcceptScene, onRejectScene]
3527
- );
3835
+ const sceneDiff = useMemo(() => {
3836
+ if (!diff || !diff.onAcceptScene && !diff.onRejectScene || changedSceneKeys.size === 0) return void 0;
3837
+ const kindAt = (episodeIdx, sceneId) => sceneKindByKey.get(sceneChangeKey(episodeIdx, sceneId)) ?? "modified";
3838
+ return {
3839
+ kindByKey: sceneKindByKey,
3840
+ onAccept: (episodeIdx, sceneId) => diff.onAcceptScene?.({ episodeIdx, sceneId, kind: kindAt(episodeIdx, sceneId) }),
3841
+ onReject: (episodeIdx, sceneId) => diff.onRejectScene?.({ episodeIdx, sceneId, kind: kindAt(episodeIdx, sceneId) })
3842
+ };
3843
+ }, [diff, sceneKindByKey, changedSceneKeys]);
3528
3844
  useEffect(() => {
3529
3845
  if (!referenceFocusRequest) return;
3530
3846
  const sel = selectionFromScriptReference(referenceFocusRequest.reference);
@@ -3542,6 +3858,10 @@ function ScriptEditor({
3542
3858
  selection,
3543
3859
  changedSceneKeys,
3544
3860
  sceneDiff,
3861
+ actionDiffByScene,
3862
+ onAcceptAction: diff?.onAcceptAction,
3863
+ onRejectAction: diff?.onRejectAction,
3864
+ scrollScopeKey,
3545
3865
  onEdit,
3546
3866
  onSelectionChange: setSelection,
3547
3867
  onRequestAssetDelete
@@ -3577,7 +3897,7 @@ function ScriptEditor({
3577
3897
  SaveCapsule,
3578
3898
  {
3579
3899
  saveStatus,
3580
- diffCount: diff ? changeSet.count : 0,
3900
+ diffCount,
3581
3901
  baselineStatus: diff?.baselineStatus,
3582
3902
  canApplyDiff: !!(diff?.onAcceptAll || diff?.onRejectAll),
3583
3903
  onDiffPrev: changedSelections.length > 0 ? () => navDiff(-1) : void 0,
@@ -3691,6 +4011,6 @@ __export(mutations_exports, {
3691
4011
  updateScenePropState: () => updateScenePropState
3692
4012
  });
3693
4013
 
3694
- 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, selectionFromScriptReference, selectionTargetKey, withOrigin };
4014
+ export { DeleteConfirmButton, EMPTY_SELECTION, EditableText, Popover, ScriptContentType, ScriptEditor, SelectField, acceptActionIntoBaseline, acceptSceneIntoBaseline, applyActionAccept, applyActionReject, buildActionDiff, buildDiffFromBaseline, buildSelectionFrom, changeSetToDiffScenes, computeChangeSet, computeSceneActionDiff, findBaselineScene, fromToSelection, getActorCueVar, getActorMap, getEpisodeTitle, getLocationMap, getPropMap, getSceneContext, getSelectionEpisodeIdx, getSpeakerMap, isClipSelected, isEpisodeOpen, isSameSelectionTarget, isSceneSelected, mutations_exports as mutations, rejectActionToBaseline, rejectSceneToBaseline, sceneChangeKey, selectionFromScriptReference, selectionTargetKey, withOrigin };
3695
4015
  //# sourceMappingURL=index.js.map
3696
4016
  //# sourceMappingURL=index.js.map