@lingjingai/script-editor 0.1.1 → 0.1.14

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 }),
@@ -1996,6 +2015,7 @@ function getActorCueVar(actorId, tone = "dialogue") {
1996
2015
  return `var(--lj-se-actor-cue-${idx})`;
1997
2016
  }
1998
2017
  var EditingSignalContext = createContext(null);
2018
+ var LIVE_COMMIT_MS = 400;
1999
2019
  function caretOffsetFromPoint(x, y) {
2000
2020
  const d = document;
2001
2021
  if (typeof d.caretRangeFromPoint === "function") {
@@ -2025,6 +2045,34 @@ function EditableText({
2025
2045
  const inputRef = useRef(null);
2026
2046
  const committedRef = useRef(value);
2027
2047
  const editingSignal = useContext(EditingSignalContext);
2048
+ const liveTimerRef = useRef(null);
2049
+ const composingRef = useRef(false);
2050
+ const draftRef = useRef(value);
2051
+ const onSaveRef = useRef(onSave);
2052
+ onSaveRef.current = onSave;
2053
+ const clearLiveTimer = () => {
2054
+ if (liveTimerRef.current) {
2055
+ clearTimeout(liveTimerRef.current);
2056
+ liveTimerRef.current = null;
2057
+ }
2058
+ };
2059
+ const flushLive = () => {
2060
+ clearLiveTimer();
2061
+ const next = draftRef.current;
2062
+ if (next !== committedRef.current) {
2063
+ committedRef.current = next;
2064
+ onSaveRef.current(next);
2065
+ }
2066
+ };
2067
+ const scheduleLive = () => {
2068
+ clearLiveTimer();
2069
+ liveTimerRef.current = setTimeout(() => {
2070
+ liveTimerRef.current = null;
2071
+ if (composingRef.current) return;
2072
+ flushLive();
2073
+ }, LIVE_COMMIT_MS);
2074
+ };
2075
+ useEffect(() => () => flushLive(), []);
2028
2076
  useEffect(() => {
2029
2077
  if (!editing || !editingSignal) return;
2030
2078
  editingSignal.inc();
@@ -2060,9 +2108,11 @@ function EditableText({
2060
2108
  if (sel && !sel.isCollapsed) return;
2061
2109
  caretRef.current = caretOffsetFromPoint(e.clientX, e.clientY);
2062
2110
  setDraft(committedRef.current);
2111
+ draftRef.current = committedRef.current;
2063
2112
  setEditing(true);
2064
2113
  }
2065
2114
  function commit() {
2115
+ clearLiveTimer();
2066
2116
  setEditing(false);
2067
2117
  onEditEnd?.();
2068
2118
  if (draft !== committedRef.current) {
@@ -2071,7 +2121,9 @@ function EditableText({
2071
2121
  }
2072
2122
  }
2073
2123
  function cancel() {
2124
+ clearLiveTimer();
2074
2125
  setDraft(committedRef.current);
2126
+ draftRef.current = committedRef.current;
2075
2127
  setEditing(false);
2076
2128
  onEditEnd?.();
2077
2129
  }
@@ -2091,8 +2143,20 @@ function EditableText({
2091
2143
  value: draft,
2092
2144
  className: "lj-se-editing" + (className ? " " + className : ""),
2093
2145
  onChange: (e) => {
2094
- setDraft(e.target.value);
2146
+ const v = e.target.value;
2147
+ setDraft(v);
2148
+ draftRef.current = v;
2095
2149
  if (multiline) autoSize(e.target);
2150
+ if (!composingRef.current) scheduleLive();
2151
+ },
2152
+ onCompositionStart: () => {
2153
+ composingRef.current = true;
2154
+ clearLiveTimer();
2155
+ },
2156
+ onCompositionEnd: (e) => {
2157
+ composingRef.current = false;
2158
+ draftRef.current = e.currentTarget.value;
2159
+ scheduleLive();
2096
2160
  },
2097
2161
  onBlur: commit,
2098
2162
  onKeyDown: (e) => {
@@ -2118,10 +2182,10 @@ function formatStateChangeLabels(item, actorMap, locationMap, propMap) {
2118
2182
  const kind = change.target_kind?.trim() || "actor";
2119
2183
  const id = change.target_id?.trim() || "";
2120
2184
  if (!id) return "";
2121
- const states = kind === "location" ? locationMap.get(id)?.states : kind === "prop" ? propMap.get(id)?.states : actorMap.get(id)?.states;
2185
+ const states2 = kind === "location" ? locationMap.get(id)?.states : kind === "prop" ? propMap.get(id)?.states : actorMap.get(id)?.states;
2122
2186
  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";
2187
+ const from = getStateDisplayName(findState(states2, change.from_state_id)) || change.from_state_id?.trim() || "\u672A\u8BBE";
2188
+ const to = getStateDisplayName(findState(states2, change.to_state_id)) || change.to_state_id?.trim() || "\u672A\u8BBE";
2125
2189
  return `${name}\uFF1A${from} \u2192 ${to}`;
2126
2190
  }).filter(Boolean);
2127
2191
  if (item.transition_prompt?.process || item.transition_prompt?.contrast) {
@@ -2158,12 +2222,19 @@ function SceneItemRowImpl({
2158
2222
  const type = item.type ?? "dialogue" /* Dialogue */;
2159
2223
  const content = item.content ?? "";
2160
2224
  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
2225
  const emotion = translateEmotionLabel(item.emotion);
2165
2226
  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);
2227
+ const multiSpeaker = type === "dialogue" /* Dialogue */ && (item.delivery === "overlap" || (item.lines?.length ?? 0) > 0 || (item.speakers?.length ?? 0) > 1);
2228
+ const resolvedActorId = actorId || (() => {
2229
+ const spkId = item.speaker_id?.trim() || item.speakers?.[0]?.speaker_id?.trim() || "";
2230
+ if (!spkId) return "";
2231
+ const spk = speakerMap.get(spkId);
2232
+ const kind = spk?.source_kind?.trim();
2233
+ if ((kind === "actor" || kind === "prop") && spk?.source_id?.trim()) return spk.source_id.trim();
2234
+ if (spkId.startsWith("spk_") && actorMap.has(spkId.slice(4))) return spkId.slice(4);
2235
+ return "";
2236
+ })();
2237
+ const resolvedActor = actorMap.get(resolvedActorId) ?? propMap.get(resolvedActorId);
2167
2238
  const handle = /* @__PURE__ */ jsx(
2168
2239
  RowControlHandle,
2169
2240
  {
@@ -2178,11 +2249,11 @@ function SceneItemRowImpl({
2178
2249
  onDelete
2179
2250
  }
2180
2251
  );
2181
- const actorCue = (tone) => structured ? /* @__PURE__ */ jsxs(
2252
+ const actorCue = (tone) => multiSpeaker ? /* @__PURE__ */ jsxs(
2182
2253
  "span",
2183
2254
  {
2184
2255
  className: "lj-se-cue lj-se-cue--structured",
2185
- style: !hasOrphanActor ? { color: getActorCueVar(actorId, tone) } : void 0,
2256
+ style: resolvedActor ? { color: getActorCueVar(resolvedActorId, tone) } : void 0,
2186
2257
  children: [
2187
2258
  getDialogueSpeakerLabel(item, actorMap, speakerMap) || "\u672A\u77E5\u53D1\u58F0\u6E90",
2188
2259
  getDialogueDeliveryLabel(item.delivery) ? /* @__PURE__ */ jsxs("span", { className: "lj-se-cue-delivery", children: [
@@ -2195,13 +2266,13 @@ function SceneItemRowImpl({
2195
2266
  ) : /* @__PURE__ */ jsx(
2196
2267
  SelectField,
2197
2268
  {
2198
- value: actorId,
2269
+ value: resolvedActorId,
2199
2270
  options: actorOptions,
2200
2271
  disabled,
2201
2272
  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,
2273
+ title: "\u5207\u6362\u89D2\u8272",
2274
+ className: cn("lj-se-cue lj-se-cue--select", resolvedActorId && !resolvedActor && "is-orphan"),
2275
+ style: resolvedActor ? { color: getActorCueVar(resolvedActorId, tone) } : void 0,
2205
2276
  onChange: onSaveActor
2206
2277
  }
2207
2278
  );
@@ -2210,7 +2281,7 @@ function SceneItemRowImpl({
2210
2281
  if (type === "dialogue" /* Dialogue */) {
2211
2282
  body2 = /* @__PURE__ */ jsxs("div", { className: "lj-se-std-dialogue", children: [
2212
2283
  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,
2284
+ /* @__PURE__ */ jsx("div", { className: "lj-se-std-emotion", children: /* @__PURE__ */ jsx(EditableText, { value: item.emotion ?? "", disabled, placeholder: "\u60C5\u7EEA\u2026", onSave: onSaveEmotion }) }),
2214
2285
  /* @__PURE__ */ jsx(
2215
2286
  EditableText,
2216
2287
  {
@@ -2276,19 +2347,19 @@ function SceneItemRowImpl({
2276
2347
  const tone = isInner ? "inner" : "dialogue";
2277
2348
  const tag = isInner ? emotion ? `OS\xB7${emotion}` : "OS" : emotion;
2278
2349
  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(
2350
+ 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
2351
  SelectField,
2281
2352
  {
2282
- value: actorId,
2353
+ value: resolvedActorId,
2283
2354
  options: actorOptions,
2284
2355
  disabled,
2285
2356
  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,
2357
+ className: cn("lj-se-asian-speaker lj-se-asian-speaker--select", resolvedActorId && !resolvedActor && "is-orphan"),
2358
+ style: resolvedActor ? { color: getActorCueVar(resolvedActorId, tone) } : void 0,
2288
2359
  onChange: onSaveActor
2289
2360
  }
2290
2361
  ),
2291
- !structured ? /* @__PURE__ */ jsxs("span", { className: "lj-se-asian-tag", children: [
2362
+ !multiSpeaker ? /* @__PURE__ */ jsxs("span", { className: "lj-se-asian-tag", children: [
2292
2363
  "\uFF08",
2293
2364
  /* @__PURE__ */ jsx(EditableText, { value: item.emotion ?? "", disabled, placeholder: isInner ? "OS" : "\u60C5\u7EEA", onSave: onSaveEmotion }),
2294
2365
  "\uFF09"
@@ -2308,6 +2379,12 @@ function SceneItemRowImpl({
2308
2379
  ] });
2309
2380
  }
2310
2381
  var SceneItemRow = memo(SceneItemRowImpl);
2382
+ function DiffActionControls({ onAccept, onReject }) {
2383
+ return /* @__PURE__ */ jsxs("span", { className: "lj-se-actiondiff-ctrl", children: [
2384
+ /* @__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 }) }),
2385
+ /* @__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 }) })
2386
+ ] });
2387
+ }
2311
2388
  function SceneEditorImpl({
2312
2389
  episodeIndex,
2313
2390
  sceneIndex,
@@ -2316,6 +2393,7 @@ function SceneEditorImpl({
2316
2393
  format,
2317
2394
  disabled,
2318
2395
  focused = false,
2396
+ actionDiff,
2319
2397
  onEdit,
2320
2398
  onNavigateAsset,
2321
2399
  onRequestAssetDelete
@@ -2323,10 +2401,18 @@ function SceneEditorImpl({
2323
2401
  const [pendingEdit, setPendingEdit] = useState(null);
2324
2402
  const { actorMap, speakerMap, locationMap, propMap } = maps;
2325
2403
  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
- );
2404
+ const actorOptions = useMemo(() => {
2405
+ const out = [];
2406
+ for (const a of actorMap.values()) {
2407
+ const v = (a.actor_id ?? "").trim();
2408
+ if (v) out.push({ value: v, label: getActorDisplayName(a) });
2409
+ }
2410
+ for (const p of propMap.values()) {
2411
+ const v = (p.prop_id ?? "").trim();
2412
+ if (v) out.push({ value: v, label: `${getPropDisplayName(p)}\uFF08\u9053\u5177\uFF09` });
2413
+ }
2414
+ return out;
2415
+ }, [actorMap, propMap]);
2330
2416
  const insertAt = useCallback(
2331
2417
  (index, type) => {
2332
2418
  const inherited = pickInheritedActorId(scene, index);
@@ -2362,6 +2448,32 @@ function SceneEditorImpl({
2362
2448
  e.preventDefault();
2363
2449
  dnd?.drop();
2364
2450
  };
2451
+ const noop = () => {
2452
+ };
2453
+ const renderOldRow = (item) => /* @__PURE__ */ jsx(
2454
+ SceneItemRow,
2455
+ {
2456
+ item,
2457
+ actorMap,
2458
+ speakerMap,
2459
+ locationMap,
2460
+ propMap,
2461
+ actorOptions,
2462
+ format,
2463
+ disabled: true,
2464
+ onSave: noop,
2465
+ onSaveActor: noop,
2466
+ onSaveEmotion: noop,
2467
+ onChangeType: noop,
2468
+ onInsertAbove: noop,
2469
+ onInsertBelow: noop,
2470
+ onDelete: noop
2471
+ }
2472
+ );
2473
+ 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: [
2474
+ /* @__PURE__ */ jsx(DiffActionControls, { onAccept: () => actionDiff.onAccept(m), onReject: () => actionDiff.onReject(m) }),
2475
+ m.item ? renderOldRow(m.item) : /* @__PURE__ */ jsx("p", { className: "lj-se-ghost-empty", children: "\uFF08\u5DF2\u5220\u9664\u7A7A\u884C\uFF09" })
2476
+ ] }, `ghost-${beforeIdx}-${gi}`)) ?? null;
2365
2477
  return /* @__PURE__ */ jsxs("div", { className: "lj-se-scene", children: [
2366
2478
  /* @__PURE__ */ jsx(
2367
2479
  SceneHeader,
@@ -2381,52 +2493,73 @@ function SceneEditorImpl({
2381
2493
  }
2382
2494
  ),
2383
2495
  /* @__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,
2496
+ actions.map((item, idx) => {
2497
+ const mark = actionDiff?.byIndex.get(idx);
2498
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
2499
+ renderGhosts(idx),
2500
+ /* @__PURE__ */ jsxs(
2501
+ "div",
2392
2502
  {
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
- }
2503
+ className: cn(
2504
+ "lj-se-action-slot",
2505
+ dragIndex === idx && "is-dragging",
2506
+ dropSlot === idx && "drop-before",
2507
+ mark && (mark.kind === "added" ? "is-diff-added" : "is-diff-modified")
2508
+ ),
2509
+ onDragOver: (e) => onSlotDragOver(e, idx),
2510
+ onDrop: onSlotDrop,
2511
+ children: [
2512
+ mark ? /* @__PURE__ */ jsx(
2513
+ DiffActionControls,
2514
+ {
2515
+ onAccept: () => actionDiff.onAccept(mark),
2516
+ onReject: () => actionDiff.onReject(mark)
2517
+ }
2518
+ ) : null,
2519
+ mark?.kind === "modified" && mark.item ? /* @__PURE__ */ jsx("div", { className: "lj-se-diff-old", children: renderOldRow(mark.item) }) : null,
2520
+ /* @__PURE__ */ jsx(
2521
+ SceneItemRow,
2522
+ {
2523
+ item,
2524
+ actorMap,
2525
+ speakerMap,
2526
+ locationMap,
2527
+ propMap,
2528
+ actorOptions,
2529
+ format,
2530
+ disabled,
2531
+ autoEdit: pendingEdit === idx,
2532
+ onEditEnd: pendingEdit === idx ? clearPending : void 0,
2533
+ canSplit: idx > 0,
2534
+ dragHandleProps: disabled ? void 0 : { draggable: true, onDragStart: (e) => onDragStart(e, idx), onDragEnd: () => dnd?.end() },
2535
+ onSave: (content) => {
2536
+ clearPending();
2537
+ onEdit((cur) => updateSceneItemContent(cur, episodeIndex, sceneIndex, idx, content));
2538
+ },
2539
+ onSaveActor: (actorId) => onEdit((cur) => updateSceneItemActor(cur, episodeIndex, sceneIndex, idx, actorId)),
2540
+ onSaveEmotion: (emotion) => onEdit((cur) => updateSceneItemEmotion(cur, episodeIndex, sceneIndex, idx, emotion)),
2541
+ onChangeType: (type) => {
2542
+ clearPending();
2543
+ onEdit((cur) => updateSceneItemType(cur, episodeIndex, sceneIndex, idx, type));
2544
+ },
2545
+ onInsertAbove: () => insertSame(idx, idx),
2546
+ onInsertBelow: () => insertSame(idx + 1, idx),
2547
+ onSplitHere: idx > 0 ? () => {
2548
+ clearPending();
2549
+ onEdit((cur) => splitScene(cur, episodeIndex, sceneIndex, idx));
2550
+ } : void 0,
2551
+ onDelete: () => {
2552
+ clearPending();
2553
+ onEdit((cur) => deleteSceneItem(cur, episodeIndex, sceneIndex, idx));
2554
+ }
2555
+ }
2556
+ )
2557
+ ]
2425
2558
  }
2426
2559
  )
2427
- },
2428
- `item-${idx}`
2429
- )),
2560
+ ] }, `item-${idx}`);
2561
+ }),
2562
+ renderGhosts(actions.length),
2430
2563
  dropSlot === actions.length && actions.length > 0 ? /* @__PURE__ */ jsx("div", { className: "lj-se-dropline-end" }) : null,
2431
2564
  actions.length === 0 && !disabled ? /* @__PURE__ */ jsxs(
2432
2565
  "button",
@@ -2804,7 +2937,7 @@ function AssetCardImpl({
2804
2937
  const name = group === "actors" ? getActorDisplayName(entity) : group === "locations" ? getLocationDisplayName(entity) : getPropDisplayName(entity);
2805
2938
  const rawDescription = entity.description ?? "";
2806
2939
  const description = rawDescription.trim();
2807
- const states = entity.states ?? [];
2940
+ const states2 = entity.states ?? [];
2808
2941
  const fromRef = buildSelectionFrom(kind, assetId);
2809
2942
  return /* @__PURE__ */ jsx("div", { className: "lj-se-assetcard-row", children: /* @__PURE__ */ jsxs("article", { className: "lj-se-assetcard", "data-no-quote-selection": true, children: [
2810
2943
  /* @__PURE__ */ jsxs("div", { className: "lj-se-assetcard-head", children: [
@@ -2852,7 +2985,7 @@ function AssetCardImpl({
2852
2985
  /* @__PURE__ */ jsxs("div", { className: "lj-se-assetcard-states-head", children: [
2853
2986
  /* @__PURE__ */ jsxs("span", { className: "lj-se-assetcard-states-label", children: [
2854
2987
  "\u72B6\u6001 ",
2855
- /* @__PURE__ */ jsx("span", { className: "lj-se-field-hint", children: states.length })
2988
+ /* @__PURE__ */ jsx("span", { className: "lj-se-field-hint", children: states2.length })
2856
2989
  ] }),
2857
2990
  !disabled ? /* @__PURE__ */ jsxs(
2858
2991
  "button",
@@ -2867,7 +3000,7 @@ function AssetCardImpl({
2867
3000
  }
2868
3001
  ) : null
2869
3002
  ] }),
2870
- states.length > 0 ? /* @__PURE__ */ jsx("div", { className: "lj-se-assetcard-state-cards", children: states.map((state, idx) => {
3003
+ states2.length > 0 ? /* @__PURE__ */ jsx("div", { className: "lj-se-assetcard-state-cards", children: states2.map((state, idx) => {
2871
3004
  const sid = (state.state_id ?? "").trim() || `idx-${idx}`;
2872
3005
  return /* @__PURE__ */ jsxs("div", { className: "lj-se-assetcard-state", children: [
2873
3006
  /* @__PURE__ */ jsxs("div", { className: "lj-se-assetcard-state-top", children: [
@@ -2922,6 +3055,10 @@ function ScriptStream({
2922
3055
  selection,
2923
3056
  changedSceneKeys,
2924
3057
  sceneDiff,
3058
+ actionDiffByScene,
3059
+ onAcceptAction,
3060
+ onRejectAction,
3061
+ scrollScopeKey,
2925
3062
  onEdit,
2926
3063
  onSelectionChange,
2927
3064
  onRequestAssetDelete
@@ -2944,6 +3081,7 @@ function ScriptStream({
2944
3081
  return next;
2945
3082
  }, [deferredValue]);
2946
3083
  const handleRef = useRef(null);
3084
+ useScrollStateMemo(scrollScopeKey, handleRef);
2947
3085
  const programmaticUntil = useRef(0);
2948
3086
  const lastEmitted = useRef(-1);
2949
3087
  const lastScrolledKey = useRef("");
@@ -3003,8 +3141,8 @@ function ScriptStream({
3003
3141
  const idx = selectionToFlatIndex(stream, selection);
3004
3142
  if (idx == null) return;
3005
3143
  lastScrolledKey.current = key;
3006
- programmaticUntil.current = Date.now() + 700;
3007
- handleRef.current?.scrollToIndex({ index: idx, align: "start", behavior: "smooth" });
3144
+ programmaticUntil.current = Date.now() + 200;
3145
+ handleRef.current?.scrollToIndex({ index: idx, align: "start", behavior: "auto" });
3008
3146
  }, [selection, stream]);
3009
3147
  const emitScrollSelection = (topIndex) => {
3010
3148
  if (Date.now() < programmaticUntil.current) return;
@@ -3021,6 +3159,7 @@ function ScriptStream({
3021
3159
  ref: handleRef,
3022
3160
  className: "lj-se-stream lj-se-scroll",
3023
3161
  data: stream.items,
3162
+ restoreStateFrom: readScrollState(scrollScopeKey),
3024
3163
  increaseViewportBy: 1500,
3025
3164
  rangeChanged: (range) => emitScrollSelection(range.startIndex),
3026
3165
  itemContent: (_index, item) => {
@@ -3064,19 +3203,26 @@ function ScriptStream({
3064
3203
  if (!scene) return null;
3065
3204
  const focused = selection.kind === "scene" && selection.episodeIdx === item.episodeIdx && selection.sceneIdx === item.sceneIdx;
3066
3205
  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" }),
3206
+ const inDiff = !disabled && (changedSceneKeys?.has(diffKey) ?? false);
3207
+ const wholeKind = !disabled ? sceneDiff?.kindByKey.get(diffKey) : void 0;
3208
+ const sceneActions = !disabled ? actionDiffByScene?.get(diffKey) : void 0;
3209
+ const actionDiff = sceneActions && onAcceptAction && onRejectAction ? {
3210
+ byIndex: sceneActions.byIndex,
3211
+ removedBefore: sceneActions.removedBefore,
3212
+ onAccept: onAcceptAction,
3213
+ onReject: onRejectAction
3214
+ } : void 0;
3215
+ 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: [
3216
+ sceneDiff && inDiff ? /* @__PURE__ */ jsxs("div", { className: "lj-se-scenediff", children: [
3217
+ /* @__PURE__ */ jsx("span", { className: cn("lj-se-scenediff-tag", wholeKind === "added" && "is-added"), children: wholeKind === "added" ? "AI \u65B0\u589E" : "AI \u6539\u52A8" }),
3072
3218
  /* @__PURE__ */ jsx("span", { className: "lj-se-scenediff-spacer" }),
3073
3219
  /* @__PURE__ */ jsx(
3074
3220
  "button",
3075
3221
  {
3076
3222
  type: "button",
3077
3223
  className: "lj-se-scenediff-reject",
3078
- onClick: () => sceneDiff.onReject(item.episodeIdx, item.sceneIdx, item.sceneId),
3079
- children: "\u62D2\u7EDD"
3224
+ onClick: () => sceneDiff.onReject(item.episodeIdx, item.sceneId),
3225
+ children: "\u6574\u573A\u62D2\u7EDD"
3080
3226
  }
3081
3227
  ),
3082
3228
  /* @__PURE__ */ jsx(
@@ -3085,13 +3231,14 @@ function ScriptStream({
3085
3231
  type: "button",
3086
3232
  className: "lj-se-scenediff-accept",
3087
3233
  onClick: () => sceneDiff.onAccept(item.episodeIdx, item.sceneId),
3088
- children: "\u63A5\u53D7"
3234
+ children: "\u6574\u573A\u63A5\u53D7"
3089
3235
  }
3090
3236
  )
3091
3237
  ] }) : null,
3092
3238
  /* @__PURE__ */ jsx(
3093
3239
  SceneEditor,
3094
3240
  {
3241
+ actionDiff,
3095
3242
  episodeIndex: item.episodeIdx,
3096
3243
  sceneIndex: item.sceneIdx,
3097
3244
  scene,
@@ -3168,7 +3315,7 @@ function EpisodeBanner({
3168
3315
  onEdit
3169
3316
  }) {
3170
3317
  return /* @__PURE__ */ jsxs("div", { className: "lj-se-epbanner", children: [
3171
- projectTitle !== void 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
3318
+ projectTitle !== void 0 ? /* @__PURE__ */ jsxs(Fragment$1, { children: [
3172
3319
  /* @__PURE__ */ jsx("div", { className: "lj-se-epbanner-kicker", children: "\u5267\u672C" }),
3173
3320
  /* @__PURE__ */ jsx("h1", { className: "lj-se-epbanner-script", children: /* @__PURE__ */ jsx(
3174
3321
  EditableText,
@@ -3267,6 +3414,21 @@ function computeChangeSet(baseline, current) {
3267
3414
  const count = (title ? 1 : 0) + (worldview ? 1 : 0) + (style ? 1 : 0) + changedScenes.size + removedCount;
3268
3415
  return { title, worldview, style, changedScenes, removedCount, count };
3269
3416
  }
3417
+ function changeSetToDiffScenes(changeSet) {
3418
+ const out = [];
3419
+ for (const [key, kind] of changeSet.changedScenes) {
3420
+ const i = key.indexOf(":");
3421
+ if (i < 0) continue;
3422
+ const episodeIdx = Number(key.slice(0, i));
3423
+ const sceneId = key.slice(i + 1);
3424
+ if (Number.isFinite(episodeIdx)) out.push({ episodeIdx, sceneId, kind });
3425
+ }
3426
+ return out;
3427
+ }
3428
+ function buildDiffFromBaseline(baseline, current) {
3429
+ const cs = computeChangeSet(baseline, current);
3430
+ return { scenes: changeSetToDiffScenes(cs), removedCount: cs.removedCount, count: cs.count };
3431
+ }
3270
3432
  function acceptSceneIntoBaseline(baseline, episodeId, episodeIdx, scene) {
3271
3433
  const episodes = (baseline.episodes ?? []).slice();
3272
3434
  const id = episodeId?.trim();
@@ -3304,6 +3466,170 @@ function findBaselineScene(baseline, episodeId, episodeIdx, sceneId) {
3304
3466
  if (!sid) return null;
3305
3467
  return (ep.scenes ?? []).find((s) => (s.scene_id ?? "").trim() === sid) ?? null;
3306
3468
  }
3469
+ function actionSig(a) {
3470
+ return [a.type ?? "", (a.content ?? "").trim(), a.actor_id ?? "", a.emotion ?? "", a.delivery ?? ""].join("\0");
3471
+ }
3472
+ function computeSceneActionDiff(baseline, current) {
3473
+ const m = baseline.length;
3474
+ const n = current.length;
3475
+ const bs = baseline.map(actionSig);
3476
+ const cs = current.map(actionSig);
3477
+ const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
3478
+ for (let i2 = m - 1; i2 >= 0; i2--) {
3479
+ for (let j2 = n - 1; j2 >= 0; j2--) {
3480
+ dp[i2][j2] = bs[i2] === cs[j2] ? dp[i2 + 1][j2 + 1] + 1 : Math.max(dp[i2 + 1][j2], dp[i2][j2 + 1]);
3481
+ }
3482
+ }
3483
+ const ops = [];
3484
+ let i = 0;
3485
+ let j = 0;
3486
+ while (i < m && j < n) {
3487
+ if (bs[i] === cs[j]) {
3488
+ ops.push({ t: "match", j });
3489
+ i++;
3490
+ j++;
3491
+ } else if (dp[i + 1][j] >= dp[i][j + 1]) {
3492
+ ops.push({ t: "del", i });
3493
+ i++;
3494
+ } else {
3495
+ ops.push({ t: "ins", j });
3496
+ j++;
3497
+ }
3498
+ }
3499
+ while (i < m) {
3500
+ ops.push({ t: "del", i });
3501
+ i++;
3502
+ }
3503
+ while (j < n) {
3504
+ ops.push({ t: "ins", j });
3505
+ j++;
3506
+ }
3507
+ const changed = [];
3508
+ const removed = [];
3509
+ let curJ = 0;
3510
+ let p = 0;
3511
+ while (p < ops.length) {
3512
+ const op = ops[p];
3513
+ if (op.t === "match") {
3514
+ curJ = op.j + 1;
3515
+ p++;
3516
+ continue;
3517
+ }
3518
+ if (op.t === "ins") {
3519
+ changed.push({ itemIdx: op.j, kind: "added" });
3520
+ curJ = op.j + 1;
3521
+ p++;
3522
+ continue;
3523
+ }
3524
+ const dels = [];
3525
+ while (p < ops.length && ops[p].t === "del") {
3526
+ dels.push(ops[p].i);
3527
+ p++;
3528
+ }
3529
+ const inss = [];
3530
+ while (p < ops.length && ops[p].t === "ins") {
3531
+ inss.push(ops[p].j);
3532
+ p++;
3533
+ }
3534
+ const pair = Math.min(dels.length, inss.length);
3535
+ for (let q = 0; q < pair; q++) changed.push({ itemIdx: inss[q], kind: "modified", baselineItemIdx: dels[q] });
3536
+ for (let q = pair; q < inss.length; q++) changed.push({ itemIdx: inss[q], kind: "added" });
3537
+ const ghostBefore = inss.length ? inss[inss.length - 1] + 1 : curJ;
3538
+ for (let q = pair; q < dels.length; q++) removed.push({ beforeItemIdx: ghostBefore, baselineItemIdx: dels[q] });
3539
+ if (inss.length) curJ = inss[inss.length - 1] + 1;
3540
+ }
3541
+ return { changed, removed };
3542
+ }
3543
+ function applyActionAccept(baselineScene, currentScene, mark) {
3544
+ const bActions = (baselineScene.actions ?? []).slice();
3545
+ const cActions = currentScene.actions ?? [];
3546
+ if (mark.kind === "modified" && mark.baselineItemIdx != null) {
3547
+ const item = cActions[mark.itemIdx];
3548
+ if (item && mark.baselineItemIdx < bActions.length) bActions[mark.baselineItemIdx] = item;
3549
+ } else if (mark.kind === "added") {
3550
+ const item = cActions[mark.itemIdx];
3551
+ if (item) bActions.splice(Math.min(mark.itemIdx, bActions.length), 0, item);
3552
+ } else if (mark.kind === "removed" && mark.baselineItemIdx != null) {
3553
+ if (mark.baselineItemIdx < bActions.length) bActions.splice(mark.baselineItemIdx, 1);
3554
+ }
3555
+ return { ...baselineScene, actions: bActions };
3556
+ }
3557
+ function applyActionReject(currentScene, baselineScene, mark) {
3558
+ const cActions = (currentScene.actions ?? []).slice();
3559
+ const bActions = baselineScene?.actions ?? [];
3560
+ if (mark.kind === "modified" && mark.baselineItemIdx != null) {
3561
+ const item = bActions[mark.baselineItemIdx];
3562
+ if (item && mark.itemIdx < cActions.length) cActions[mark.itemIdx] = item;
3563
+ } else if (mark.kind === "added") {
3564
+ if (mark.itemIdx < cActions.length) cActions.splice(mark.itemIdx, 1);
3565
+ } else if (mark.kind === "removed") {
3566
+ const item = mark.item ?? (mark.baselineItemIdx != null ? bActions[mark.baselineItemIdx] : void 0);
3567
+ if (item) cActions.splice(Math.min(mark.itemIdx, cActions.length), 0, item);
3568
+ }
3569
+ return { ...currentScene, actions: cActions };
3570
+ }
3571
+ function acceptActionIntoBaseline(baseline, current, mark) {
3572
+ const cEp = current.episodes?.[mark.episodeIdx];
3573
+ const curScene = cEp?.scenes?.find((s) => (s.scene_id ?? "").trim() === mark.sceneId.trim());
3574
+ const baseScene = findBaselineScene(baseline, cEp?.episode_id, mark.episodeIdx, mark.sceneId);
3575
+ if (!baseScene) {
3576
+ return curScene ? acceptSceneIntoBaseline(baseline, cEp?.episode_id, mark.episodeIdx, curScene) : baseline;
3577
+ }
3578
+ const next = applyActionAccept(baseScene, curScene ?? baseScene, mark);
3579
+ return acceptSceneIntoBaseline(baseline, cEp?.episode_id, mark.episodeIdx, next);
3580
+ }
3581
+ function rejectActionToBaseline(current, baseline, mark) {
3582
+ const cEp = current.episodes?.[mark.episodeIdx];
3583
+ const sceneIdx = cEp?.scenes?.findIndex((s) => (s.scene_id ?? "").trim() === mark.sceneId.trim()) ?? -1;
3584
+ if (sceneIdx < 0 || !cEp?.scenes) return current;
3585
+ const curScene = cEp.scenes[sceneIdx];
3586
+ const baseScene = findBaselineScene(baseline, cEp.episode_id, mark.episodeIdx, mark.sceneId);
3587
+ const next = applyActionReject(curScene, baseScene, mark);
3588
+ return rejectSceneToBaseline(current, mark.episodeIdx, sceneIdx, next);
3589
+ }
3590
+ function buildActionDiff(baseline, current) {
3591
+ const cs = computeChangeSet(baseline, current);
3592
+ const scenes = [];
3593
+ const actions = [];
3594
+ for (const [key, kind] of cs.changedScenes) {
3595
+ const ci = key.indexOf(":");
3596
+ const episodeIdx = Number(key.slice(0, ci));
3597
+ const sceneId = key.slice(ci + 1);
3598
+ if (!Number.isFinite(episodeIdx)) continue;
3599
+ if (kind === "added") {
3600
+ scenes.push({ episodeIdx, sceneId, kind: "added" });
3601
+ continue;
3602
+ }
3603
+ const ep = current.episodes?.[episodeIdx];
3604
+ const curScene = ep?.scenes?.find((s) => (s.scene_id ?? "").trim() === sceneId);
3605
+ const baseScene = findBaselineScene(baseline, ep?.episode_id, episodeIdx, sceneId);
3606
+ if (!curScene) continue;
3607
+ const sd = computeSceneActionDiff(baseScene?.actions ?? [], curScene.actions ?? []);
3608
+ for (const c of sd.changed) {
3609
+ actions.push({
3610
+ episodeIdx,
3611
+ sceneId,
3612
+ kind: c.kind,
3613
+ itemIdx: c.itemIdx,
3614
+ baselineItemIdx: c.baselineItemIdx,
3615
+ // modified carries the OLD baseline action so the editor can render the
3616
+ // deleted line above the new one (unified-diff view)
3617
+ item: c.kind === "modified" && c.baselineItemIdx != null ? baseScene?.actions?.[c.baselineItemIdx] : void 0
3618
+ });
3619
+ }
3620
+ for (const r of sd.removed) {
3621
+ actions.push({
3622
+ episodeIdx,
3623
+ sceneId,
3624
+ kind: "removed",
3625
+ itemIdx: r.beforeItemIdx,
3626
+ baselineItemIdx: r.baselineItemIdx,
3627
+ item: baseScene?.actions?.[r.baselineItemIdx]
3628
+ });
3629
+ }
3630
+ }
3631
+ return { scenes, actions, removedCount: cs.removedCount, count: scenes.length + actions.length + cs.removedCount };
3632
+ }
3307
3633
 
3308
3634
  // src/reference.ts
3309
3635
  function asNumber(value) {
@@ -3433,6 +3759,7 @@ function ScriptEditor({
3433
3759
  onRedo,
3434
3760
  onEditingChange,
3435
3761
  referenceFocusRequest,
3762
+ scrollScopeKey,
3436
3763
  diff,
3437
3764
  asset,
3438
3765
  onAddChatReferences,
@@ -3453,6 +3780,31 @@ function ScriptEditor({
3453
3780
  }),
3454
3781
  []
3455
3782
  );
3783
+ const undoKeysRef = useRef({ onUndo, onRedo, canUndo, canRedo });
3784
+ undoKeysRef.current = { onUndo, onRedo, canUndo, canRedo };
3785
+ useEffect(() => {
3786
+ if (disabled) return;
3787
+ const onKeyDown = (e) => {
3788
+ if (!(e.metaKey || e.ctrlKey)) return;
3789
+ const t = e.target;
3790
+ if (t instanceof HTMLElement && (t.tagName === "INPUT" || t.tagName === "TEXTAREA" || t.tagName === "SELECT" || t.isContentEditable)) {
3791
+ return;
3792
+ }
3793
+ const key = e.key.toLowerCase();
3794
+ const { onUndo: u, onRedo: r, canUndo: cu, canRedo: cr } = undoKeysRef.current;
3795
+ if (key === "z" && !e.shiftKey) {
3796
+ if (!u || !cu) return;
3797
+ e.preventDefault();
3798
+ u();
3799
+ } else if (key === "z" && e.shiftKey || key === "y") {
3800
+ if (!r || !cr) return;
3801
+ e.preventDefault();
3802
+ r();
3803
+ }
3804
+ };
3805
+ document.addEventListener("keydown", onKeyDown);
3806
+ return () => document.removeEventListener("keydown", onKeyDown);
3807
+ }, [disabled]);
3456
3808
  const [innerSelection, setInnerSelection] = useState(EMPTY_SELECTION);
3457
3809
  const selection = controlledSelection ?? innerSelection;
3458
3810
  const setSelection = useCallback(
@@ -3471,14 +3823,36 @@ function ScriptEditor({
3471
3823
  },
3472
3824
  [onFormatChange]
3473
3825
  );
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
- );
3826
+ const sceneKindByKey = useMemo(() => {
3827
+ const m = /* @__PURE__ */ new Map();
3828
+ for (const s of diff?.scenes ?? []) m.set(sceneChangeKey(s.episodeIdx, s.sceneId), s.kind);
3829
+ return m;
3830
+ }, [diff?.scenes]);
3831
+ const actionDiffByScene = useMemo(() => {
3832
+ const map = /* @__PURE__ */ new Map();
3833
+ for (const m of diff?.actions ?? []) {
3834
+ const key = sceneChangeKey(m.episodeIdx, m.sceneId);
3835
+ let e = map.get(key);
3836
+ if (!e) {
3837
+ e = { byIndex: /* @__PURE__ */ new Map(), removedBefore: /* @__PURE__ */ new Map() };
3838
+ map.set(key, e);
3839
+ }
3840
+ if (m.kind === "removed") {
3841
+ const arr = e.removedBefore.get(m.itemIdx) ?? [];
3842
+ arr.push(m);
3843
+ e.removedBefore.set(m.itemIdx, arr);
3844
+ } else {
3845
+ e.byIndex.set(m.itemIdx, m);
3846
+ }
3847
+ }
3848
+ return map;
3849
+ }, [diff?.actions]);
3850
+ const changedSceneKeys = useMemo(() => {
3851
+ const s = new Set(sceneKindByKey.keys());
3852
+ for (const k of actionDiffByScene.keys()) s.add(k);
3853
+ return s;
3854
+ }, [sceneKindByKey, actionDiffByScene]);
3855
+ const diffCount = diff ? diff.count ?? diff.scenes.length + (diff.actions?.length ?? 0) + (diff.removedCount ?? 0) : 0;
3482
3856
  const changedSelections = useMemo(() => {
3483
3857
  if (changedSceneKeys.size === 0) return [];
3484
3858
  const out = [];
@@ -3503,28 +3877,15 @@ function ScriptEditor({
3503
3877
  },
3504
3878
  [changedSelections, diffCursor, setSelection]
3505
3879
  );
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
- );
3880
+ const sceneDiff = useMemo(() => {
3881
+ if (!diff || !diff.onAcceptScene && !diff.onRejectScene || changedSceneKeys.size === 0) return void 0;
3882
+ const kindAt = (episodeIdx, sceneId) => sceneKindByKey.get(sceneChangeKey(episodeIdx, sceneId)) ?? "modified";
3883
+ return {
3884
+ kindByKey: sceneKindByKey,
3885
+ onAccept: (episodeIdx, sceneId) => diff.onAcceptScene?.({ episodeIdx, sceneId, kind: kindAt(episodeIdx, sceneId) }),
3886
+ onReject: (episodeIdx, sceneId) => diff.onRejectScene?.({ episodeIdx, sceneId, kind: kindAt(episodeIdx, sceneId) })
3887
+ };
3888
+ }, [diff, sceneKindByKey, changedSceneKeys]);
3528
3889
  useEffect(() => {
3529
3890
  if (!referenceFocusRequest) return;
3530
3891
  const sel = selectionFromScriptReference(referenceFocusRequest.reference);
@@ -3542,6 +3903,10 @@ function ScriptEditor({
3542
3903
  selection,
3543
3904
  changedSceneKeys,
3544
3905
  sceneDiff,
3906
+ actionDiffByScene,
3907
+ onAcceptAction: diff?.onAcceptAction,
3908
+ onRejectAction: diff?.onRejectAction,
3909
+ scrollScopeKey,
3545
3910
  onEdit,
3546
3911
  onSelectionChange: setSelection,
3547
3912
  onRequestAssetDelete
@@ -3577,7 +3942,7 @@ function ScriptEditor({
3577
3942
  SaveCapsule,
3578
3943
  {
3579
3944
  saveStatus,
3580
- diffCount: diff ? changeSet.count : 0,
3945
+ diffCount,
3581
3946
  baselineStatus: diff?.baselineStatus,
3582
3947
  canApplyDiff: !!(diff?.onAcceptAll || diff?.onRejectAll),
3583
3948
  onDiffPrev: changedSelections.length > 0 ? () => navDiff(-1) : void 0,
@@ -3691,6 +4056,6 @@ __export(mutations_exports, {
3691
4056
  updateScenePropState: () => updateScenePropState
3692
4057
  });
3693
4058
 
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 };
4059
+ 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
4060
  //# sourceMappingURL=index.js.map
3696
4061
  //# sourceMappingURL=index.js.map