dqy08 commited on
Commit
f3beede
·
1 Parent(s): 654165a

代码评审;功能改进

Browse files
client/src/causal_flow.html CHANGED
@@ -326,7 +326,7 @@
326
  <div class="semantic-submode-row attribution-delete-prompt-patterns-header">
327
  <span class="semantic-submode-group">
328
  <label class="attribution-use-mapping-label"
329
- title="When enabled, each line below is a regex with the global flag, matched only within the initial static prompt prefix. Matched prompt tokens are physically removed from the DAG — they neither appear nor occupy layout space (stricter than Exclude + Hide)."
330
  data-i18n="title">
331
  <input type="checkbox" id="gen_attr_delete_prompt_patterns_enable">
332
  <span></span>
@@ -340,14 +340,14 @@
340
  placeholder="One regex per line — matched prompt tokens are removed from layout"
341
  spellcheck="false"
342
  autocomplete="off"
343
- title="One regex per line (global flag), matched only within the initial prompt prefix; matched tokens are deleted from the DAG and do not occupy layout space."
344
  data-i18n="placeholder,title"></textarea>
345
  </div>
346
  <div class="attribution-exclude-prompt-patterns-row">
347
  <div class="semantic-submode-row attribution-exclude-prompt-patterns-header">
348
  <span class="semantic-submode-group">
349
  <label class="attribution-use-mapping-label"
350
- title="When enabled, each line below is a regex with the global flag, matched only within the initial static prompt prefix (excluding generated continuation). If a token offset lies fully inside a match, its score is treated as 0."
351
  data-i18n="title">
352
  <input type="checkbox" id="gen_attr_exclude_prompt_patterns_enable" checked>
353
  <span></span>
@@ -358,10 +358,10 @@
358
  </span>
359
  </div>
360
  <textarea id="gen_attr_exclude_prompt_patterns" class="attribution-exclude-prompt-patterns-input" rows="1"
361
- placeholder="One regex per line (prompt prefix only)"
362
  spellcheck="false"
363
  autocomplete="off"
364
- title="One regex per line (global flag), matched only within the initial prompt prefix; if a token offset lies fully inside a match, its score is treated as 0."
365
  data-i18n="placeholder,title"></textarea>
366
  </div>
367
 
@@ -433,6 +433,16 @@
433
  </span>
434
  </span>
435
  </div>
 
 
 
 
 
 
 
 
 
 
436
  </section>
437
  </div>
438
  </section>
 
326
  <div class="semantic-submode-row attribution-delete-prompt-patterns-header">
327
  <span class="semantic-submode-group">
328
  <label class="attribution-use-mapping-label"
329
+ title="When enabled, each line below is a regex with the global flag, matched only within input areas (excluding generated continuation). Matched prompt tokens are physically removed from the DAG — they neither appear nor occupy layout space (stricter than Exclude + Hide)."
330
  data-i18n="title">
331
  <input type="checkbox" id="gen_attr_delete_prompt_patterns_enable">
332
  <span></span>
 
340
  placeholder="One regex per line — matched prompt tokens are removed from layout"
341
  spellcheck="false"
342
  autocomplete="off"
343
+ title="One regex per line (global flag), matched only within input areas (excluding generated continuation); matched tokens are deleted from the DAG and do not occupy layout space."
344
  data-i18n="placeholder,title"></textarea>
345
  </div>
346
  <div class="attribution-exclude-prompt-patterns-row">
347
  <div class="semantic-submode-row attribution-exclude-prompt-patterns-header">
348
  <span class="semantic-submode-group">
349
  <label class="attribution-use-mapping-label"
350
+ title="When enabled, each line below is a regex with the global flag, matched only within input areas (excluding generated continuation). If a token offset lies fully inside a match, its score is treated as 0."
351
  data-i18n="title">
352
  <input type="checkbox" id="gen_attr_exclude_prompt_patterns_enable" checked>
353
  <span></span>
 
358
  </span>
359
  </div>
360
  <textarea id="gen_attr_exclude_prompt_patterns" class="attribution-exclude-prompt-patterns-input" rows="1"
361
+ placeholder="One regex per line (input areas only)"
362
  spellcheck="false"
363
  autocomplete="off"
364
+ title="One regex per line (global flag), matched only within input areas (excluding generated continuation); if a token offset lies fully inside a match, its score is treated as 0."
365
  data-i18n="placeholder,title"></textarea>
366
  </div>
367
 
 
433
  </span>
434
  </span>
435
  </div>
436
+ <div class="gen-attr-dag-measure-width-row semantic-submode-row gen-attr-dag-replay-auto-zoom-row">
437
+ <span class="semantic-submode-group">
438
+ <label class="semantic-submode-label">
439
+ <input type="checkbox" id="gen_attr_dag_replay_auto_zoom"
440
+ title="When checked, step replay (▶) fits the viewport after each step (stops if you pan or zoom). Saved locally."
441
+ data-i18n="title">
442
+ <span data-i18n>Auto zoom</span>
443
+ </label>
444
+ </span>
445
+ </div>
446
  </section>
447
  </div>
448
  </section>
client/src/pages/causal_flow/index.ts CHANGED
@@ -150,6 +150,7 @@ const GEN_ATTR_DAG_MEASURE_WIDTH_STORAGE_KEY = 'info_radar_gen_attr_dag_measure_
150
  const GEN_ATTR_DAG_LAYOUT_MODE_STORAGE_KEY = 'info_radar_gen_attr_dag_layout_mode';
151
  const GEN_ATTR_DAG_PLAYBACK_STEP_MS_STORAGE_KEY = 'info_radar_gen_attr_dag_playback_step_ms';
152
  const GEN_ATTR_DAG_REPLAY_PACING_MODE_STORAGE_KEY = 'info_radar_gen_attr_dag_replay_pacing_mode';
 
153
  const GEN_ATTR_DAG_PLAYBACK_TOTAL_S_STORAGE_KEY = 'info_radar_gen_attr_dag_playback_total_s';
154
  const GEN_ATTR_DAG_NODE_CI_VISUAL_SCALE_STORAGE_KEY = 'info_radar_gen_attr_dag_node_ci_visual_scale';
155
  const GEN_ATTR_DAG_DECAY_ATTRIBUTION_HIGH_SURPRISAL_STORAGE_KEY =
@@ -199,6 +200,8 @@ const GEN_ATTR_DAG_PLAYBACK_STEP_MS_MAX = 10000;
199
  const GEN_ATTR_DAG_PLAYBACK_TOTAL_S_DEFAULT = 7;
200
  const GEN_ATTR_DAG_PLAYBACK_TOTAL_S_MIN = 1;
201
  const GEN_ATTR_DAG_PLAYBACK_TOTAL_S_MAX = 3600;
 
 
202
 
203
  /** 与无 demoUiOptions 本地缓存时「读出默认」对齐,供重置与可读性单一的来源 */
204
  const DEFAULT_GEN_ATTR_DEMO_UI_OPTIONS: GenAttrDemoUiOptions = {
@@ -219,6 +222,7 @@ const DEFAULT_GEN_ATTR_DEMO_UI_OPTIONS: GenAttrDemoUiOptions = {
219
  recursiveEdgeBatchAnimationDirection: 'forward',
220
  showTokenInfoOnSelected: false,
221
  replayPacingMode: 'total',
 
222
  playbackTotalS: GEN_ATTR_DAG_PLAYBACK_TOTAL_S_DEFAULT,
223
  playbackStepMs: GEN_ATTR_DAG_PLAYBACK_STEP_MS_DEFAULT,
224
  excludePromptPatternsEnabled: true,
@@ -290,17 +294,36 @@ function readStoredDagPlaybackStepMs(): number {
290
  }
291
 
292
  function clampDagPlaybackTotalS(n: number): number {
 
 
293
  return Math.max(
294
  GEN_ATTR_DAG_PLAYBACK_TOTAL_S_MIN,
295
- Math.min(GEN_ATTR_DAG_PLAYBACK_TOTAL_S_MAX, Math.round(n))
296
  );
297
  }
298
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
299
  function readStoredDagPlaybackTotalS(): number {
300
  return lsReadNumber(
301
  GEN_ATTR_DAG_PLAYBACK_TOTAL_S_STORAGE_KEY,
302
  GEN_ATTR_DAG_PLAYBACK_TOTAL_S_DEFAULT,
303
- { clamp: clampDagPlaybackTotalS },
304
  );
305
  }
306
 
@@ -312,6 +335,14 @@ function readStoredDagReplayPacingMode(): DagReplayPacingMode {
312
  );
313
  }
314
 
 
 
 
 
 
 
 
 
315
  function readStoredDagLayoutMode(): DagLayoutMode {
316
  return lsReadEnum(
317
  GEN_ATTR_DAG_LAYOUT_MODE_STORAGE_KEY,
@@ -406,6 +437,9 @@ const dagPlaybackTotalSInput = document.getElementById(
406
  ) as HTMLInputElement | null;
407
  const dagReplayTotalWrap = document.getElementById('gen_attr_dag_replay_total_wrap');
408
  const dagReplayStepWrap = document.getElementById('gen_attr_dag_replay_step_wrap');
 
 
 
409
 
410
  /** 与 `#gen_attr_dag_replay_mode` 同步;非法或缺失时视为 `total`。 */
411
  function currentDagReplayPacingMode(): DagReplayPacingMode {
@@ -418,13 +452,13 @@ function readDagReplayPacingFromControls(options?: { writeBack?: boolean }): Dag
418
  const stepMs = Number.isFinite(rawStep)
419
  ? clampDagPlaybackStepMs(rawStep)
420
  : readStoredDagPlaybackStepMs();
421
- const rawS = parseInt(dagPlaybackTotalSInput?.value ?? '', 10);
422
  const totalS = Number.isFinite(rawS)
423
  ? clampDagPlaybackTotalS(rawS)
424
  : readStoredDagPlaybackTotalS();
425
  if (options?.writeBack) {
426
  if (dagPlaybackStepMsInput) dagPlaybackStepMsInput.value = String(stepMs);
427
- if (dagPlaybackTotalSInput) dagPlaybackTotalSInput.value = String(totalS);
428
  }
429
  return { mode: currentDagReplayPacingMode(), stepMs, totalS };
430
  }
@@ -542,6 +576,7 @@ bindExcludePatternsUi({
542
  onEffectiveChange: onExcludePatternsEffectiveChange,
543
  defaultTextWhenKeyAbsent: '',
544
  defaultEnabledWhenKeyAbsent: false,
 
545
  });
546
  bindExcludePatternsUi({
547
  storageKeys: {
@@ -552,6 +587,7 @@ bindExcludePatternsUi({
552
  enableCheckbox: genAttrExcludePromptPatternsEnable,
553
  onEffectiveChange: onExcludePatternsEffectiveChange,
554
  defaultTextWhenKeyAbsent: DEFAULT_EXCLUDE_PROMPT_PATTERNS_TEXT,
 
555
  });
556
  bindExcludePatternsUi({
557
  storageKeys: {
@@ -562,6 +598,7 @@ bindExcludePatternsUi({
562
  enableCheckbox: genAttrExcludeGeneratedPatternsEnable,
563
  onEffectiveChange: onExcludePatternsEffectiveChange,
564
  defaultTextWhenKeyAbsent: DEFAULT_EXCLUDE_GENERATED_PATTERNS_TEXT,
 
565
  });
566
 
567
  /** 与 DAG 同源:DAG 预处理按当前控件即时读取,不读 Attribution 的 localStorage。 */
@@ -598,7 +635,9 @@ if (dagPlaybackStepMsInput) dagPlaybackStepMsInput.value = String(initialDagPlay
598
  const initialDagReplayPacingMode = readStoredDagReplayPacingMode();
599
  if (dagReplayModeSelect) dagReplayModeSelect.value = initialDagReplayPacingMode;
600
  const initialDagPlaybackTotalS = readStoredDagPlaybackTotalS();
601
- if (dagPlaybackTotalSInput) dagPlaybackTotalSInput.value = String(initialDagPlaybackTotalS);
 
 
602
  applyDagReplaySpeedUi();
603
 
604
  const genAttrResultsNode = genAttrResultsEl.node() as HTMLElement | null;
@@ -613,9 +652,7 @@ const initialDagNodeCiVisualScale = readStoredDagNodeCiVisualScale();
613
  if (dagNodeCiVisualScaleInput) dagNodeCiVisualScaleInput.checked = initialDagNodeCiVisualScale;
614
  setDagNodeCiVisualScaleEnabled(initialDagNodeCiVisualScale);
615
  dagNodeCiVisualScaleInput?.addEventListener('change', () => {
616
- const enabled = dagNodeCiVisualScaleInput.checked;
617
- lsWriteBool(GEN_ATTR_DAG_NODE_CI_VISUAL_SCALE_STORAGE_KEY, enabled, '1');
618
- setDagNodeCiVisualScaleEnabled(enabled);
619
  tryResetAndReplayDag();
620
  });
621
 
@@ -632,9 +669,7 @@ if (dagDecayAttributionHighSurprisalInput) {
632
  }
633
  setDagDecayAttributionToHighSurprisalTargetEnabled(initialDagDecayAttributionHighSurprisal);
634
  dagDecayAttributionHighSurprisalInput?.addEventListener('change', () => {
635
- const enabled = dagDecayAttributionHighSurprisalInput.checked;
636
- lsWriteBool(GEN_ATTR_DAG_DECAY_ATTRIBUTION_HIGH_SURPRISAL_STORAGE_KEY, enabled, '1');
637
- setDagDecayAttributionToHighSurprisalTargetEnabled(enabled);
638
  tryResetAndReplayDag({ refit: false });
639
  });
640
 
@@ -653,9 +688,7 @@ const initialDagHideInactiveEdges = readStoredDagHideInactiveEdges();
653
  if (dagHideInactiveEdgesInput) dagHideInactiveEdgesInput.checked = initialDagHideInactiveEdges;
654
  applyDagHideInactiveEdges(initialDagHideInactiveEdges);
655
  dagHideInactiveEdgesInput?.addEventListener('change', () => {
656
- const hide = dagHideInactiveEdgesInput.checked;
657
- lsWriteBool(GEN_ATTR_DAG_HIDE_INACTIVE_EDGES_STORAGE_KEY, hide, '1');
658
- applyDagHideInactiveEdges(hide);
659
  });
660
 
661
  function readStoredDagShowDownstreamInfluence(): boolean {
@@ -670,9 +703,7 @@ if (dagShowDownstreamInfluenceInput) {
670
  dagShowDownstreamInfluenceInput.checked = initialDagShowDownstreamInfluence;
671
  }
672
  dagShowDownstreamInfluenceInput?.addEventListener('change', () => {
673
- const show = dagShowDownstreamInfluenceInput.checked;
674
- lsWriteBool(GEN_ATTR_DAG_SHOW_DOWNSTREAM_INFLUENCE_STORAGE_KEY, show, '1');
675
- dagHandle.setShowDownstreamInfluence(show);
676
  });
677
 
678
  function syncDimInactiveTokensThresholdInputUi(): void {
@@ -790,24 +821,18 @@ setDimInactiveTokensThresholdControlFromFraction(initialDagDimInactiveTokensThre
790
  applyDagRecursiveAttributionSubmodeUi();
791
  syncDimInactiveTokensThresholdInputUi();
792
  dagRecursiveAttributionInput?.addEventListener('change', () => {
793
- const enabled = dagRecursiveAttributionInput.checked;
794
- lsWriteBool(GEN_ATTR_DAG_RECURSIVE_ATTRIBUTION_STORAGE_KEY, enabled, '1');
795
  applyDagRecursiveAttributionSubmodeUi();
796
- dagHandle.setRecursiveAttributionEnabled(enabled);
797
  });
798
  dagRecursiveEdgeAnimationDirectionSelect?.addEventListener('change', () => {
799
  const direction = currentDagRecursiveEdgeAnimationDirection();
800
  dagRecursiveEdgeAnimationDirectionSelect.value = direction;
801
- lsWriteString(GEN_ATTR_DAG_RECURSIVE_EDGE_ANIMATION_DIRECTION_STORAGE_KEY, direction);
802
  dagHandle.setRecursiveEdgeBatchAnimationDirection(direction);
803
  });
804
 
805
  dagDimInactiveTokensInput?.addEventListener('change', () => {
806
- const enabled = dagDimInactiveTokensInput.checked;
807
- lsWriteBool(GEN_ATTR_DAG_DIM_INACTIVE_TOKENS_STORAGE_KEY, enabled, '1');
808
  syncDimInactiveTokensThresholdInputUi();
809
  applyDagDimInactiveTokensFromControls();
810
- syncGenAttrResetUiOptionsButtonState();
811
  });
812
 
813
  dagDimInactiveTokensThresholdInput?.addEventListener('input', () => {
@@ -816,16 +841,11 @@ dagDimInactiveTokensThresholdInput?.addEventListener('input', () => {
816
  dagDimInactiveTokensThresholdInput?.addEventListener('change', () => {
817
  const t = readDimInactiveTokensThresholdFromControl();
818
  setDimInactiveTokensThresholdControlFromFraction(t);
819
- lsSet(GEN_ATTR_DAG_DIM_INACTIVE_TOKENS_THRESHOLD_STORAGE_KEY, String(t));
820
  applyDagDimInactiveTokensFromControls();
821
- syncGenAttrResetUiOptionsButtonState();
822
  });
823
 
824
  dagDimInactiveNotInAnimationInput?.addEventListener('change', () => {
825
- const enabled = dagDimInactiveNotInAnimationInput.checked;
826
- lsWriteBool(GEN_ATTR_DAG_DIM_INACTIVE_NOT_IN_ANIMATION_STORAGE_KEY, enabled, '1');
827
  applyDagDimInactiveTokensFromControls();
828
- syncGenAttrResetUiOptionsButtonState();
829
  });
830
 
831
  function readStoredDagHideExcludedTokens(): boolean {
@@ -847,15 +867,11 @@ function readStoredDagShowTopkOnSelected(): boolean {
847
  const initialDagShowTopkOnSelected = readStoredDagShowTopkOnSelected();
848
  if (dagShowTopkOnSelectedInput) dagShowTopkOnSelectedInput.checked = initialDagShowTopkOnSelected;
849
  dagHideExcludedTokensInput?.addEventListener('change', () => {
850
- const hide = dagHideExcludedTokensInput.checked;
851
- lsWriteBool(GEN_ATTR_DAG_HIDE_EXCLUDED_TOKENS_STORAGE_KEY, hide, '1');
852
- dagHandle.setHideExcludedTokens(hide);
853
  });
854
 
855
  dagShowTopkOnSelectedInput?.addEventListener('change', () => {
856
- const show = dagShowTopkOnSelectedInput.checked;
857
- lsWriteBool(GEN_ATTR_DAG_SHOW_TOPK_ON_SELECTED_STORAGE_KEY, show, '1');
858
- dagHandle.setShowTokenInfoOnSelected(show);
859
  });
860
 
861
  setPageOptsGetter(() => {
@@ -878,22 +894,28 @@ dagPlaybackStepMsInput?.addEventListener('change', () => {
878
  ? clampDagPlaybackStepMs(raw)
879
  : GEN_ATTR_DAG_PLAYBACK_STEP_MS_DEFAULT;
880
  dagPlaybackStepMsInput.value = String(ms);
881
- lsSet(GEN_ATTR_DAG_PLAYBACK_STEP_MS_STORAGE_KEY, String(ms));
882
  });
883
 
884
  dagReplayModeSelect?.addEventListener('change', () => {
885
- const mode = currentDagReplayPacingMode();
886
- lsWriteString(GEN_ATTR_DAG_REPLAY_PACING_MODE_STORAGE_KEY, mode);
887
  applyDagReplaySpeedUi();
888
  });
889
 
 
 
 
 
 
 
 
 
 
 
890
  dagPlaybackTotalSInput?.addEventListener('change', () => {
891
- const raw = parseInt(dagPlaybackTotalSInput.value, 10);
892
- const s = Number.isFinite(raw)
893
- ? clampDagPlaybackTotalS(raw)
894
- : GEN_ATTR_DAG_PLAYBACK_TOTAL_S_DEFAULT;
895
- dagPlaybackTotalSInput.value = String(s);
896
- lsSet(GEN_ATTR_DAG_PLAYBACK_TOTAL_S_STORAGE_KEY, String(s));
897
  });
898
 
899
  function isSkipChatTemplate(): boolean {
@@ -1210,7 +1232,7 @@ function handleDagPlaybackToggle(wantPlay: boolean): void {
1210
  cancelDagLastTokenDwell();
1211
  const steps = h.getAllSteps();
1212
  if (dagPlaybackNextIndex >= steps.length) {
1213
- // `reset()` 默认会清 `layoutDirty`,每步 `update` 就会 fit;重放前需保留用户 pan/zoom 时保留 dirty
1214
  dagHandle.reset(true);
1215
  dagPlaybackNextIndex = 0;
1216
  }
@@ -1261,15 +1283,19 @@ function handleDagPlaybackToggle(wantPlay: boolean): void {
1261
  if (visible) toolCallingPendingLine?.show();
1262
  else toolCallingPendingLine?.hide();
1263
  },
1264
- showPrompt: (fitViewport) => {
1265
  const firstStep = steps[0]!;
1266
  syncDagInputLayerAtStep({
1267
  catalogSpans: currentRunPromptSpans,
1268
  layoutWire: firstStep.context,
1269
  inputRanges: firstStep.inputRanges,
1270
- fitViewport,
1271
  });
1272
  },
 
 
 
 
 
1273
  showToolResponse: (stepIndex) => {
1274
  const step = steps[stepIndex]!;
1275
  syncDagInputLayerAtStep({
@@ -1342,10 +1368,8 @@ toolCallingPendingLine = attachToolCallingPendingLine(
1342
  );
1343
 
1344
  dagLayoutModeSelect?.addEventListener('change', () => {
1345
- const mode = currentDagLayoutMode();
1346
- lsWriteString(GEN_ATTR_DAG_LAYOUT_MODE_STORAGE_KEY, mode);
1347
  applyDagLayoutModeUi();
1348
- dagHandle.setLayoutMode(mode);
1349
  });
1350
 
1351
  /**
@@ -1397,7 +1421,6 @@ dagMeasureWidthInput?.addEventListener('change', () => {
1397
  ? clampDagMeasureWidth(raw)
1398
  : GEN_ATTR_DAG_MEASURE_WIDTH_DEFAULT;
1399
  dagMeasureWidthInput.value = String(w);
1400
- lsSet(GEN_ATTR_DAG_MEASURE_WIDTH_STORAGE_KEY, String(w));
1401
  dagHandle.setMeasureWidthPx(w);
1402
  tryResetAndReplayDag();
1403
  });
@@ -1406,7 +1429,6 @@ dagCompactnessInput?.addEventListener('change', () => {
1406
  const raw = parseFloat(dagCompactnessInput.value);
1407
  const c = Number.isFinite(raw) ? clampDagCompactness(raw) : DAG_COMPACTNESS_DEFAULT;
1408
  dagCompactnessInput.value = String(c);
1409
- lsSet(GEN_ATTR_DAG_COMPACTNESS_STORAGE_KEY, String(c));
1410
  dagHandle.setDagCompactness(c);
1411
  tryResetAndReplayDag();
1412
  });
@@ -1417,7 +1439,6 @@ dagEdgeTopPCoverageInput?.addEventListener('change', () => {
1417
  ? clampDagEdgeTopPCoverage(raw)
1418
  : DAG_EDGE_TOP_P_COVERAGE_DEFAULT;
1419
  dagEdgeTopPCoverageInput.value = String(c);
1420
- lsSet(GEN_ATTR_DAG_EDGE_TOP_P_COVERAGE_STORAGE_KEY, String(c));
1421
  dagHandle.setEdgeTopPCoverage(c);
1422
  tryResetAndReplayDag({ refit: false });
1423
  });
@@ -1428,7 +1449,6 @@ dagLinearArcIntervalInput?.addEventListener('change', () => {
1428
  ? clampLinearArcAdjacentGap(raw)
1429
  : LINEAR_ARC_ADJACENT_GAP_DEFAULT;
1430
  dagLinearArcIntervalInput.value = String(n);
1431
- lsSet(GEN_ATTR_DAG_LINEAR_ARC_GAP_STORAGE_KEY, String(n));
1432
  dagHandle.setLinearArcAdjacentGapPx(n, { skipRefit: isDagBusy() });
1433
  });
1434
 
@@ -1476,6 +1496,7 @@ function readGenAttrDemoUiOptionsFromControls(): GenAttrDemoUiOptions {
1476
  recursiveEdgeBatchAnimationDirection: currentDagRecursiveEdgeAnimationDirection(),
1477
  showTokenInfoOnSelected: dagShowTopkOnSelectedInput?.checked ?? false,
1478
  replayPacingMode,
 
1479
  playbackTotalS,
1480
  playbackStepMs,
1481
  excludePromptPatternsEnabled: genAttrExcludePromptPatternsEnable?.checked ?? true,
@@ -1510,8 +1531,133 @@ function syncGenAttrResetUiOptionsButtonState(): void {
1510
  }
1511
 
1512
  /**
1513
- * `demoUiOptions` 还原排除控件(仅 DOM);与施加 DAG demo 不回写 GEN_ATTR_LS 的策略一致,`replay` 读当前控件生效
 
 
 
1514
  */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1515
  function applyGenAttrExcludePatternsFromDemoUiSnap(snap: Partial<GenAttrDemoUiOptions>): void {
1516
  const {
1517
  deletePromptPatternsEnabled,
@@ -1663,9 +1809,12 @@ function applyGenAttrDemoUiOptionsSnap(snap: Partial<GenAttrDemoUiOptions>): voi
1663
  if (dagReplayModeSelect) dagReplayModeSelect.value = snap.replayPacingMode;
1664
  applyDagReplaySpeedUi();
1665
  }
 
 
 
1666
  if (snap.playbackTotalS !== undefined) {
1667
  const s = clampDagPlaybackTotalS(snap.playbackTotalS);
1668
- if (dagPlaybackTotalSInput) dagPlaybackTotalSInput.value = String(s);
1669
  }
1670
  if (snap.playbackStepMs !== undefined) {
1671
  const ms = clampDagPlaybackStepMs(snap.playbackStepMs);
@@ -1673,6 +1822,7 @@ function applyGenAttrDemoUiOptionsSnap(snap: Partial<GenAttrDemoUiOptions>): voi
1673
  }
1674
 
1675
  applyGenAttrExcludePatternsFromDemoUiSnap(snap);
 
1676
  syncGenAttrResetUiOptionsButtonState();
1677
  }
1678
 
@@ -1695,38 +1845,6 @@ function applyGenAttrDemoUiOptionsFromRecord(rec: GenAttrCachedRun): void {
1695
  applyGenAttrDemoUiOptionsSnap(rec.demoUiOptions);
1696
  }
1697
 
1698
- /** Gen Attribute demo-UI scope:与 {@link readGenAttrDemoUiOptionsFromControls} / IndexedDB demo 快照一致(不含 Model、Max tokens、prompt 正文)。 */
1699
- const GEN_ATTR_DEMO_UI_LOCAL_STORAGE_KEYS: readonly string[] = [
1700
- GEN_ATTR_DAG_MEASURE_WIDTH_STORAGE_KEY,
1701
- GEN_ATTR_DAG_LAYOUT_MODE_STORAGE_KEY,
1702
- GEN_ATTR_DAG_PLAYBACK_STEP_MS_STORAGE_KEY,
1703
- GEN_ATTR_DAG_REPLAY_PACING_MODE_STORAGE_KEY,
1704
- GEN_ATTR_DAG_PLAYBACK_TOTAL_S_STORAGE_KEY,
1705
- GEN_ATTR_DAG_NODE_CI_VISUAL_SCALE_STORAGE_KEY,
1706
- GEN_ATTR_DAG_DECAY_ATTRIBUTION_HIGH_SURPRISAL_STORAGE_KEY,
1707
- GEN_ATTR_DAG_HIDE_INACTIVE_EDGES_STORAGE_KEY,
1708
- GEN_ATTR_DAG_SHOW_DOWNSTREAM_INFLUENCE_STORAGE_KEY,
1709
- GEN_ATTR_DAG_RECURSIVE_ATTRIBUTION_STORAGE_KEY,
1710
- GEN_ATTR_DAG_RECURSIVE_EDGE_ANIMATION_DIRECTION_STORAGE_KEY,
1711
- GEN_ATTR_DAG_HIDE_EXCLUDED_TOKENS_STORAGE_KEY,
1712
- GEN_ATTR_DAG_SHOW_TOPK_ON_SELECTED_STORAGE_KEY,
1713
- GEN_ATTR_DAG_LINEAR_ARC_GAP_STORAGE_KEY,
1714
- GEN_ATTR_DAG_COMPACTNESS_STORAGE_KEY,
1715
- GEN_ATTR_DAG_EDGE_TOP_P_COVERAGE_STORAGE_KEY,
1716
- GEN_ATTR_DELETE_PROMPT_PATTERNS_STORAGE_KEY,
1717
- GEN_ATTR_DELETE_PROMPT_PATTERNS_ENABLED_STORAGE_KEY,
1718
- GEN_ATTR_EXCLUDE_PROMPT_PATTERNS_STORAGE_KEY,
1719
- GEN_ATTR_EXCLUDE_PROMPT_PATTERNS_ENABLED_STORAGE_KEY,
1720
- GEN_ATTR_EXCLUDE_GENERATED_PATTERNS_STORAGE_KEY,
1721
- GEN_ATTR_EXCLUDE_GENERATED_PATTERNS_ENABLED_STORAGE_KEY,
1722
- ];
1723
-
1724
- function removeGenAttrDemoUiOptionsFromLocalStorage(): void {
1725
- for (const k of GEN_ATTR_DEMO_UI_LOCAL_STORAGE_KEYS) {
1726
- lsRemove(k);
1727
- }
1728
- }
1729
-
1730
  /** 重置「DAG 演示用 UI」:清 LS 后以 {@link DEFAULT_GEN_ATTR_DEMO_UI_OPTIONS} 全量套用。 */
1731
  function resetGenAttrDemoUiOptionsToDefaults(): void {
1732
  stopDagPlayback();
@@ -1740,10 +1858,18 @@ genAttrResetUiOptionsBtn?.addEventListener('click', resetGenAttrDemoUiOptionsToD
1740
  (() => {
1741
  const panel = document.querySelector('.gen-attribute-page .input-section');
1742
  if (!panel) return;
1743
- const sync = () => syncGenAttrResetUiOptionsButtonState();
1744
- panel.addEventListener('change', sync);
1745
- panel.addEventListener('input', sync);
1746
- sync();
 
 
 
 
 
 
 
 
1747
  })();
1748
 
1749
  window.addEventListener('pagehide', (ev) => {
 
150
  const GEN_ATTR_DAG_LAYOUT_MODE_STORAGE_KEY = 'info_radar_gen_attr_dag_layout_mode';
151
  const GEN_ATTR_DAG_PLAYBACK_STEP_MS_STORAGE_KEY = 'info_radar_gen_attr_dag_playback_step_ms';
152
  const GEN_ATTR_DAG_REPLAY_PACING_MODE_STORAGE_KEY = 'info_radar_gen_attr_dag_replay_pacing_mode';
153
+ const GEN_ATTR_DAG_REPLAY_AUTO_ZOOM_STORAGE_KEY = 'info_radar_gen_attr_dag_replay_auto_zoom';
154
  const GEN_ATTR_DAG_PLAYBACK_TOTAL_S_STORAGE_KEY = 'info_radar_gen_attr_dag_playback_total_s';
155
  const GEN_ATTR_DAG_NODE_CI_VISUAL_SCALE_STORAGE_KEY = 'info_radar_gen_attr_dag_node_ci_visual_scale';
156
  const GEN_ATTR_DAG_DECAY_ATTRIBUTION_HIGH_SURPRISAL_STORAGE_KEY =
 
200
  const GEN_ATTR_DAG_PLAYBACK_TOTAL_S_DEFAULT = 7;
201
  const GEN_ATTR_DAG_PLAYBACK_TOTAL_S_MIN = 1;
202
  const GEN_ATTR_DAG_PLAYBACK_TOTAL_S_MAX = 3600;
203
+ /** 手输总量化的步长;原生 step=1 箭头在 `input` 里另取整。 */
204
+ const GEN_ATTR_DAG_PLAYBACK_TOTAL_S_STEP = 0.1;
205
 
206
  /** 与无 demoUiOptions 本地缓存时「读出默认」对齐,供重置与可读性单一的来源 */
207
  const DEFAULT_GEN_ATTR_DEMO_UI_OPTIONS: GenAttrDemoUiOptions = {
 
222
  recursiveEdgeBatchAnimationDirection: 'forward',
223
  showTokenInfoOnSelected: false,
224
  replayPacingMode: 'total',
225
+ replayAutoZoom: false,
226
  playbackTotalS: GEN_ATTR_DAG_PLAYBACK_TOTAL_S_DEFAULT,
227
  playbackStepMs: GEN_ATTR_DAG_PLAYBACK_STEP_MS_DEFAULT,
228
  excludePromptPatternsEnabled: true,
 
294
  }
295
 
296
  function clampDagPlaybackTotalS(n: number): number {
297
+ const stepped =
298
+ Math.round(n / GEN_ATTR_DAG_PLAYBACK_TOTAL_S_STEP) * GEN_ATTR_DAG_PLAYBACK_TOTAL_S_STEP;
299
  return Math.max(
300
  GEN_ATTR_DAG_PLAYBACK_TOTAL_S_MIN,
301
+ Math.min(GEN_ATTR_DAG_PLAYBACK_TOTAL_S_MAX, stepped),
302
  );
303
  }
304
 
305
+ function formatDagPlaybackTotalS(n: number): string {
306
+ const s = clampDagPlaybackTotalS(n);
307
+ return Number.isInteger(s) ? String(s) : s.toFixed(1);
308
+ }
309
+
310
+ function readDagPlaybackTotalSFromInput(): number {
311
+ const raw = parseFloat(dagPlaybackTotalSInput?.value ?? '');
312
+ return Number.isFinite(raw)
313
+ ? clampDagPlaybackTotalS(raw)
314
+ : GEN_ATTR_DAG_PLAYBACK_TOTAL_S_DEFAULT;
315
+ }
316
+
317
+ function commitDagPlaybackTotalSInput(): void {
318
+ if (!dagPlaybackTotalSInput) return;
319
+ dagPlaybackTotalSInput.value = formatDagPlaybackTotalS(readDagPlaybackTotalSFromInput());
320
+ }
321
+
322
  function readStoredDagPlaybackTotalS(): number {
323
  return lsReadNumber(
324
  GEN_ATTR_DAG_PLAYBACK_TOTAL_S_STORAGE_KEY,
325
  GEN_ATTR_DAG_PLAYBACK_TOTAL_S_DEFAULT,
326
+ { parse: 'float', clamp: clampDagPlaybackTotalS },
327
  );
328
  }
329
 
 
335
  );
336
  }
337
 
338
+ function readStoredDagReplayAutoZoom(): boolean {
339
+ return lsReadBool(
340
+ GEN_ATTR_DAG_REPLAY_AUTO_ZOOM_STORAGE_KEY,
341
+ DEFAULT_GEN_ATTR_DEMO_UI_OPTIONS.replayAutoZoom,
342
+ { encoding: '1' },
343
+ );
344
+ }
345
+
346
  function readStoredDagLayoutMode(): DagLayoutMode {
347
  return lsReadEnum(
348
  GEN_ATTR_DAG_LAYOUT_MODE_STORAGE_KEY,
 
437
  ) as HTMLInputElement | null;
438
  const dagReplayTotalWrap = document.getElementById('gen_attr_dag_replay_total_wrap');
439
  const dagReplayStepWrap = document.getElementById('gen_attr_dag_replay_step_wrap');
440
+ const dagReplayAutoZoomInput = document.getElementById(
441
+ 'gen_attr_dag_replay_auto_zoom',
442
+ ) as HTMLInputElement | null;
443
 
444
  /** 与 `#gen_attr_dag_replay_mode` 同步;非法或缺失时视为 `total`。 */
445
  function currentDagReplayPacingMode(): DagReplayPacingMode {
 
452
  const stepMs = Number.isFinite(rawStep)
453
  ? clampDagPlaybackStepMs(rawStep)
454
  : readStoredDagPlaybackStepMs();
455
+ const rawS = parseFloat(dagPlaybackTotalSInput?.value ?? '');
456
  const totalS = Number.isFinite(rawS)
457
  ? clampDagPlaybackTotalS(rawS)
458
  : readStoredDagPlaybackTotalS();
459
  if (options?.writeBack) {
460
  if (dagPlaybackStepMsInput) dagPlaybackStepMsInput.value = String(stepMs);
461
+ if (dagPlaybackTotalSInput) dagPlaybackTotalSInput.value = formatDagPlaybackTotalS(totalS);
462
  }
463
  return { mode: currentDagReplayPacingMode(), stepMs, totalS };
464
  }
 
576
  onEffectiveChange: onExcludePatternsEffectiveChange,
577
  defaultTextWhenKeyAbsent: '',
578
  defaultEnabledWhenKeyAbsent: false,
579
+ skipLocalStoragePersist: true,
580
  });
581
  bindExcludePatternsUi({
582
  storageKeys: {
 
587
  enableCheckbox: genAttrExcludePromptPatternsEnable,
588
  onEffectiveChange: onExcludePatternsEffectiveChange,
589
  defaultTextWhenKeyAbsent: DEFAULT_EXCLUDE_PROMPT_PATTERNS_TEXT,
590
+ skipLocalStoragePersist: true,
591
  });
592
  bindExcludePatternsUi({
593
  storageKeys: {
 
598
  enableCheckbox: genAttrExcludeGeneratedPatternsEnable,
599
  onEffectiveChange: onExcludePatternsEffectiveChange,
600
  defaultTextWhenKeyAbsent: DEFAULT_EXCLUDE_GENERATED_PATTERNS_TEXT,
601
+ skipLocalStoragePersist: true,
602
  });
603
 
604
  /** 与 DAG 同源:DAG 预处理按当前控件即时读取,不读 Attribution 的 localStorage。 */
 
635
  const initialDagReplayPacingMode = readStoredDagReplayPacingMode();
636
  if (dagReplayModeSelect) dagReplayModeSelect.value = initialDagReplayPacingMode;
637
  const initialDagPlaybackTotalS = readStoredDagPlaybackTotalS();
638
+ if (dagPlaybackTotalSInput) dagPlaybackTotalSInput.value = formatDagPlaybackTotalS(initialDagPlaybackTotalS);
639
+ const initialDagReplayAutoZoom = readStoredDagReplayAutoZoom();
640
+ if (dagReplayAutoZoomInput) dagReplayAutoZoomInput.checked = initialDagReplayAutoZoom;
641
  applyDagReplaySpeedUi();
642
 
643
  const genAttrResultsNode = genAttrResultsEl.node() as HTMLElement | null;
 
652
  if (dagNodeCiVisualScaleInput) dagNodeCiVisualScaleInput.checked = initialDagNodeCiVisualScale;
653
  setDagNodeCiVisualScaleEnabled(initialDagNodeCiVisualScale);
654
  dagNodeCiVisualScaleInput?.addEventListener('change', () => {
655
+ setDagNodeCiVisualScaleEnabled(dagNodeCiVisualScaleInput.checked);
 
 
656
  tryResetAndReplayDag();
657
  });
658
 
 
669
  }
670
  setDagDecayAttributionToHighSurprisalTargetEnabled(initialDagDecayAttributionHighSurprisal);
671
  dagDecayAttributionHighSurprisalInput?.addEventListener('change', () => {
672
+ setDagDecayAttributionToHighSurprisalTargetEnabled(dagDecayAttributionHighSurprisalInput.checked);
 
 
673
  tryResetAndReplayDag({ refit: false });
674
  });
675
 
 
688
  if (dagHideInactiveEdgesInput) dagHideInactiveEdgesInput.checked = initialDagHideInactiveEdges;
689
  applyDagHideInactiveEdges(initialDagHideInactiveEdges);
690
  dagHideInactiveEdgesInput?.addEventListener('change', () => {
691
+ applyDagHideInactiveEdges(dagHideInactiveEdgesInput.checked);
 
 
692
  });
693
 
694
  function readStoredDagShowDownstreamInfluence(): boolean {
 
703
  dagShowDownstreamInfluenceInput.checked = initialDagShowDownstreamInfluence;
704
  }
705
  dagShowDownstreamInfluenceInput?.addEventListener('change', () => {
706
+ dagHandle.setShowDownstreamInfluence(dagShowDownstreamInfluenceInput.checked);
 
 
707
  });
708
 
709
  function syncDimInactiveTokensThresholdInputUi(): void {
 
821
  applyDagRecursiveAttributionSubmodeUi();
822
  syncDimInactiveTokensThresholdInputUi();
823
  dagRecursiveAttributionInput?.addEventListener('change', () => {
 
 
824
  applyDagRecursiveAttributionSubmodeUi();
825
+ dagHandle.setRecursiveAttributionEnabled(dagRecursiveAttributionInput.checked);
826
  });
827
  dagRecursiveEdgeAnimationDirectionSelect?.addEventListener('change', () => {
828
  const direction = currentDagRecursiveEdgeAnimationDirection();
829
  dagRecursiveEdgeAnimationDirectionSelect.value = direction;
 
830
  dagHandle.setRecursiveEdgeBatchAnimationDirection(direction);
831
  });
832
 
833
  dagDimInactiveTokensInput?.addEventListener('change', () => {
 
 
834
  syncDimInactiveTokensThresholdInputUi();
835
  applyDagDimInactiveTokensFromControls();
 
836
  });
837
 
838
  dagDimInactiveTokensThresholdInput?.addEventListener('input', () => {
 
841
  dagDimInactiveTokensThresholdInput?.addEventListener('change', () => {
842
  const t = readDimInactiveTokensThresholdFromControl();
843
  setDimInactiveTokensThresholdControlFromFraction(t);
 
844
  applyDagDimInactiveTokensFromControls();
 
845
  });
846
 
847
  dagDimInactiveNotInAnimationInput?.addEventListener('change', () => {
 
 
848
  applyDagDimInactiveTokensFromControls();
 
849
  });
850
 
851
  function readStoredDagHideExcludedTokens(): boolean {
 
867
  const initialDagShowTopkOnSelected = readStoredDagShowTopkOnSelected();
868
  if (dagShowTopkOnSelectedInput) dagShowTopkOnSelectedInput.checked = initialDagShowTopkOnSelected;
869
  dagHideExcludedTokensInput?.addEventListener('change', () => {
870
+ dagHandle.setHideExcludedTokens(dagHideExcludedTokensInput.checked);
 
 
871
  });
872
 
873
  dagShowTopkOnSelectedInput?.addEventListener('change', () => {
874
+ dagHandle.setShowTokenInfoOnSelected(dagShowTopkOnSelectedInput.checked);
 
 
875
  });
876
 
877
  setPageOptsGetter(() => {
 
894
  ? clampDagPlaybackStepMs(raw)
895
  : GEN_ATTR_DAG_PLAYBACK_STEP_MS_DEFAULT;
896
  dagPlaybackStepMsInput.value = String(ms);
 
897
  });
898
 
899
  dagReplayModeSelect?.addEventListener('change', () => {
 
 
900
  applyDagReplaySpeedUi();
901
  });
902
 
903
+ dagPlaybackTotalSInput?.addEventListener('input', (e) => {
904
+ if (!dagPlaybackTotalSInput) return;
905
+ if (!(e instanceof InputEvent) || (e.inputType !== 'increment' && e.inputType !== 'decrement')) {
906
+ return;
907
+ }
908
+ const raw = parseFloat(dagPlaybackTotalSInput.value);
909
+ if (!Number.isFinite(raw)) return;
910
+ dagPlaybackTotalSInput.value = formatDagPlaybackTotalS(clampDagPlaybackTotalS(Math.round(raw)));
911
+ });
912
+
913
  dagPlaybackTotalSInput?.addEventListener('change', () => {
914
+ commitDagPlaybackTotalSInput();
915
+ });
916
+
917
+ dagPlaybackTotalSInput?.addEventListener('blur', () => {
918
+ commitDagPlaybackTotalSInput();
 
919
  });
920
 
921
  function isSkipChatTemplate(): boolean {
 
1232
  cancelDagLastTokenDwell();
1233
  const steps = h.getAllSteps();
1234
  if (dagPlaybackNextIndex >= steps.length) {
1235
+ // 保留 `layoutDirty`用户 pan/zoom Auto zoom 仍应停止 fit
1236
  dagHandle.reset(true);
1237
  dagPlaybackNextIndex = 0;
1238
  }
 
1283
  if (visible) toolCallingPendingLine?.show();
1284
  else toolCallingPendingLine?.hide();
1285
  },
1286
+ showPrompt: () => {
1287
  const firstStep = steps[0]!;
1288
  syncDagInputLayerAtStep({
1289
  catalogSpans: currentRunPromptSpans,
1290
  layoutWire: firstStep.context,
1291
  inputRanges: firstStep.inputRanges,
 
1292
  });
1293
  },
1294
+ afterStepShown: () => {
1295
+ if (dagReplayAutoZoomInput?.checked) {
1296
+ dagHandle.fitViewportToContent();
1297
+ }
1298
+ },
1299
  showToolResponse: (stepIndex) => {
1300
  const step = steps[stepIndex]!;
1301
  syncDagInputLayerAtStep({
 
1368
  );
1369
 
1370
  dagLayoutModeSelect?.addEventListener('change', () => {
 
 
1371
  applyDagLayoutModeUi();
1372
+ dagHandle.setLayoutMode(currentDagLayoutMode());
1373
  });
1374
 
1375
  /**
 
1421
  ? clampDagMeasureWidth(raw)
1422
  : GEN_ATTR_DAG_MEASURE_WIDTH_DEFAULT;
1423
  dagMeasureWidthInput.value = String(w);
 
1424
  dagHandle.setMeasureWidthPx(w);
1425
  tryResetAndReplayDag();
1426
  });
 
1429
  const raw = parseFloat(dagCompactnessInput.value);
1430
  const c = Number.isFinite(raw) ? clampDagCompactness(raw) : DAG_COMPACTNESS_DEFAULT;
1431
  dagCompactnessInput.value = String(c);
 
1432
  dagHandle.setDagCompactness(c);
1433
  tryResetAndReplayDag();
1434
  });
 
1439
  ? clampDagEdgeTopPCoverage(raw)
1440
  : DAG_EDGE_TOP_P_COVERAGE_DEFAULT;
1441
  dagEdgeTopPCoverageInput.value = String(c);
 
1442
  dagHandle.setEdgeTopPCoverage(c);
1443
  tryResetAndReplayDag({ refit: false });
1444
  });
 
1449
  ? clampLinearArcAdjacentGap(raw)
1450
  : LINEAR_ARC_ADJACENT_GAP_DEFAULT;
1451
  dagLinearArcIntervalInput.value = String(n);
 
1452
  dagHandle.setLinearArcAdjacentGapPx(n, { skipRefit: isDagBusy() });
1453
  });
1454
 
 
1496
  recursiveEdgeBatchAnimationDirection: currentDagRecursiveEdgeAnimationDirection(),
1497
  showTokenInfoOnSelected: dagShowTopkOnSelectedInput?.checked ?? false,
1498
  replayPacingMode,
1499
+ replayAutoZoom: dagReplayAutoZoomInput?.checked ?? false,
1500
  playbackTotalS,
1501
  playbackStepMs,
1502
  excludePromptPatternsEnabled: genAttrExcludePromptPatternsEnable?.checked ?? true,
 
1531
  }
1532
 
1533
  /**
1534
+ * 演示 UI 控件 id localStorage 键:面板委托识别、批量清除 LS
1535
+ * 新增控件须同步改:本表、`persistGenAttrDemoUiOptionsToLocalStorage`、
1536
+ * {@link readGenAttrDemoUiOptionsFromControls}、`applyGenAttrDemoUiOptionsSnap`、
1537
+ * {@link DEFAULT_GEN_ATTR_DEMO_UI_OPTIONS}(不含 Model、Max tokens、prompt 正文)。
1538
  */
1539
+ const GEN_ATTR_DEMO_UI_PERSIST_SPECS: ReadonlyArray<{
1540
+ readonly controlId: string;
1541
+ readonly storageKey: string;
1542
+ }> = [
1543
+ { controlId: 'gen_attr_dag_layout_mode', storageKey: GEN_ATTR_DAG_LAYOUT_MODE_STORAGE_KEY },
1544
+ { controlId: 'gen_attr_dag_compactness', storageKey: GEN_ATTR_DAG_COMPACTNESS_STORAGE_KEY },
1545
+ { controlId: 'gen_attr_dag_measure_width', storageKey: GEN_ATTR_DAG_MEASURE_WIDTH_STORAGE_KEY },
1546
+ { controlId: 'gen_attr_dag_linear_arc_interval', storageKey: GEN_ATTR_DAG_LINEAR_ARC_GAP_STORAGE_KEY },
1547
+ { controlId: 'gen_attr_dag_node_ci_visual_scale', storageKey: GEN_ATTR_DAG_NODE_CI_VISUAL_SCALE_STORAGE_KEY },
1548
+ {
1549
+ controlId: 'gen_attr_dag_decay_attribution_high_surprisal',
1550
+ storageKey: GEN_ATTR_DAG_DECAY_ATTRIBUTION_HIGH_SURPRISAL_STORAGE_KEY,
1551
+ },
1552
+ { controlId: 'gen_attr_dag_recursive_attribution', storageKey: GEN_ATTR_DAG_RECURSIVE_ATTRIBUTION_STORAGE_KEY },
1553
+ {
1554
+ controlId: 'gen_attr_dag_recursive_edge_animation_direction',
1555
+ storageKey: GEN_ATTR_DAG_RECURSIVE_EDGE_ANIMATION_DIRECTION_STORAGE_KEY,
1556
+ },
1557
+ { controlId: 'gen_attr_dag_dim_inactive_tokens', storageKey: GEN_ATTR_DAG_DIM_INACTIVE_TOKENS_STORAGE_KEY },
1558
+ {
1559
+ controlId: 'gen_attr_dag_dim_inactive_tokens_threshold',
1560
+ storageKey: GEN_ATTR_DAG_DIM_INACTIVE_TOKENS_THRESHOLD_STORAGE_KEY,
1561
+ },
1562
+ {
1563
+ controlId: 'gen_attr_dag_dim_inactive_not_in_animation',
1564
+ storageKey: GEN_ATTR_DAG_DIM_INACTIVE_NOT_IN_ANIMATION_STORAGE_KEY,
1565
+ },
1566
+ {
1567
+ controlId: 'gen_attr_dag_show_downstream_influence',
1568
+ storageKey: GEN_ATTR_DAG_SHOW_DOWNSTREAM_INFLUENCE_STORAGE_KEY,
1569
+ },
1570
+ { controlId: 'gen_attr_dag_edge_top_p_coverage', storageKey: GEN_ATTR_DAG_EDGE_TOP_P_COVERAGE_STORAGE_KEY },
1571
+ { controlId: 'gen_attr_dag_hide_inactive_edges', storageKey: GEN_ATTR_DAG_HIDE_INACTIVE_EDGES_STORAGE_KEY },
1572
+ { controlId: 'gen_attr_dag_hide_excluded_tokens', storageKey: GEN_ATTR_DAG_HIDE_EXCLUDED_TOKENS_STORAGE_KEY },
1573
+ { controlId: 'gen_attr_dag_show_topk_on_selected', storageKey: GEN_ATTR_DAG_SHOW_TOPK_ON_SELECTED_STORAGE_KEY },
1574
+ { controlId: 'gen_attr_dag_replay_mode', storageKey: GEN_ATTR_DAG_REPLAY_PACING_MODE_STORAGE_KEY },
1575
+ { controlId: 'gen_attr_dag_replay_auto_zoom', storageKey: GEN_ATTR_DAG_REPLAY_AUTO_ZOOM_STORAGE_KEY },
1576
+ { controlId: 'gen_attr_dag_playback_total_s', storageKey: GEN_ATTR_DAG_PLAYBACK_TOTAL_S_STORAGE_KEY },
1577
+ { controlId: 'gen_attr_dag_playback_step_ms', storageKey: GEN_ATTR_DAG_PLAYBACK_STEP_MS_STORAGE_KEY },
1578
+ {
1579
+ controlId: 'gen_attr_delete_prompt_patterns_enable',
1580
+ storageKey: GEN_ATTR_DELETE_PROMPT_PATTERNS_ENABLED_STORAGE_KEY,
1581
+ },
1582
+ { controlId: 'gen_attr_delete_prompt_patterns', storageKey: GEN_ATTR_DELETE_PROMPT_PATTERNS_STORAGE_KEY },
1583
+ {
1584
+ controlId: 'gen_attr_exclude_prompt_patterns_enable',
1585
+ storageKey: GEN_ATTR_EXCLUDE_PROMPT_PATTERNS_ENABLED_STORAGE_KEY,
1586
+ },
1587
+ { controlId: 'gen_attr_exclude_prompt_patterns', storageKey: GEN_ATTR_EXCLUDE_PROMPT_PATTERNS_STORAGE_KEY },
1588
+ {
1589
+ controlId: 'gen_attr_exclude_generated_patterns_enable',
1590
+ storageKey: GEN_ATTR_EXCLUDE_GENERATED_PATTERNS_ENABLED_STORAGE_KEY,
1591
+ },
1592
+ { controlId: 'gen_attr_exclude_generated_patterns', storageKey: GEN_ATTR_EXCLUDE_GENERATED_PATTERNS_STORAGE_KEY },
1593
+ ];
1594
+
1595
+ const GEN_ATTR_DEMO_UI_CONTROL_IDS = new Set(GEN_ATTR_DEMO_UI_PERSIST_SPECS.map((s) => s.controlId));
1596
+
1597
+ const GEN_ATTR_DEMO_UI_LOCAL_STORAGE_KEYS: readonly string[] = GEN_ATTR_DEMO_UI_PERSIST_SPECS.map(
1598
+ (s) => s.storageKey,
1599
+ );
1600
+
1601
+ function isGenAttrDemoUiControl(target: EventTarget | null): boolean {
1602
+ return target instanceof HTMLElement && GEN_ATTR_DEMO_UI_CONTROL_IDS.has(target.id);
1603
+ }
1604
+
1605
+ function removeGenAttrDemoUiOptionsFromLocalStorage(): void {
1606
+ for (const k of GEN_ATTR_DEMO_UI_LOCAL_STORAGE_KEYS) {
1607
+ lsRemove(k);
1608
+ }
1609
+ }
1610
+
1611
+ function persistGenAttrDemoUiOptionsToLocalStorage(snap: GenAttrDemoUiOptions): void {
1612
+ lsWriteString(GEN_ATTR_DAG_LAYOUT_MODE_STORAGE_KEY, snap.layoutMode);
1613
+ lsSet(GEN_ATTR_DAG_MEASURE_WIDTH_STORAGE_KEY, String(snap.measureWidthPx));
1614
+ lsSet(GEN_ATTR_DAG_COMPACTNESS_STORAGE_KEY, String(snap.dagCompactness));
1615
+ lsSet(GEN_ATTR_DAG_LINEAR_ARC_GAP_STORAGE_KEY, String(snap.linearArcAdjacentGapPx));
1616
+ lsSet(GEN_ATTR_DAG_EDGE_TOP_P_COVERAGE_STORAGE_KEY, String(snap.edgeTopPCoverage));
1617
+ lsWriteBool(GEN_ATTR_DAG_HIDE_EXCLUDED_TOKENS_STORAGE_KEY, snap.hideExcludedTokens, '1');
1618
+ lsWriteBool(GEN_ATTR_DAG_DIM_INACTIVE_TOKENS_STORAGE_KEY, snap.dimInactiveTokens, '1');
1619
+ lsSet(GEN_ATTR_DAG_DIM_INACTIVE_TOKENS_THRESHOLD_STORAGE_KEY, String(snap.dimInactiveTokensThreshold));
1620
+ lsWriteBool(
1621
+ GEN_ATTR_DAG_DIM_INACTIVE_NOT_IN_ANIMATION_STORAGE_KEY,
1622
+ snap.dimInactiveNotDuringAnimation,
1623
+ '1',
1624
+ );
1625
+ lsWriteBool(GEN_ATTR_DAG_NODE_CI_VISUAL_SCALE_STORAGE_KEY, snap.nodeCiVisualScaleEnabled, '1');
1626
+ lsWriteBool(
1627
+ GEN_ATTR_DAG_DECAY_ATTRIBUTION_HIGH_SURPRISAL_STORAGE_KEY,
1628
+ snap.decayAttributionToHighSurprisalTargetEnabled,
1629
+ '1',
1630
+ );
1631
+ lsWriteBool(GEN_ATTR_DAG_HIDE_INACTIVE_EDGES_STORAGE_KEY, snap.hideInactiveEdges, '1');
1632
+ lsWriteBool(GEN_ATTR_DAG_SHOW_DOWNSTREAM_INFLUENCE_STORAGE_KEY, snap.showDownstreamInfluence, '1');
1633
+ lsWriteBool(GEN_ATTR_DAG_RECURSIVE_ATTRIBUTION_STORAGE_KEY, snap.recursiveAttributionEnabled, '1');
1634
+ lsWriteString(
1635
+ GEN_ATTR_DAG_RECURSIVE_EDGE_ANIMATION_DIRECTION_STORAGE_KEY,
1636
+ snap.recursiveEdgeBatchAnimationDirection,
1637
+ );
1638
+ lsWriteBool(GEN_ATTR_DAG_SHOW_TOPK_ON_SELECTED_STORAGE_KEY, snap.showTokenInfoOnSelected, '1');
1639
+ lsWriteString(GEN_ATTR_DAG_REPLAY_PACING_MODE_STORAGE_KEY, snap.replayPacingMode);
1640
+ lsWriteBool(GEN_ATTR_DAG_REPLAY_AUTO_ZOOM_STORAGE_KEY, snap.replayAutoZoom, '1');
1641
+ lsSet(GEN_ATTR_DAG_PLAYBACK_TOTAL_S_STORAGE_KEY, String(snap.playbackTotalS));
1642
+ lsSet(GEN_ATTR_DAG_PLAYBACK_STEP_MS_STORAGE_KEY, String(snap.playbackStepMs));
1643
+ lsSet(GEN_ATTR_DELETE_PROMPT_PATTERNS_STORAGE_KEY, snap.deletePromptPatternsText);
1644
+ lsWriteBool(GEN_ATTR_DELETE_PROMPT_PATTERNS_ENABLED_STORAGE_KEY, snap.deletePromptPatternsEnabled, '1');
1645
+ lsSet(GEN_ATTR_EXCLUDE_PROMPT_PATTERNS_STORAGE_KEY, snap.excludePromptPatternsText);
1646
+ lsWriteBool(GEN_ATTR_EXCLUDE_PROMPT_PATTERNS_ENABLED_STORAGE_KEY, snap.excludePromptPatternsEnabled, '1');
1647
+ lsSet(GEN_ATTR_EXCLUDE_GENERATED_PATTERNS_STORAGE_KEY, snap.excludeGeneratedPatternsText);
1648
+ lsWriteBool(
1649
+ GEN_ATTR_EXCLUDE_GENERATED_PATTERNS_ENABLED_STORAGE_KEY,
1650
+ snap.excludeGeneratedPatternsEnabled,
1651
+ '1',
1652
+ );
1653
+ }
1654
+
1655
+ /** 演示用 UI:DOM → localStorage 的唯一写路径(用户改控件经面板委托;程序化 apply 末尾调用)。 */
1656
+ function syncGenAttrDemoUiOptionsToLocalStorage(): void {
1657
+ persistGenAttrDemoUiOptionsToLocalStorage(readGenAttrDemoUiOptionsFromControls());
1658
+ }
1659
+
1660
+ /** 从 `demoUiOptions` 还原排除控件(仅 DOM);`replay` 读当前控件生效。 */
1661
  function applyGenAttrExcludePatternsFromDemoUiSnap(snap: Partial<GenAttrDemoUiOptions>): void {
1662
  const {
1663
  deletePromptPatternsEnabled,
 
1809
  if (dagReplayModeSelect) dagReplayModeSelect.value = snap.replayPacingMode;
1810
  applyDagReplaySpeedUi();
1811
  }
1812
+ if (snap.replayAutoZoom !== undefined) {
1813
+ if (dagReplayAutoZoomInput) dagReplayAutoZoomInput.checked = snap.replayAutoZoom;
1814
+ }
1815
  if (snap.playbackTotalS !== undefined) {
1816
  const s = clampDagPlaybackTotalS(snap.playbackTotalS);
1817
+ if (dagPlaybackTotalSInput) dagPlaybackTotalSInput.value = formatDagPlaybackTotalS(s);
1818
  }
1819
  if (snap.playbackStepMs !== undefined) {
1820
  const ms = clampDagPlaybackStepMs(snap.playbackStepMs);
 
1822
  }
1823
 
1824
  applyGenAttrExcludePatternsFromDemoUiSnap(snap);
1825
+ syncGenAttrDemoUiOptionsToLocalStorage();
1826
  syncGenAttrResetUiOptionsButtonState();
1827
  }
1828
 
 
1845
  applyGenAttrDemoUiOptionsSnap(rec.demoUiOptions);
1846
  }
1847
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1848
  /** 重置「DAG 演示用 UI」:清 LS 后以 {@link DEFAULT_GEN_ATTR_DEMO_UI_OPTIONS} 全量套用。 */
1849
  function resetGenAttrDemoUiOptionsToDefaults(): void {
1850
  stopDagPlayback();
 
1858
  (() => {
1859
  const panel = document.querySelector('.gen-attribute-page .input-section');
1860
  if (!panel) return;
1861
+ const onDemoUiPersist = (e: Event) => {
1862
+ if (!isGenAttrDemoUiControl(e.target)) return;
1863
+ syncGenAttrDemoUiOptionsToLocalStorage();
1864
+ syncGenAttrResetUiOptionsButtonState();
1865
+ };
1866
+ panel.addEventListener('change', onDemoUiPersist);
1867
+ panel.addEventListener('blur', onDemoUiPersist, true);
1868
+ panel.addEventListener('input', (e) => {
1869
+ if (!isGenAttrDemoUiControl(e.target)) return;
1870
+ syncGenAttrResetUiOptionsButtonState();
1871
+ });
1872
+ syncGenAttrResetUiOptionsButtonState();
1873
  })();
1874
 
1875
  window.addEventListener('pagehide', (ev) => {
client/src/shared/lang/translations.ts CHANGED
@@ -40,11 +40,15 @@ export const translations: Translations = {
40
  'Max new tokens must not exceed {limit}': 'Max new tokens 不得超过 {limit}',
41
  'Max new tokens must be a positive integer': 'Max new tokens 须为正整数',
42
  // LLM Causal Flow(gen_attribute)页:placeholder / title 文案
43
- 'When enabled, each line below is a regex with the global flag, matched only within the initial static prompt prefix (excluding generated continuation). If a token offset lies fully inside a match, its score is treated as 0.':
44
- '启用后仅在初始静态 prompt 前缀内按下列正则匹配(不含已生成 continuation);token offset 完全落在某次匹配区间内则 score 视为 0。',
45
- 'One regex per line (prompt prefix only)': '每行一条正则(仅 prompt 前缀)',
46
- 'One regex per line (global flag), matched only within the initial prompt prefix; if a token offset lies fully inside a match, its score is treated as 0.':
47
- '每行一条正则,`g`,仅在初始 prompt 前缀内匹配;token offset 完全落在某次匹配区间内则 score 视为 0',
 
 
 
 
48
  'When enabled, each line below is a regex with the global flag, matched only within the model-generated continuation (excluding the initial static prompt). If a token offset lies fully inside a match, its score is treated as 0.':
49
  '启用后仅在模型已生成的 continuation(不含初始静态 prompt)内按下列正则匹配;token 的 offset 完全落在某次匹配区间内则 score 视为 0。',
50
  'One regex per line (generated continuation only)': '每行一条正则(仅已生成 continuation)',
@@ -127,6 +131,9 @@ export const translations: Translations = {
127
  'Restore DAG options, play speed, exclusions, etc. to defaults and clear saved preferences for those controls.':
128
  '将 DAG 参数、播放速度、排除正则等恢复为默认值,并清除这些控件的本地保存项。',
129
  'Play speed': '播放速度',
 
 
 
130
  'Causal Flow Mode': '因果流模式',
131
  'Unchecked: direct attribution — immediate predecessors only (default). Checked: Causal Flow Mode (↯) — trace from the focused token back to information sources. Sources: prompt; surprising or teacher-forced generated tokens (MI decay stops the chain). Conduits: high-confidence middle tokens—attribution passes through. Blue edges: propagated share; node ring: attribution stay (strong where explanation lands). Use with Decay attribution to high-surprisal targets. On the DAG, ↯ plays the focus propagation chain; ▶ replays generation steps when no token is focused.':
132
  '未勾选:原始直接归因(仅一跳前驱,默认)。勾选:因果流模式(↯)— 从焦点 token 向上追溯到信息来源。来源:prompt;高惊讶或 teacher-forced 的生成 token(MI 衰减可截断链)。传导:高置信中间 token,归因穿过。蓝边:传播份额;节点环:归因停留(解释落点处更强)。建议与「向高惊讶目标衰减归因」配合使用。DAG 上:↯ 播放焦点传��链;无焦点时 ▶ 步进重放生成过程。',
 
40
  'Max new tokens must not exceed {limit}': 'Max new tokens 不得超过 {limit}',
41
  'Max new tokens must be a positive integer': 'Max new tokens 须为正整数',
42
  // LLM Causal Flow(gen_attribute)页:placeholder / title 文案
43
+ 'When enabled, each line below is a regex with the global flag, matched only within input areas (excluding generated continuation). Matched prompt tokens are physically removed from the DAG they neither appear nor occupy layout space (stricter than Exclude + Hide).':
44
+ '启用后仅在 input (不含已生成 continuation)内按下列正则匹配命中的 prompt token DAG 物理移除,不占布局(比 Exclude + Hide 更严格)。',
45
+ 'One regex per line (global flag), matched only within input areas (excluding generated continuation); matched tokens are deleted from the DAG and do not occupy layout space.':
46
+ '每行一条正则,`g`,仅在 input 区(不含已生成 continuation)内匹配;命中的 token DAG 删除且不占布局空间。',
47
+ 'When enabled, each line below is a regex with the global flag, matched only within input areas (excluding generated continuation). If a token offset lies fully inside a match, its score is treated as 0.':
48
+ '启用后仅在 input 区(不含已生成 continuation)内按下列正则匹配;token 的 offset 完全落在某次匹配区间内则 score 视为 0。',
49
+ 'One regex per line (input areas only)': '每行一条正则(仅 input 区)',
50
+ 'One regex per line (global flag), matched only within input areas (excluding generated continuation); if a token offset lies fully inside a match, its score is treated as 0.':
51
+ '每行一条正则,`g`,仅在 input 区(不含已生成 continuation)内匹配;token 的 offset 完全落在某次匹配区间内则 score 视为 0。',
52
  'When enabled, each line below is a regex with the global flag, matched only within the model-generated continuation (excluding the initial static prompt). If a token offset lies fully inside a match, its score is treated as 0.':
53
  '启用后仅在模型已生成的 continuation(不含初始静态 prompt)内按下列正则匹配;token 的 offset 完全落在某次匹配区间内则 score 视为 0。',
54
  'One regex per line (generated continuation only)': '每行一条正则(仅已生成 continuation)',
 
131
  'Restore DAG options, play speed, exclusions, etc. to defaults and clear saved preferences for those controls.':
132
  '将 DAG 参数、播放速度、排除正则等恢复为默认值,并清除这些控件的本地保存项。',
133
  'Play speed': '播放速度',
134
+ 'Auto zoom': '自动缩放',
135
+ 'When checked, step replay (▶) fits the viewport after each step (stops if you pan or zoom). Saved locally.':
136
+ '勾选后,步进重放(▶)每步自动适配视口(手动平移/缩放后停止)。本地保存。',
137
  'Causal Flow Mode': '因果流模式',
138
  'Unchecked: direct attribution — immediate predecessors only (default). Checked: Causal Flow Mode (↯) — trace from the focused token back to information sources. Sources: prompt; surprising or teacher-forced generated tokens (MI decay stops the chain). Conduits: high-confidence middle tokens—attribution passes through. Blue edges: propagated share; node ring: attribution stay (strong where explanation lands). Use with Decay attribution to high-surprisal targets. On the DAG, ↯ plays the focus propagation chain; ▶ replays generation steps when no token is focused.':
139
  '未勾选:原始直接归因(仅一跳前驱,默认)。勾选:因果流模式(↯)— 从焦点 token 向上追溯到信息来源。来源:prompt;高惊讶或 teacher-forced 的生成 token(MI 衰减可截断链)。传导:高置信中间 token,归因穿过。蓝边:传播份额;节点环:归因停留(解释落点处更强)。建议与「向高惊讶目标衰减归因」配合使用。DAG 上:↯ 播放焦点传��链;无焦点时 ▶ 步进重放生成过程。',
client/src/shared/prediction_attribution/causal_flow/genAttributeDagStepPlayback.ts CHANGED
@@ -140,9 +140,11 @@ export type RunDagStepPlaybackLoopOptions = {
140
  isStale: () => boolean;
141
  setTimer: (cb: () => void, delayMs: number) => void;
142
  setToolPendingVisible: (visible: boolean) => void;
143
- showPrompt: (fitViewport: boolean) => void;
144
  showToolResponse: (stepIndex: number) => void;
145
  showOutputGen: (stepIndex: number) => void;
 
 
146
  onOutputGenShown: (stepIndex: number) => void;
147
  onAllOutputGensShown: () => void;
148
  };
@@ -168,7 +170,7 @@ export function runDagStepPlaybackLoop(opts: RunDagStepPlaybackLoopOptions): voi
168
 
169
  switch (event.kind) {
170
  case 'prompt':
171
- opts.showPrompt(true);
172
  break;
173
  case 'toolResponse':
174
  opts.showToolResponse(event.stepIndex);
@@ -182,6 +184,7 @@ export function runDagStepPlaybackLoop(opts: RunDagStepPlaybackLoopOptions): voi
182
  void _exhaustive;
183
  }
184
  }
 
185
 
186
  const nextIndex = eventIndex + 1;
187
  if (nextIndex >= opts.events.length) {
 
140
  isStale: () => boolean;
141
  setTimer: (cb: () => void, delayMs: number) => void;
142
  setToolPendingVisible: (visible: boolean) => void;
143
+ showPrompt: () => void;
144
  showToolResponse: (stepIndex: number) => void;
145
  showOutputGen: (stepIndex: number) => void;
146
+ /** 每段内容展示后调用(如步进重放开启 Auto zoom 时 fit 视口)。 */
147
+ afterStepShown?: () => void;
148
  onOutputGenShown: (stepIndex: number) => void;
149
  onAllOutputGensShown: () => void;
150
  };
 
170
 
171
  switch (event.kind) {
172
  case 'prompt':
173
+ opts.showPrompt();
174
  break;
175
  case 'toolResponse':
176
  opts.showToolResponse(event.stepIndex);
 
184
  void _exhaustive;
185
  }
186
  }
187
+ opts.afterStepShown?.();
188
 
189
  const nextIndex = eventIndex + 1;
190
  if (nextIndex >= opts.events.length) {
client/src/shared/prediction_attribution/causal_flow/genAttributeDagTextMeasure.ts CHANGED
@@ -199,12 +199,15 @@ export function createGenAttributeDagTextMeasure(
199
  } {
200
  // 合并重叠区间,保证 originalToCompacted 与 compactText 行为一致。
201
  let mergedDeleteIntervals = mergeDeleteIntervals(deleteIntervals);
202
- /** 当前测量层中的完整文本(prompt 部分已压缩 + 生成 token 原样拼接)。 */
203
  let fullText = '';
204
- /** 原始 prompt 长度(即 setPrompt/appendInputSpans 最后一次传入的 promptText/layoutText 长度)。 */
205
- let originalPromptLength = 0;
206
- /** 压缩后 prompt 的长度(deleteIntervals 已移除部分不计)。 */
207
- let compactedPromptLength = 0;
 
 
 
208
  let bpeStrings: FrontendToken[] = [];
209
  const calculator = new TokenPositionCalculator(measureRoot);
210
 
@@ -229,8 +232,8 @@ export function createGenAttributeDagTextMeasure(
229
  return {
230
  reset(): void {
231
  fullText = '';
232
- originalPromptLength = 0;
233
- compactedPromptLength = 0;
234
  bpeStrings = [];
235
  while (measureRoot.firstChild) measureRoot.removeChild(measureRoot.firstChild);
236
  calculator.resetIndex();
@@ -247,8 +250,8 @@ export function createGenAttributeDagTextMeasure(
247
  setPrompt(promptText: string, spans: PromptTokenSpan[]): Map<string, GenAttrDagTokenGeom> {
248
  // 将被删区间字符物理移除,DOM 里不留空洞。
249
  const compactedPrompt = compactText(promptText, mergedDeleteIntervals);
250
- originalPromptLength = promptText.length;
251
- compactedPromptLength = compactedPrompt.length;
252
  fullText = compactedPrompt;
253
 
254
  // 过滤掉被删 span(调用方也会跳过,双重保险)。
@@ -291,8 +294,8 @@ export function createGenAttributeDagTextMeasure(
291
  'genAttributeDagTextMeasure: appendInputSpans layoutText must extend current fullText prefix',
292
  );
293
  }
294
- originalPromptLength = layoutText.length;
295
- compactedPromptLength = compactedLayout.length;
296
  fullText = compactedLayout;
297
 
298
  const keptNew = newSpans.filter(
@@ -328,9 +331,9 @@ export function createGenAttributeDagTextMeasure(
328
 
329
  appendGeneratedToken(token: string, offset: [number, number]): GenAttrDagTokenGeom {
330
  // offset[0] 是原始坐标系下该 token 在全文中的起点(= context.length at generation time)。
331
- // 映射到压缩坐标系:prompt 字符删除,仅需减去 prompt 被删除的总字符数
332
- const generatedRelative = offset[0] - originalPromptLength;
333
- const compactedStart = compactedPromptLength + generatedRelative;
334
  const compactedOffset: [number, number] = [compactedStart, compactedStart + token.length];
335
 
336
  fullText += token;
 
199
  } {
200
  // 合并重叠区间,保证 originalToCompacted 与 compactText 行为一致。
201
  let mergedDeleteIntervals = mergeDeleteIntervals(deleteIntervals);
202
+ /** 当前测量层中的完整文本(prompt 前缀已压缩 + 生成 token 原样拼接)。 */
203
  let fullText = '';
204
+ /**
205
+ * 最近一次 input 同步时 `layoutWire` 在原始坐标系下的长度(`setPrompt` 为 prompt 长度;
206
+ * `appendInputSpans` 为含 tool response 等长)。 {@link appendGeneratedToken} 锚定生成区起点。
207
+ */
208
+ let layoutAnchorLength = 0;
209
+ /** 与 {@link layoutAnchorLength} 同锚点、经 delete 压缩后的坐标长度。 */
210
+ let compactedLayoutAnchorLength = 0;
211
  let bpeStrings: FrontendToken[] = [];
212
  const calculator = new TokenPositionCalculator(measureRoot);
213
 
 
232
  return {
233
  reset(): void {
234
  fullText = '';
235
+ layoutAnchorLength = 0;
236
+ compactedLayoutAnchorLength = 0;
237
  bpeStrings = [];
238
  while (measureRoot.firstChild) measureRoot.removeChild(measureRoot.firstChild);
239
  calculator.resetIndex();
 
250
  setPrompt(promptText: string, spans: PromptTokenSpan[]): Map<string, GenAttrDagTokenGeom> {
251
  // 将被删区间字符物理移除,DOM 里不留空洞。
252
  const compactedPrompt = compactText(promptText, mergedDeleteIntervals);
253
+ layoutAnchorLength = promptText.length;
254
+ compactedLayoutAnchorLength = compactedPrompt.length;
255
  fullText = compactedPrompt;
256
 
257
  // 过滤掉被删 span(调用方也会跳过,双重保险)。
 
294
  'genAttributeDagTextMeasure: appendInputSpans layoutText must extend current fullText prefix',
295
  );
296
  }
297
+ layoutAnchorLength = layoutText.length;
298
+ compactedLayoutAnchorLength = compactedLayout.length;
299
  fullText = compactedLayout;
300
 
301
  const keptNew = newSpans.filter(
 
331
 
332
  appendGeneratedToken(token: string, offset: [number, number]): GenAttrDagTokenGeom {
333
  // offset[0] 是原始坐标系下该 token 在全文中的起点(= context.length at generation time)。
334
+ // 映射到压缩坐标系: layoutAnchor 之后生成区delete,按锚点相对偏移平移
335
+ const generatedRelative = offset[0] - layoutAnchorLength;
336
+ const compactedStart = compactedLayoutAnchorLength + generatedRelative;
337
  const compactedOffset: [number, number] = [compactedStart, compactedStart + token.length];
338
 
339
  fullText += token;
client/src/shared/prediction_attribution/causal_flow/genAttributeDagView.ts CHANGED
@@ -592,8 +592,8 @@ export type GenAttributeDagHandle = {
592
  /**
593
  * 清空图与测量状态;不修改当前 SVG 上的 d3 zoom 变换(视口平移/缩放由 `layoutDirty` 与
594
  * `fitViewportToContent` 控制)。
595
- * @param preserveUserViewport 为 `true` 时保留调用前的 `layoutDirty`:用于「从末尾播前 reset」
596
- * 时若用户已手动画布,重放后仍不自动 fit。默认 `false`(新一次 run 等场景仍从干净视口策略起算)。
597
  */
598
  reset(preserveUserViewport?: boolean): void;
599
  /**
@@ -1358,6 +1358,9 @@ export function initGenAttributeDagView(
1358
  );
1359
  }
1360
 
 
 
 
1361
  /**
1362
  * Hide exclude/inactive 时,参与布局的节点集随焦点 / dim 阈值变化;须重算 linear-arc / spiral 几何。
1363
  */
@@ -1372,14 +1375,14 @@ export function initGenAttributeDagView(
1372
  if (layoutIncludedNodeIdsKey === LAYOUT_INCLUDED_ALL_KEY) return;
1373
  layoutIncludedNodeIdsKey = LAYOUT_INCLUDED_ALL_KEY;
1374
  paint();
1375
- if (!layoutDirty) fitViewportToContent(true);
1376
  return;
1377
  }
1378
  const key = computeLayoutIncludedNodeIdsKey(focusId, focusState);
1379
  if (key === layoutIncludedNodeIdsKey) return;
1380
  layoutIncludedNodeIdsKey = key;
1381
  paint();
1382
- if (!layoutDirty) fitViewportToContent(true);
1383
  }
1384
 
1385
  function invalidateLayoutIncludedNodeIdsKey(): void {
@@ -1425,7 +1428,7 @@ export function initGenAttributeDagView(
1425
  */
1426
  let dagExcludeIntervals: [number, number][] = [];
1427
  /**
1428
- * 每次 {@link setPromptTokenSpans} 按当前 `layoutWire` + `inputRanges` 重算(与 exclude 一致;多轮追加 input 区时扩展)。
1429
  * 命中区间内的 prompt token 不进入图也不进入测量层(textMeasure 物理压缩布局空间)。
1430
  */
1431
  let dagDeleteIntervals: [number, number][] = [];
@@ -2199,9 +2202,10 @@ export function initGenAttributeDagView(
2199
  recursiveEdgeAnimation.stopPlayback();
2200
  if (batchDepth === 0) {
2201
  syncGraphToSvg();
2202
- // 生成 / 单回放 / DAG 步进放(均非批内每步 `fitViewportToContent()`;其内部在
2203
- // `layoutDirty` 时 no-op。整段批回放仅 `endBatch` → `syncGraphToSvg`,fit 由调用方或 RO。
2204
- fitViewportToContent();
 
2205
  }
2206
  }
2207
 
@@ -2330,7 +2334,6 @@ export function initGenAttributeDagView(
2330
  });
2331
  ro.observe(stackEl);
2332
 
2333
- let dagPlaybackPlaying = false;
2334
  const playBtn = resultsRoot
2335
  .append('button')
2336
  .attr('type', 'button')
 
592
  /**
593
  * 清空图与测量状态;不修改当前 SVG 上的 d3 zoom 变换(视口平移/缩放由 `layoutDirty` 与
594
  * `fitViewportToContent` 控制)。
595
+ * @param preserveUserViewport 为 `true` 时保留调用前的 `layoutDirty`:设置项切换后放、
596
+ * 步进重放从末尾重头播放等保留用户 pan/zoom。默认 `false`(新一次 run 等场景仍从干净视口起算)。
597
  */
598
  reset(preserveUserViewport?: boolean): void;
599
  /**
 
1358
  );
1359
  }
1360
 
1361
+ /** 步进重放(▶)期间为 true;fit 由页面 `afterStepShown` + Auto zoom 统一处理,见 {@link syncLayoutForLowVisibilityMembership}。 */
1362
+ let dagPlaybackPlaying = false;
1363
+
1364
  /**
1365
  * Hide exclude/inactive 时,参与布局的节点集随焦点 / dim 阈值变化;须重算 linear-arc / spiral 几何。
1366
  */
 
1375
  if (layoutIncludedNodeIdsKey === LAYOUT_INCLUDED_ALL_KEY) return;
1376
  layoutIncludedNodeIdsKey = LAYOUT_INCLUDED_ALL_KEY;
1377
  paint();
1378
+ if (!layoutDirty && !dagPlaybackPlaying) fitViewportToContent(true);
1379
  return;
1380
  }
1381
  const key = computeLayoutIncludedNodeIdsKey(focusId, focusState);
1382
  if (key === layoutIncludedNodeIdsKey) return;
1383
  layoutIncludedNodeIdsKey = key;
1384
  paint();
1385
+ if (!layoutDirty && !dagPlaybackPlaying) fitViewportToContent(true);
1386
  }
1387
 
1388
  function invalidateLayoutIncludedNodeIdsKey(): void {
 
1428
  */
1429
  let dagExcludeIntervals: [number, number][] = [];
1430
  /**
1431
+ * 每次 {@link setPromptTokenSpans} 按 `layoutWire` + `inputRanges` 重算(与 exclude 一致;多轮追加 input 区时扩展)。
1432
  * 命中区间内的 prompt token 不进入图也不进入测量层(textMeasure 物理压缩布局空间)。
1433
  */
1434
  let dagDeleteIntervals: [number, number][] = [];
 
2202
  recursiveEdgeAnimation.stopPlayback();
2203
  if (batchDepth === 0) {
2204
  syncGraphToSvg();
2205
+ // 生成时每fit;步进放(由页面按 Auto zoom 在 `afterStepShown` 统一处理。
2206
+ if (!dagPlaybackPlaying) {
2207
+ fitViewportToContent();
2208
+ }
2209
  }
2210
  }
2211
 
 
2334
  });
2335
  ro.observe(stackEl);
2336
 
 
2337
  const playBtn = resultsRoot
2338
  .append('button')
2339
  .attr('type', 'button')
client/src/shared/prediction_attribution/core/excludePromptPatternsUi.ts CHANGED
@@ -20,6 +20,8 @@ export type BindExcludePatternsUiOptions = {
20
  defaultTextWhenKeyAbsent?: string;
21
  /** enable 键从未写入时的默认勾选态;缺省 `true`(与 Attribution Exclude 一致) */
22
  defaultEnabledWhenKeyAbsent?: boolean;
 
 
23
  };
24
 
25
  export type BindExcludePromptPatternsUiOptions = Omit<
@@ -46,6 +48,7 @@ export function bindExcludePatternsUi(options: BindExcludePatternsUiOptions): vo
46
  onEffectiveChange,
47
  defaultTextWhenKeyAbsent,
48
  defaultEnabledWhenKeyAbsent = true,
 
49
  } = options;
50
  const { textKey, enabledKey } = storageKeys;
51
 
@@ -70,16 +73,20 @@ export function bindExcludePatternsUi(options: BindExcludePatternsUiOptions): vo
70
  syncEnableGatedTextInputVisibility(enableCheckbox, textInput);
71
 
72
  enableCheckbox?.addEventListener('change', () => {
73
- if (textInput) {
74
- lsSet(textKey, textInput.value);
 
 
 
75
  }
76
- lsWriteBool(enabledKey, enableCheckbox.checked, '1');
77
  syncEnableGatedTextInputVisibility(enableCheckbox, textInput);
78
  onEffectiveChange();
79
  });
80
 
81
  textInput?.addEventListener('blur', () => {
82
- lsSet(textKey, textInput.value);
 
 
83
  onEffectiveChange();
84
  });
85
 
 
20
  defaultTextWhenKeyAbsent?: string;
21
  /** enable 键从未写入时的默认勾选态;缺省 `true`(与 Attribution Exclude 一致) */
22
  defaultEnabledWhenKeyAbsent?: boolean;
23
+ /** 为 true 时不写 localStorage(由调用方统一 sync,如 Gen Attribute demo UI 面板委托) */
24
+ skipLocalStoragePersist?: boolean;
25
  };
26
 
27
  export type BindExcludePromptPatternsUiOptions = Omit<
 
48
  onEffectiveChange,
49
  defaultTextWhenKeyAbsent,
50
  defaultEnabledWhenKeyAbsent = true,
51
+ skipLocalStoragePersist = false,
52
  } = options;
53
  const { textKey, enabledKey } = storageKeys;
54
 
 
73
  syncEnableGatedTextInputVisibility(enableCheckbox, textInput);
74
 
75
  enableCheckbox?.addEventListener('change', () => {
76
+ if (!skipLocalStoragePersist) {
77
+ if (textInput) {
78
+ lsSet(textKey, textInput.value);
79
+ }
80
+ lsWriteBool(enabledKey, enableCheckbox.checked, '1');
81
  }
 
82
  syncEnableGatedTextInputVisibility(enableCheckbox, textInput);
83
  onEffectiveChange();
84
  });
85
 
86
  textInput?.addEventListener('blur', () => {
87
+ if (!skipLocalStoragePersist) {
88
+ lsSet(textKey, textInput.value);
89
+ }
90
  onEffectiveChange();
91
  });
92
 
client/src/shared/storage/genAttributeRunCache.ts CHANGED
@@ -91,6 +91,8 @@ export type GenAttrDemoUiOptions = {
91
  /** 是否显示 token tooltip(UI: Show token tooltip;`showTokenInfoOnSelected`)。 */
92
  showTokenInfoOnSelected: boolean;
93
  replayPacingMode: 'total' | 'step';
 
 
94
  playbackTotalS: number;
95
  playbackStepMs: number;
96
  /** 删除 prompt token(物理移除,不占布局):使能与正则文本(`info_radar_gen_attr_delete_prompt_*`)。 */
@@ -332,6 +334,9 @@ function isValidDemoUiOptionsPayload(v: unknown): v is Partial<GenAttrDemoUiOpti
332
  if (d.replayPacingMode !== undefined && d.replayPacingMode !== 'total' && d.replayPacingMode !== 'step') {
333
  return false;
334
  }
 
 
 
335
  if (d.playbackTotalS !== undefined && (typeof d.playbackTotalS !== 'number' || !Number.isFinite(d.playbackTotalS))) {
336
  return false;
337
  }
 
91
  /** 是否显示 token tooltip(UI: Show token tooltip;`showTokenInfoOnSelected`)。 */
92
  showTokenInfoOnSelected: boolean;
93
  replayPacingMode: 'total' | 'step';
94
+ /** 步进重放(▶)每步是否自动 fit 视口。 */
95
+ replayAutoZoom: boolean;
96
  playbackTotalS: number;
97
  playbackStepMs: number;
98
  /** 删除 prompt token(物理移除,不占布局):使能与正则文本(`info_radar_gen_attr_delete_prompt_*`)。 */
 
334
  if (d.replayPacingMode !== undefined && d.replayPacingMode !== 'total' && d.replayPacingMode !== 'step') {
335
  return false;
336
  }
337
+ if (d.replayAutoZoom !== undefined && typeof d.replayAutoZoom !== 'boolean') {
338
+ return false;
339
+ }
340
  if (d.playbackTotalS !== undefined && (typeof d.playbackTotalS !== 'number' || !Number.isFinite(d.playbackTotalS))) {
341
  return false;
342
  }
client/src/tests/prediction_attribution/genAttributeDagPreprocess.test.ts ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * DAG 预处理:delete prompt 区间收集
3
+ * 运行: cd client/src && npx tsx tests/prediction_attribution/genAttributeDagPreprocess.test.ts
4
+ */
5
+ import { collectDeletePromptIntervals } from '../../shared/prediction_attribution/causal_flow/genAttributeDagPreprocess';
6
+
7
+ let passed = 0;
8
+ let failed = 0;
9
+
10
+ function assert(label: string, cond: boolean): void {
11
+ if (cond) {
12
+ passed++;
13
+ console.log(` ✓ ${label}`);
14
+ } else {
15
+ failed++;
16
+ console.log(` ✗ ${label}`);
17
+ }
18
+ }
19
+
20
+ function assertEq<T>(label: string, actual: T, expected: T): void {
21
+ assert(label, JSON.stringify(actual) === JSON.stringify(expected));
22
+ }
23
+
24
+ const wire = 'PROMPT_END|TOOL_RESPONSE';
25
+ const inputRanges: [number, number][] = [
26
+ [0, 11],
27
+ [11, wire.length],
28
+ ];
29
+
30
+ console.log('collectDeletePromptIntervals');
31
+
32
+ assertEq('empty inputRanges → []', collectDeletePromptIntervals(wire, [], 'TOOL'), []);
33
+
34
+ assertEq(
35
+ 'match only in inputRanges[0]',
36
+ collectDeletePromptIntervals(wire, inputRanges, 'PROMPT'),
37
+ [[0, 6]],
38
+ );
39
+
40
+ assertEq(
41
+ 'match in later input range',
42
+ collectDeletePromptIntervals(wire, inputRanges, 'TOOL'),
43
+ [[11, 15]],
44
+ );
45
+
46
+ assertEq('empty pattern → []', collectDeletePromptIntervals(wire, inputRanges, ''), []);
47
+
48
+ console.log(`\n${passed} passed, ${failed} failed`);
49
+ if (failed > 0) process.exit(1);