dqy08 commited on
Commit
4bd7747
·
1 Parent(s): 0c4dc0a

Demo支持完整UI快照;增加新的demo;其它小改进

Browse files
client/src/demos/gen_attribute/{翻译.json → CN->EN翻译.json} RENAMED
The diff for this file is too large to render. See raw diff
 
client/src/demos/gen_attribute/Write a sonnet about love.json CHANGED
@@ -117181,5 +117181,240 @@
117181
  "token": "<|im_end|>",
117182
  "currentText": "In twilight's hush, where shadows play, \nLove whispers soft, a gentle ray. \nIt blooms in hearts, a fleeting flame, \nThat lights the darkest, darkest lane. \n\nIt seeks the soul, with tender grace, \nTo mend the wounds, to heal the face. \nThrough joy and sorrow, it remains, \nA constant flame in life's vast sea. \n\nYet love is not a rose that fades, \nBut seeds that grow in time and space. \nIt bends, it breaks, it grows again, \nA promise etched in every pain. \n\nSo let the world proclaim its might, \nBut in the heart, love's truest light.<|im_end|>"
117183
  }
117184
- ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
117185
  }
 
117181
  "token": "<|im_end|>",
117182
  "currentText": "In twilight's hush, where shadows play, \nLove whispers soft, a gentle ray. \nIt blooms in hearts, a fleeting flame, \nThat lights the darkest, darkest lane. \n\nIt seeks the soul, with tender grace, \nTo mend the wounds, to heal the face. \nThrough joy and sorrow, it remains, \nA constant flame in life's vast sea. \n\nYet love is not a rose that fades, \nBut seeds that grow in time and space. \nIt bends, it breaks, it grows again, \nA promise etched in every pain. \n\nSo let the world proclaim its might, \nBut in the heart, love's truest light.<|im_end|>"
117183
  }
117184
+ ],
117185
+ "promptSpans": [
117186
+ {
117187
+ "offset": [
117188
+ 0,
117189
+ 12
117190
+ ],
117191
+ "raw": "<|im_start|>"
117192
+ },
117193
+ {
117194
+ "offset": [
117195
+ 12,
117196
+ 18
117197
+ ],
117198
+ "raw": "system"
117199
+ },
117200
+ {
117201
+ "offset": [
117202
+ 18,
117203
+ 19
117204
+ ],
117205
+ "raw": "\n"
117206
+ },
117207
+ {
117208
+ "offset": [
117209
+ 19,
117210
+ 22
117211
+ ],
117212
+ "raw": "You"
117213
+ },
117214
+ {
117215
+ "offset": [
117216
+ 22,
117217
+ 26
117218
+ ],
117219
+ "raw": " are"
117220
+ },
117221
+ {
117222
+ "offset": [
117223
+ 26,
117224
+ 28
117225
+ ],
117226
+ "raw": " a"
117227
+ },
117228
+ {
117229
+ "offset": [
117230
+ 28,
117231
+ 36
117232
+ ],
117233
+ "raw": " helpful"
117234
+ },
117235
+ {
117236
+ "offset": [
117237
+ 36,
117238
+ 46
117239
+ ],
117240
+ "raw": " assistant"
117241
+ },
117242
+ {
117243
+ "offset": [
117244
+ 46,
117245
+ 47
117246
+ ],
117247
+ "raw": "."
117248
+ },
117249
+ {
117250
+ "offset": [
117251
+ 47,
117252
+ 57
117253
+ ],
117254
+ "raw": "<|im_end|>"
117255
+ },
117256
+ {
117257
+ "offset": [
117258
+ 57,
117259
+ 58
117260
+ ],
117261
+ "raw": "\n"
117262
+ },
117263
+ {
117264
+ "offset": [
117265
+ 58,
117266
+ 70
117267
+ ],
117268
+ "raw": "<|im_start|>"
117269
+ },
117270
+ {
117271
+ "offset": [
117272
+ 70,
117273
+ 74
117274
+ ],
117275
+ "raw": "user"
117276
+ },
117277
+ {
117278
+ "offset": [
117279
+ 74,
117280
+ 75
117281
+ ],
117282
+ "raw": "\n"
117283
+ },
117284
+ {
117285
+ "offset": [
117286
+ 75,
117287
+ 80
117288
+ ],
117289
+ "raw": "Write"
117290
+ },
117291
+ {
117292
+ "offset": [
117293
+ 80,
117294
+ 82
117295
+ ],
117296
+ "raw": " a"
117297
+ },
117298
+ {
117299
+ "offset": [
117300
+ 82,
117301
+ 86
117302
+ ],
117303
+ "raw": " son"
117304
+ },
117305
+ {
117306
+ "offset": [
117307
+ 86,
117308
+ 89
117309
+ ],
117310
+ "raw": "net"
117311
+ },
117312
+ {
117313
+ "offset": [
117314
+ 89,
117315
+ 95
117316
+ ],
117317
+ "raw": " about"
117318
+ },
117319
+ {
117320
+ "offset": [
117321
+ 95,
117322
+ 100
117323
+ ],
117324
+ "raw": " love"
117325
+ },
117326
+ {
117327
+ "offset": [
117328
+ 100,
117329
+ 101
117330
+ ],
117331
+ "raw": "."
117332
+ },
117333
+ {
117334
+ "offset": [
117335
+ 101,
117336
+ 111
117337
+ ],
117338
+ "raw": "<|im_end|>"
117339
+ },
117340
+ {
117341
+ "offset": [
117342
+ 111,
117343
+ 112
117344
+ ],
117345
+ "raw": "\n"
117346
+ },
117347
+ {
117348
+ "offset": [
117349
+ 112,
117350
+ 124
117351
+ ],
117352
+ "raw": "<|im_start|>"
117353
+ },
117354
+ {
117355
+ "offset": [
117356
+ 124,
117357
+ 133
117358
+ ],
117359
+ "raw": "assistant"
117360
+ },
117361
+ {
117362
+ "offset": [
117363
+ 133,
117364
+ 134
117365
+ ],
117366
+ "raw": "\n"
117367
+ },
117368
+ {
117369
+ "offset": [
117370
+ 134,
117371
+ 141
117372
+ ],
117373
+ "raw": "<think>"
117374
+ },
117375
+ {
117376
+ "offset": [
117377
+ 141,
117378
+ 143
117379
+ ],
117380
+ "raw": "\n\n"
117381
+ },
117382
+ {
117383
+ "offset": [
117384
+ 143,
117385
+ 151
117386
+ ],
117387
+ "raw": "</think>"
117388
+ },
117389
+ {
117390
+ "offset": [
117391
+ 151,
117392
+ 153
117393
+ ],
117394
+ "raw": "\n\n"
117395
+ }
117396
+ ],
117397
+ "draft": {
117398
+ "mode": "raw",
117399
+ "model": "base",
117400
+ "maxTokens": 200
117401
+ },
117402
+ "demoUiOptions": {
117403
+ "layoutMode": "text-flow",
117404
+ "measureWidthPx": 400,
117405
+ "dagCompactness": 0.6,
117406
+ "linearArcAdjacentGapPx": 0,
117407
+ "hideExcludedTokens": false,
117408
+ "edgeTopPCoverage": 0.7,
117409
+ "nodeCiVisualScaleEnabled": true,
117410
+ "edgeWeakenHighSurprisalEnabled": true,
117411
+ "hideInactiveEdges": false,
117412
+ "replayPacingMode": "step",
117413
+ "playbackTotalS": 7,
117414
+ "playbackStepMs": 100,
117415
+ "excludePromptPatternsEnabled": true,
117416
+ "excludePromptPatternsText": "#comment# use '#comment#' to comment lines; support regex\n<\\|im_start\\|>system\\n\n<\\|im_start\\|>user\\n\n<\\|im_start\\|>assistant\\n\n<\\|im_start\\|>assistant\\n\\n\n<\\|im_end\\|>\\n\n<think>\\n\\n\n</think>\\n\\n\n<\\|im_start\\|>system\\n[\\s\\S]*?<\\|im_end\\|>#comment# all system prompt",
117417
+ "excludeGeneratedPatternsEnabled": true,
117418
+ "excludeGeneratedPatternsText": "<think>\\n\n</think>\\n\\n"
117419
+ }
117420
  }
client/src/demos/gen_attribute/写一首绝句,主题是春天.json CHANGED
@@ -15733,5 +15733,254 @@
15733
  "token": "<|im_end|>",
15734
  "currentText": "春色满园花满枝, \n轻风拂面柳依依。 \n桃红柳绿新晴日, \n一缕芬芳入梦时。<|im_end|>"
15735
  }
15736
- ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15737
  }
 
15733
  "token": "<|im_end|>",
15734
  "currentText": "春色满园花满枝, \n轻风拂面柳依依。 \n桃红柳绿新晴日, \n一缕芬芳入梦时。<|im_end|>"
15735
  }
15736
+ ],
15737
+ "promptSpans": [
15738
+ {
15739
+ "offset": [
15740
+ 0,
15741
+ 12
15742
+ ],
15743
+ "raw": "<|im_start|>"
15744
+ },
15745
+ {
15746
+ "offset": [
15747
+ 12,
15748
+ 18
15749
+ ],
15750
+ "raw": "system"
15751
+ },
15752
+ {
15753
+ "offset": [
15754
+ 18,
15755
+ 19
15756
+ ],
15757
+ "raw": "\n"
15758
+ },
15759
+ {
15760
+ "offset": [
15761
+ 19,
15762
+ 22
15763
+ ],
15764
+ "raw": "You"
15765
+ },
15766
+ {
15767
+ "offset": [
15768
+ 22,
15769
+ 26
15770
+ ],
15771
+ "raw": " are"
15772
+ },
15773
+ {
15774
+ "offset": [
15775
+ 26,
15776
+ 28
15777
+ ],
15778
+ "raw": " a"
15779
+ },
15780
+ {
15781
+ "offset": [
15782
+ 28,
15783
+ 36
15784
+ ],
15785
+ "raw": " helpful"
15786
+ },
15787
+ {
15788
+ "offset": [
15789
+ 36,
15790
+ 46
15791
+ ],
15792
+ "raw": " assistant"
15793
+ },
15794
+ {
15795
+ "offset": [
15796
+ 46,
15797
+ 47
15798
+ ],
15799
+ "raw": "."
15800
+ },
15801
+ {
15802
+ "offset": [
15803
+ 47,
15804
+ 57
15805
+ ],
15806
+ "raw": "<|im_end|>"
15807
+ },
15808
+ {
15809
+ "offset": [
15810
+ 57,
15811
+ 58
15812
+ ],
15813
+ "raw": "\n"
15814
+ },
15815
+ {
15816
+ "offset": [
15817
+ 58,
15818
+ 70
15819
+ ],
15820
+ "raw": "<|im_start|>"
15821
+ },
15822
+ {
15823
+ "offset": [
15824
+ 70,
15825
+ 74
15826
+ ],
15827
+ "raw": "user"
15828
+ },
15829
+ {
15830
+ "offset": [
15831
+ 74,
15832
+ 75
15833
+ ],
15834
+ "raw": "\n"
15835
+ },
15836
+ {
15837
+ "offset": [
15838
+ 75,
15839
+ 76
15840
+ ],
15841
+ "raw": "写"
15842
+ },
15843
+ {
15844
+ "offset": [
15845
+ 76,
15846
+ 78
15847
+ ],
15848
+ "raw": "一首"
15849
+ },
15850
+ {
15851
+ "offset": [
15852
+ 78,
15853
+ 79
15854
+ ],
15855
+ "raw": "绝"
15856
+ },
15857
+ {
15858
+ "offset": [
15859
+ 79,
15860
+ 80
15861
+ ],
15862
+ "raw": "句"
15863
+ },
15864
+ {
15865
+ "offset": [
15866
+ 80,
15867
+ 81
15868
+ ],
15869
+ "raw": ","
15870
+ },
15871
+ {
15872
+ "offset": [
15873
+ 81,
15874
+ 83
15875
+ ],
15876
+ "raw": "主题"
15877
+ },
15878
+ {
15879
+ "offset": [
15880
+ 83,
15881
+ 84
15882
+ ],
15883
+ "raw": "是"
15884
+ },
15885
+ {
15886
+ "offset": [
15887
+ 84,
15888
+ 86
15889
+ ],
15890
+ "raw": "春天"
15891
+ },
15892
+ {
15893
+ "offset": [
15894
+ 86,
15895
+ 87
15896
+ ],
15897
+ "raw": "。"
15898
+ },
15899
+ {
15900
+ "offset": [
15901
+ 87,
15902
+ 97
15903
+ ],
15904
+ "raw": "<|im_end|>"
15905
+ },
15906
+ {
15907
+ "offset": [
15908
+ 97,
15909
+ 98
15910
+ ],
15911
+ "raw": "\n"
15912
+ },
15913
+ {
15914
+ "offset": [
15915
+ 98,
15916
+ 110
15917
+ ],
15918
+ "raw": "<|im_start|>"
15919
+ },
15920
+ {
15921
+ "offset": [
15922
+ 110,
15923
+ 119
15924
+ ],
15925
+ "raw": "assistant"
15926
+ },
15927
+ {
15928
+ "offset": [
15929
+ 119,
15930
+ 120
15931
+ ],
15932
+ "raw": "\n"
15933
+ },
15934
+ {
15935
+ "offset": [
15936
+ 120,
15937
+ 127
15938
+ ],
15939
+ "raw": "<think>"
15940
+ },
15941
+ {
15942
+ "offset": [
15943
+ 127,
15944
+ 129
15945
+ ],
15946
+ "raw": "\n\n"
15947
+ },
15948
+ {
15949
+ "offset": [
15950
+ 129,
15951
+ 137
15952
+ ],
15953
+ "raw": "</think>"
15954
+ },
15955
+ {
15956
+ "offset": [
15957
+ 137,
15958
+ 139
15959
+ ],
15960
+ "raw": "\n\n"
15961
+ }
15962
+ ],
15963
+ "draft": {
15964
+ "mode": "raw",
15965
+ "model": "base",
15966
+ "maxTokens": 200
15967
+ },
15968
+ "demoUiOptions": {
15969
+ "layoutMode": "text-flow",
15970
+ "measureWidthPx": 400,
15971
+ "dagCompactness": 0.5,
15972
+ "linearArcAdjacentGapPx": 0,
15973
+ "hideExcludedTokens": false,
15974
+ "edgeTopPCoverage": 0.7,
15975
+ "nodeCiVisualScaleEnabled": true,
15976
+ "edgeWeakenHighSurprisalEnabled": true,
15977
+ "hideInactiveEdges": false,
15978
+ "replayPacingMode": "step",
15979
+ "playbackTotalS": 7,
15980
+ "playbackStepMs": 200,
15981
+ "excludePromptPatternsEnabled": true,
15982
+ "excludePromptPatternsText": "#comment# use '#comment#' to comment lines; support regex\n<\\|im_start\\|>system\\n\n<\\|im_start\\|>user\\n\n<\\|im_start\\|>assistant\\n\n<\\|im_start\\|>assistant\\n\\n\n<\\|im_end\\|>\\n\n<think>\\n\\n\n</think>\\n\\n\n<\\|im_start\\|>system\\n[\\s\\S]*?<\\|im_end\\|>#comment# all system prompt",
15983
+ "excludeGeneratedPatternsEnabled": true,
15984
+ "excludeGeneratedPatternsText": "<think>\\n\n</think>\\n\\n"
15985
+ }
15986
  }
client/src/demos/gen_attribute/总结.json DELETED
The diff for this file is too large to render. See raw diff
 
client/src/demos/gen_attribute/过拟合|李白 将进酒.json ADDED
The diff for this file is too large to render. See raw diff
 
client/src/ts/attribution/attributionExcludePromptPatternsStorage.ts CHANGED
@@ -1,14 +1,10 @@
1
  /**
2
- * {@link ./excludePromptPatternsUi bindExcludePromptPatternsUi} / {@link ./excludePromptPatternsUi bindExcludeGeneratedPatternsUi} 同步持久化
3
- * Prompt 键名保留历史前缀 `exclude_tokens`,避免户已存配置失效
4
  */
5
  export const EXCLUDE_PROMPT_PATTERNS_STORAGE_KEY = 'info_radar_attribution_exclude_tokens';
6
  export const EXCLUDE_PROMPT_PATTERNS_ENABLED_STORAGE_KEY = 'info_radar_attribution_exclude_tokens_enabled';
7
 
8
- export const EXCLUDE_GENERATED_PATTERNS_STORAGE_KEY = 'info_radar_attribution_exclude_generated_tokens';
9
- export const EXCLUDE_GENERATED_PATTERNS_ENABLED_STORAGE_KEY =
10
- 'info_radar_attribution_exclude_generated_tokens_enabled';
11
-
12
  /**
13
  * 首次使用(`exclude_tokens` 键从未写入)时 UI 与生效逻辑采用的默认行;`''` 表示用户已显式清空,不再使用本默认。
14
  * 与 {@link readStoredEffectiveExcludePromptPatternsText} 的 `null` / 有值区分一致。
@@ -26,7 +22,7 @@ export const DEFAULT_EXCLUDE_PROMPT_PATTERNS_TEXT = [
26
  ].join('\n');
27
 
28
  /**
29
- * 首次使用(`exclude_generated_tokens` 键从未写入)时 Generate & Attribute「Exclude generated」默认
30
  */
31
  export const DEFAULT_EXCLUDE_GENERATED_PATTERNS_TEXT = [
32
  '<think>\\n',
@@ -49,20 +45,3 @@ export function readStoredEffectiveExcludePromptPatternsText(): string {
49
  return '';
50
  }
51
  }
52
-
53
- /**
54
- * Generate & Attribute「Exclude generated patterns」:未持久化文本键(`null`)时用
55
- * {@link DEFAULT_EXCLUDE_GENERATED_PATTERNS_TEXT};已持久化空串表示用户选择排除零条模式。
56
- */
57
- export function readStoredEffectiveExcludeGeneratedPatternsText(): string {
58
- try {
59
- const enabledRaw = localStorage.getItem(EXCLUDE_GENERATED_PATTERNS_ENABLED_STORAGE_KEY);
60
- const enabled = enabledRaw === null ? true : enabledRaw === '1';
61
- if (!enabled) return '';
62
- const raw = localStorage.getItem(EXCLUDE_GENERATED_PATTERNS_STORAGE_KEY);
63
- if (raw === null) return DEFAULT_EXCLUDE_GENERATED_PATTERNS_TEXT;
64
- return raw;
65
- } catch {
66
- return '';
67
- }
68
- }
 
1
  /**
2
+ * 归因页 Exclude prompt:`bindExcludePromptPatternsUi` / {@link readStoredEffectiveExcludePromptPatternsText} 共用键名
3
+ * Prompt 键名保留历史前缀 `exclude_tokens`。Generate & Attribute 页的排除正则使独立 `info_radar_gen_attr_exclude_*`,与此处解耦
4
  */
5
  export const EXCLUDE_PROMPT_PATTERNS_STORAGE_KEY = 'info_radar_attribution_exclude_tokens';
6
  export const EXCLUDE_PROMPT_PATTERNS_ENABLED_STORAGE_KEY = 'info_radar_attribution_exclude_tokens_enabled';
7
 
 
 
 
 
8
  /**
9
  * 首次使用(`exclude_tokens` 键从未写入)时 UI 与生效逻辑采用的默认行;`''` 表示用户已显式清空,不再使用本默认。
10
  * 与 {@link readStoredEffectiveExcludePromptPatternsText} 的 `null` / 有值区分一致。
 
22
  ].join('\n');
23
 
24
  /**
25
+ * Generate & Attribute「Exclude generated」占位默认文案;该页的存储键前缀为 `info_radar_gen_attr_exclude_generated_*`
26
  */
27
  export const DEFAULT_EXCLUDE_GENERATED_PATTERNS_TEXT = [
28
  '<think>\\n',
 
45
  return '';
46
  }
47
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
client/src/ts/attribution/excludePromptPatternsUi.ts CHANGED
@@ -1,8 +1,5 @@
1
  import {
2
- DEFAULT_EXCLUDE_GENERATED_PATTERNS_TEXT,
3
  DEFAULT_EXCLUDE_PROMPT_PATTERNS_TEXT,
4
- EXCLUDE_GENERATED_PATTERNS_ENABLED_STORAGE_KEY,
5
- EXCLUDE_GENERATED_PATTERNS_STORAGE_KEY,
6
  EXCLUDE_PROMPT_PATTERNS_ENABLED_STORAGE_KEY,
7
  EXCLUDE_PROMPT_PATTERNS_STORAGE_KEY,
8
  } from './attributionExcludePromptPatternsStorage';
@@ -92,7 +89,8 @@ function bindExcludePatternsUi(options: BindExcludePatternsUiOptions): void {
92
  }
93
 
94
  /**
95
- * Exclude prompt patterns:Attribution 与 Generate & Attribute 共用,storage 见 {@link attributionExcludePromptPatternsStorage}。
 
96
  */
97
  export function bindExcludePromptPatternsUi(options: BindExcludePromptPatternsUiOptions): void {
98
  bindExcludePatternsUi({
@@ -104,15 +102,3 @@ export function bindExcludePromptPatternsUi(options: BindExcludePromptPatternsUi
104
  defaultTextWhenKeyAbsent: DEFAULT_EXCLUDE_PROMPT_PATTERNS_TEXT,
105
  });
106
  }
107
-
108
- /** Exclude generated patterns:仅 Generate & Attribute 页绑定;storage 键见 {@link attributionExcludePromptPatternsStorage}。 */
109
- export function bindExcludeGeneratedPatternsUi(options: BindExcludePromptPatternsUiOptions): void {
110
- bindExcludePatternsUi({
111
- storageKeys: {
112
- textKey: EXCLUDE_GENERATED_PATTERNS_STORAGE_KEY,
113
- enabledKey: EXCLUDE_GENERATED_PATTERNS_ENABLED_STORAGE_KEY,
114
- },
115
- ...options,
116
- defaultTextWhenKeyAbsent: DEFAULT_EXCLUDE_GENERATED_PATTERNS_TEXT,
117
- });
118
- }
 
1
  import {
 
2
  DEFAULT_EXCLUDE_PROMPT_PATTERNS_TEXT,
 
 
3
  EXCLUDE_PROMPT_PATTERNS_ENABLED_STORAGE_KEY,
4
  EXCLUDE_PROMPT_PATTERNS_STORAGE_KEY,
5
  } from './attributionExcludePromptPatternsStorage';
 
89
  }
90
 
91
  /**
92
+ * Exclude prompt patterns:归因绑定;键名见 {@link ./attributionExcludePromptPatternsStorage}。
93
+ * Generate & Attribute 页的排除在 `gen_attribute.ts` 内单独绑定(`info_radar_gen_attr_exclude_*`)。
94
  */
95
  export function bindExcludePromptPatternsUi(options: BindExcludePromptPatternsUiOptions): void {
96
  bindExcludePatternsUi({
 
102
  defaultTextWhenKeyAbsent: DEFAULT_EXCLUDE_PROMPT_PATTERNS_TEXT,
103
  });
104
  }
 
 
 
 
 
 
 
 
 
 
 
 
client/src/ts/attribution/genAttributeDagPreprocess.ts CHANGED
@@ -1,7 +1,3 @@
1
- import {
2
- readStoredEffectiveExcludeGeneratedPatternsText,
3
- readStoredEffectiveExcludePromptPatternsText,
4
- } from './attributionExcludePromptPatternsStorage';
5
  import {
6
  collectExcludeRegexMatchIntervals,
7
  isOffsetSpanFullyExcluded,
@@ -120,18 +116,23 @@ export function extractPromptTokenSpans(step: TokenGenStep): PromptTokenSpan[] {
120
  return [...byKey.values()];
121
  }
122
 
123
- /** 与 {@link excludeNodeAggregatedEntries} 使用同一套 prompt / 生成区与 storage 文本,在 `intervalCtx` 上收集排除区间(全串下标)。 */
 
 
 
124
  export function collectGenAttrDagExcludeIntervals(
125
  intervalCtx: string,
126
  promptRegionEnd: number,
 
 
127
  ): [number, number][] {
128
  const pe = promptRegionEnd;
129
  return [
130
- ...collectExcludeRegexMatchIntervals(intervalCtx, readStoredEffectiveExcludePromptPatternsText(), {
131
  start: 0,
132
  end: pe,
133
  }),
134
- ...collectExcludeRegexMatchIntervals(intervalCtx, readStoredEffectiveExcludeGeneratedPatternsText(), {
135
  start: pe,
136
  end: intervalCtx.length,
137
  }),
@@ -144,17 +145,26 @@ export function collectGenAttrDagExcludeIntervals(
144
  *
145
  * @param excludeIntervalContext 取匹配区间所用的全文(与 DAG 节点 offset 同源)。流式场景传**当前已写出的累积串**
146
  *(如 `steps[last].context + steps[last].token`),使跨多 token 才闭合的正则与下标一致;缺省为 `step.context`。
 
 
147
  */
148
  export function excludeNodeAggregatedEntries(
149
  step: TokenGenStep,
150
  entries: NodeAggregatedEntry[],
151
- excludeIntervalContext?: string,
 
 
152
  ): NodeAggregatedEntry[] {
153
  if (!entries.length) return [];
154
 
155
  const pe = step.promptRegionEnd;
156
  const intervalCtx = excludeIntervalContext ?? step.context;
157
- const excludeIntervals = collectGenAttrDagExcludeIntervals(intervalCtx, pe);
 
 
 
 
 
158
  return entries.map((t) => {
159
  const [ts, te] = t.offset;
160
  const excluded = isOffsetSpanFullyExcluded(ts, te, excludeIntervals);
 
 
 
 
 
1
  import {
2
  collectExcludeRegexMatchIntervals,
3
  isOffsetSpanFullyExcluded,
 
116
  return [...byKey.values()];
117
  }
118
 
119
+ /**
120
+ * 在 `intervalCtx` 上收集排除区间(全串下标);正则全文由调用方提供(Gen Attribute 页与控件一致)。
121
+ * 与 {@link excludeNodeAggregatedEntries} 须传入同一套 `excludePromptPatternsText` / `excludeGeneratedPatternsText`。
122
+ */
123
  export function collectGenAttrDagExcludeIntervals(
124
  intervalCtx: string,
125
  promptRegionEnd: number,
126
+ excludePromptPatternsText: string,
127
+ excludeGeneratedPatternsText: string,
128
  ): [number, number][] {
129
  const pe = promptRegionEnd;
130
  return [
131
+ ...collectExcludeRegexMatchIntervals(intervalCtx, excludePromptPatternsText, {
132
  start: 0,
133
  end: pe,
134
  }),
135
+ ...collectExcludeRegexMatchIntervals(intervalCtx, excludeGeneratedPatternsText, {
136
  start: pe,
137
  end: intervalCtx.length,
138
  }),
 
145
  *
146
  * @param excludeIntervalContext 取匹配区间所用的全文(与 DAG 节点 offset 同源)。流式场景传**当前已写出的累积串**
147
  *(如 `steps[last].context + steps[last].token`),使跨多 token 才闭合的正则与下标一致;缺省为 `step.context`。
148
+ * @param excludePromptPatternsText prompt 区 `[0, promptRegionEnd)` 上使用的排除正则全文(勾选关时传 `''`)。
149
+ * @param excludeGeneratedPatternsText 已生成后缀区上使用的排除正则全文(勾选关时传 `''`)。
150
  */
151
  export function excludeNodeAggregatedEntries(
152
  step: TokenGenStep,
153
  entries: NodeAggregatedEntry[],
154
+ excludeIntervalContext: string | undefined,
155
+ excludePromptPatternsText: string,
156
+ excludeGeneratedPatternsText: string,
157
  ): NodeAggregatedEntry[] {
158
  if (!entries.length) return [];
159
 
160
  const pe = step.promptRegionEnd;
161
  const intervalCtx = excludeIntervalContext ?? step.context;
162
+ const excludeIntervals = collectGenAttrDagExcludeIntervals(
163
+ intervalCtx,
164
+ pe,
165
+ excludePromptPatternsText,
166
+ excludeGeneratedPatternsText,
167
+ );
168
  return entries.map((t) => {
169
  const [ts, te] = t.offset;
170
  const excluded = isOffsetSpanFullyExcluded(ts, te, excludeIntervals);
client/src/ts/attribution/genAttributeDagView.ts CHANGED
@@ -572,11 +572,17 @@ export type InitGenAttributeDagViewOptions = {
572
  edgeTopPCoverage?: number;
573
  /** 进入/退出/切换全屏失败时(常见于移动端不支持元素全屏等)。不传则无提示。 */
574
  onFullscreenError?: (message: string) => void;
 
 
 
 
 
 
575
  };
576
 
577
  export function initGenAttributeDagView(
578
  resultsRoot: D3Sel,
579
- options?: InitGenAttributeDagViewOptions
580
  ): GenAttributeDagHandle {
581
  const onDagRefresh = options?.onDagRefresh;
582
  const onDagPlaybackToggle = options?.onDagPlaybackToggle;
@@ -631,6 +637,8 @@ export function initGenAttributeDagView(
631
  };
632
  }
633
 
 
 
634
  detachGenAttributeDagPanel.get(rootEl)?.();
635
  resultsRoot
636
  .selectAll(
@@ -1089,7 +1097,12 @@ export function initGenAttributeDagView(
1089
  const prevIdx = firstNewIdx + i - 1;
1090
  snapSubwordNode(addedNodes[i]!, prevIdx >= 0 ? nodes[prevIdx]! : null);
1091
  }
1092
- dagExcludeIntervals = collectGenAttrDagExcludeIntervals(promptText, promptText.length);
 
 
 
 
 
1093
  if (batchDepth === 0) syncGraphToSvg();
1094
  }
1095
 
@@ -1150,7 +1163,13 @@ export function initGenAttributeDagView(
1150
  step: stepProcessed,
1151
  targetToken: token,
1152
  });
1153
- const afterExclude = excludeNodeAggregatedEntries(step, aggregated, excludeIntervalContext);
 
 
 
 
 
 
1154
  const selected = phase2RankAndSparsify(afterExclude, { cumulativeShare: edgeTopPCoverage });
1155
 
1156
  const mutualInformationRatio = computeMutualInformationRatio(response.target_prob);
@@ -1197,7 +1216,12 @@ export function initGenAttributeDagView(
1197
  });
1198
  }
1199
 
1200
- const excludeIntervals = collectGenAttrDagExcludeIntervals(intervalCtx, step.promptRegionEnd);
 
 
 
 
 
1201
  dagExcludeIntervals = excludeIntervals;
1202
  pruneDagLinksTouchingFullyExcludedNodes(graph, links, excludeIntervals);
1203
 
 
572
  edgeTopPCoverage?: number;
573
  /** 进入/退出/切换全屏失败时(常见于移动端不支持元素全屏等)。不传则无提示。 */
574
  onFullscreenError?: (message: string) => void;
575
+ /**
576
+ * DAG 归因排除:prompt 区正则的**生效**全文(勾选关则 `''`)。须与 Gen Attribute 页控件同源(仅该页使用本视图)。
577
+ */
578
+ getEffectiveExcludePromptPatternsText: () => string;
579
+ /** 已生成后缀区排除正则的生效全文(勾选关则 `''`)。 */
580
+ getEffectiveExcludeGeneratedPatternsText: () => string;
581
  };
582
 
583
  export function initGenAttributeDagView(
584
  resultsRoot: D3Sel,
585
+ options: InitGenAttributeDagViewOptions
586
  ): GenAttributeDagHandle {
587
  const onDagRefresh = options?.onDagRefresh;
588
  const onDagPlaybackToggle = options?.onDagPlaybackToggle;
 
637
  };
638
  }
639
 
640
+ const { getEffectiveExcludePromptPatternsText, getEffectiveExcludeGeneratedPatternsText } = options;
641
+
642
  detachGenAttributeDagPanel.get(rootEl)?.();
643
  resultsRoot
644
  .selectAll(
 
1097
  const prevIdx = firstNewIdx + i - 1;
1098
  snapSubwordNode(addedNodes[i]!, prevIdx >= 0 ? nodes[prevIdx]! : null);
1099
  }
1100
+ dagExcludeIntervals = collectGenAttrDagExcludeIntervals(
1101
+ promptText,
1102
+ promptText.length,
1103
+ getEffectiveExcludePromptPatternsText(),
1104
+ getEffectiveExcludeGeneratedPatternsText(),
1105
+ );
1106
  if (batchDepth === 0) syncGraphToSvg();
1107
  }
1108
 
 
1163
  step: stepProcessed,
1164
  targetToken: token,
1165
  });
1166
+ const afterExclude = excludeNodeAggregatedEntries(
1167
+ step,
1168
+ aggregated,
1169
+ excludeIntervalContext,
1170
+ getEffectiveExcludePromptPatternsText(),
1171
+ getEffectiveExcludeGeneratedPatternsText(),
1172
+ );
1173
  const selected = phase2RankAndSparsify(afterExclude, { cumulativeShare: edgeTopPCoverage });
1174
 
1175
  const mutualInformationRatio = computeMutualInformationRatio(response.target_prob);
 
1216
  });
1217
  }
1218
 
1219
+ const excludeIntervals = collectGenAttrDagExcludeIntervals(
1220
+ intervalCtx,
1221
+ step.promptRegionEnd,
1222
+ getEffectiveExcludePromptPatternsText(),
1223
+ getEffectiveExcludeGeneratedPatternsText(),
1224
+ );
1225
  dagExcludeIntervals = excludeIntervals;
1226
  pruneDagLinksTouchingFullyExcludedNodes(graph, links, excludeIntervals);
1227
 
client/src/ts/attribution/genAttributeDagViewSpiralMode.ts CHANGED
@@ -7,11 +7,11 @@ const SPIRAL_R0 = 80;
7
  /** 相邻两圈之间的径向间距(px)。 */
8
  const SPIRAL_SPACING = 60;
9
  /** 每个 token 沿螺旋弧长占据的固定步长(px)。 */
10
- const SPIRAL_ARC_STEP = 40;
11
  /** 螺旋旋转相位(弧度):控制螺旋臂展开方向。0 = 向右,-Math.PI/2 = 向上。 */
12
  const SPIRAL_PHASE = Math.PI * 0.6;
13
  /** 螺旋上第一个(起始位置)token 的相对视觉放大倍数(仅 spiral 布局)。 */
14
- const SPIRAL_FIRST_TOKEN_SCALE = 2;
15
  // ────────────────────────────────────────────────────────────────────────────
16
 
17
  type SpiralNodeLike = { nodeW: number; nodeH: number };
 
7
  /** 相邻两圈之间的径向间距(px)。 */
8
  const SPIRAL_SPACING = 60;
9
  /** 每个 token 沿螺旋弧长占据的固定步长(px)。 */
10
+ const SPIRAL_ARC_STEP = 30;
11
  /** 螺旋旋转相位(弧度):控制螺旋臂展开方向。0 = 向右,-Math.PI/2 = 向上。 */
12
  const SPIRAL_PHASE = Math.PI * 0.6;
13
  /** 螺旋上第一个(起始位置)token 的相对视觉放大倍数(仅 spiral 布局)。 */
14
+ const SPIRAL_FIRST_TOKEN_SCALE = 1.5;
15
  // ────────────────────────────────────────────────────────────────────────────
16
 
17
  type SpiralNodeLike = { nodeW: number; nodeH: number };
client/src/ts/demos/genAttributeBundledDemoManifest.generated.ts CHANGED
@@ -1,4 +1,4 @@
1
  /**
2
  * Generated by GenAttributeDemoManifestPlugin — do not edit.
3
  */
4
- export const GEN_ATTRIBUTE_BUNDLED_DEMO_SLUGS: readonly string[] = ["Write a sonnet about love","写一首绝句,主题是春天","总结","翻译"];
 
1
  /**
2
  * Generated by GenAttributeDemoManifestPlugin — do not edit.
3
  */
4
+ export const GEN_ATTRIBUTE_BUNDLED_DEMO_SLUGS: readonly string[] = ["CN->EN翻译","Write a sonnet about love","写一首绝句,主题是春天","过拟合|李白 将进酒"];
client/src/ts/demos/genAttributeBundledDemos.ts CHANGED
@@ -2,8 +2,11 @@
2
  * Gen Attribute 打包 demo:大 JSON 在 `dist/demos/gen_attribute/`,运行时 fetch;slug 列表构建期内联自 generated 模块。
3
  */
4
 
5
- import type { GenAttrCachedRun } from '../storage/genAttributeRunCache';
6
- import { isKnownPersistedCompletionReason } from '../utils/generationEndReasonLabel';
 
 
 
7
  import { GEN_ATTRIBUTE_BUNDLED_DEMO_SLUGS } from './genAttributeBundledDemoManifest.generated';
8
 
9
  const BASE = 'demos/gen_attribute/';
@@ -18,28 +21,6 @@ function isSafeDemoSlug(s: string): boolean {
18
  return true;
19
  }
20
 
21
- function isValidGenAttrCachedRunPayload(v: unknown): v is GenAttrCachedRun {
22
- if (v == null || typeof v !== 'object') return false;
23
- const o = v as {
24
- initialContext?: unknown;
25
- steps?: unknown;
26
- completionReason?: unknown;
27
- };
28
- if (
29
- typeof o.initialContext !== 'string' ||
30
- !Array.isArray(o.steps) ||
31
- o.steps.length === 0
32
- ) {
33
- return false;
34
- }
35
- if (o.completionReason !== undefined) {
36
- if (typeof o.completionReason !== 'string' || !isKnownPersistedCompletionReason(o.completionReason)) {
37
- return false;
38
- }
39
- }
40
- return true;
41
- }
42
-
43
  const payloadCache = new Map<string, GenAttrCachedRun>();
44
  const payloadInflight = new Map<string, Promise<GenAttrCachedRun | undefined>>();
45
 
@@ -67,12 +48,10 @@ export async function fetchBundledGenAttributeDemoBySlug(
67
  const r = await fetch(fileUrl);
68
  if (!r.ok) return undefined;
69
  const raw: unknown = await r.json();
70
- if (!isValidGenAttrCachedRunPayload(raw)) {
71
- console.warn(`[genAttributeBundledDemos] invalid demo JSON: ${s}`);
72
- return undefined;
73
- }
74
- payloadCache.set(s, raw);
75
- return raw;
76
  })().finally(() => {
77
  payloadInflight.delete(s);
78
  });
 
2
  * Gen Attribute 打包 demo:大 JSON 在 `dist/demos/gen_attribute/`,运行时 fetch;slug 列表构建期内联自 generated 模块。
3
  */
4
 
5
+ import {
6
+ isValidGenAttrCachedRunPayload,
7
+ parseGenAttrCachedRunPayload,
8
+ type GenAttrCachedRun,
9
+ } from '../storage/genAttributeRunCache';
10
  import { GEN_ATTRIBUTE_BUNDLED_DEMO_SLUGS } from './genAttributeBundledDemoManifest.generated';
11
 
12
  const BASE = 'demos/gen_attribute/';
 
21
  return true;
22
  }
23
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
  const payloadCache = new Map<string, GenAttrCachedRun>();
25
  const payloadInflight = new Map<string, Promise<GenAttrCachedRun | undefined>>();
26
 
 
48
  const r = await fetch(fileUrl);
49
  if (!r.ok) return undefined;
50
  const raw: unknown = await r.json();
51
+ const parsed = parseGenAttrCachedRunPayload(raw, `bundled demo slug=${s}`);
52
+ if (!parsed) return undefined;
53
+ payloadCache.set(s, parsed);
54
+ return parsed;
 
 
55
  })().finally(() => {
56
  payloadInflight.delete(s);
57
  });
client/src/ts/gen_attribute.ts CHANGED
@@ -44,14 +44,21 @@ import { fetchTokenize } from './attribution/predictionAttributeClient';
44
  import { completionFinishReasonLabel, type CompletionFinishReason } from './utils/generationEndReasonLabel';
45
  import {
46
  buildCachedContentUrlParam,
 
47
  getCachedEntryByContentKey,
48
  listCachedHistoryRows,
49
  removeCachedEntryByContentKey,
50
  save,
51
  touchCachedEntryByContentKey,
 
 
52
  type GenAttrCacheKey,
 
53
  } from './storage/genAttributeRunCache';
54
- import { bindExcludeGeneratedPatternsUi, bindExcludePromptPatternsUi } from './attribution/excludePromptPatternsUi';
 
 
 
55
  import { initCachedHistoryQueryDropdown, type CachedHistorySelectContext } from './utils/cachedHistoryUi';
56
  import {
57
  DEFAULT_CONTENT_URL_PARAM,
@@ -69,7 +76,6 @@ import {
69
  } from './demos/genAttributeBundledDemos';
70
  import { extractErrorMessage } from './utils/errorUtils';
71
  import { exportJsonFile } from './storage/localFileIO';
72
- import type { GenAttrCachedRun, GenAttrRunDraft } from './storage/genAttributeRunCache';
73
  import {
74
  GEN_ATTR_RAW_INPUT_HISTORY_KEY,
75
  GEN_ATTR_SYSTEM_INPUT_HISTORY_KEY,
@@ -107,6 +113,13 @@ const GEN_ATTR_DAG_LINEAR_ARC_GAP_STORAGE_KEY =
107
  'info_radar_gen_attr_dag_linear_arc_adjacent_gap';
108
  const GEN_ATTR_DAG_COMPACTNESS_STORAGE_KEY = 'info_radar_gen_attr_dag_compactness';
109
  const GEN_ATTR_DAG_EDGE_TOP_P_COVERAGE_STORAGE_KEY = 'info_radar_gen_attr_dag_edge_top_p_coverage';
 
 
 
 
 
 
 
110
 
111
  /** 步进回放节奏:`total`=整段剩余回放总时长内均分间隔;`step`=固定每步间隔(ms)。 */
112
  type DagReplayPacingMode = 'total' | 'step';
@@ -392,8 +405,69 @@ const dagEdgeWeakenHighSurprisalInput = document.getElementById(
392
  const dagHideInactiveEdgesInput = document.getElementById(
393
  'gen_attr_dag_hide_inactive_edges'
394
  ) as HTMLInputElement | null;
 
 
 
 
 
 
 
 
 
 
 
 
395
  const completeReasonEl = d3.select('#gen_attr_complete_reason');
396
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
397
  if (modelVariantSelect) modelVariantSelect.value = readStoredModelVariant();
398
  if (maxTokensInput) maxTokensInput.value = String(readStoredMaxTokens());
399
  const initialDagLayoutMode = readStoredDagLayoutMode();
@@ -625,6 +699,29 @@ function teacherForcingContinuationForRun(): string | undefined {
625
  return t.length > 0 ? t : undefined;
626
  }
627
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
628
  function syncTeacherForcingRow(): void {
629
  if (genAttrTeacherForcingBlock) {
630
  genAttrTeacherForcingBlock.hidden = !isGenAttrTeacherForcingUiOn();
@@ -903,6 +1000,8 @@ const dagHandle = initGenAttributeDagView(d3.select('#results'), {
903
  hideExcludedTokens: initialDagHideExcludedTokens,
904
  edgeTopPCoverage: initialDagEdgeTopPCoverage,
905
  onFullscreenError: (message) => showToast(message, 'error'),
 
 
906
  });
907
 
908
  dagLayoutModeSelect?.addEventListener('change', () => {
@@ -994,6 +1093,152 @@ dagLinearArcIntervalInput?.addEventListener('change', () => {
994
  dagHandle.setLinearArcAdjacentGapPx(n, { skipRefit: isDagBusy() });
995
  });
996
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
997
  window.addEventListener('pagehide', (ev) => {
998
  if (ev.persisted) return;
999
  dagHandle.detach();
@@ -1007,16 +1252,40 @@ function onExcludePatternsEffectiveChange(): void {
1007
  dagHandle.clearNodeSelection();
1008
  }
1009
 
1010
- bindExcludePromptPatternsUi({
1011
- textInput: document.getElementById('gen_attr_exclude_prompt_patterns') as HTMLTextAreaElement | null,
1012
- enableCheckbox: document.getElementById('gen_attr_exclude_prompt_patterns_enable') as HTMLInputElement | null,
1013
- onEffectiveChange: onExcludePatternsEffectiveChange,
1014
- });
1015
- bindExcludeGeneratedPatternsUi({
1016
- textInput: document.getElementById('gen_attr_exclude_generated_patterns') as HTMLTextAreaElement | null,
1017
- enableCheckbox: document.getElementById('gen_attr_exclude_generated_patterns_enable') as HTMLInputElement | null,
1018
- onEffectiveChange: onExcludePatternsEffectiveChange,
1019
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1020
 
1021
  function currentModelVariant(): PredictionAttributeModelVariant {
1022
  const v = modelVariantSelect?.value;
@@ -1306,6 +1575,7 @@ async function applyGenAttrCachedRun(
1306
 
1307
  stopDagPlayback();
1308
  dagHandle.reset();
 
1309
  runnerHandle = createHydratedTokenGenHandle(rec.steps);
1310
  lastRunInitialContext = rec.initialContext;
1311
  lastRunInputSnapshot = getInputSnapshotForRun();
@@ -1475,7 +1745,11 @@ void (async () => {
1475
  try {
1476
  const rec = await fetchBundledGenAttributeDemoBySlug(demoRaw);
1477
  if (!isStaleGenAttrCachedApply(applyGen) && rec && isGenAttrRunPayloadValidForUi(rec)) {
1478
- await applyGenAttrCachedRun(rec, { afterUrl: { kind: 'demo', slug: demoRaw } }, applyGen);
 
 
 
 
1479
  if (!isStaleGenAttrCachedApply(applyGen)) {
1480
  applied = true;
1481
  }
@@ -1593,20 +1867,7 @@ async function runGeneration(): Promise<void> {
1593
  const stopAfterTF = isStopAfterTeacherForcingOn();
1594
  const maxTokens = currentMaxTokens();
1595
  const tokenizeModel = currentModelVariant();
1596
- const tfDraftFields = teacherForcingText !== undefined
1597
- ? { teacherForcing: teacherForcingText, stopAfterTeacherForcing: stopAfterTF }
1598
- : {};
1599
- const runDraft: GenAttrRunDraft = isSkipChatTemplate()
1600
- ? { mode: 'raw', model: tokenizeModel, maxTokens, ...tfDraftFields }
1601
- : {
1602
- mode: 'chat',
1603
- model: tokenizeModel,
1604
- maxTokens,
1605
- system: systemPromptTextarea?.value ?? '',
1606
- user: userPromptTextarea?.value ?? '',
1607
- useSystem: isGenAttrUseSystemPrompt(),
1608
- ...tfDraftFields,
1609
- };
1610
  const prompt = getActivePromptValue();
1611
  analyzeProgressEl.text('Assembling prompt…').style('display', null);
1612
  initialContext = await resolveInitialContext(signal);
@@ -1773,18 +2034,29 @@ function syncGenAttrExportDemoBtn(): void {
1773
  syncGenAttrExportDemoBtn();
1774
  adminManager.onAdminModeChange(() => syncGenAttrExportDemoBtn());
1775
  exportDemoBtn?.addEventListener('click', () => {
1776
- const h = runnerHandle;
1777
- const ic = lastRunInitialContext;
1778
- if (!h || !ic || h.tokenCount < 1) {
1779
- showToast(tr('No run to export'), 'error');
1780
- return;
1781
- }
1782
- const payload: GenAttrCachedRun = {
1783
- initialContext: ic,
1784
- steps: h.getAllSteps(),
1785
- ...(lastRunCompletionReason != null ? { completionReason: lastRunCompletionReason } : {}),
1786
- };
1787
- void exportJsonFile(payload, `genattr-${Date.now()}.json`);
 
 
 
 
 
 
 
 
 
 
 
1788
  });
1789
 
1790
  initChatPanelLayout({ storageKey: PANEL_SPLIT_STORAGE_KEY_GEN_ATTRIBUTE });
 
44
  import { completionFinishReasonLabel, type CompletionFinishReason } from './utils/generationEndReasonLabel';
45
  import {
46
  buildCachedContentUrlParam,
47
+ buildGenAttrExportedDemoPayload,
48
  getCachedEntryByContentKey,
49
  listCachedHistoryRows,
50
  removeCachedEntryByContentKey,
51
  save,
52
  touchCachedEntryByContentKey,
53
+ type GenAttrCachedRun,
54
+ type GenAttrDemoUiOptions,
55
  type GenAttrCacheKey,
56
+ type GenAttrRunDraft,
57
  } from './storage/genAttributeRunCache';
58
+ import {
59
+ DEFAULT_EXCLUDE_GENERATED_PATTERNS_TEXT,
60
+ DEFAULT_EXCLUDE_PROMPT_PATTERNS_TEXT,
61
+ } from './attribution/attributionExcludePromptPatternsStorage';
62
  import { initCachedHistoryQueryDropdown, type CachedHistorySelectContext } from './utils/cachedHistoryUi';
63
  import {
64
  DEFAULT_CONTENT_URL_PARAM,
 
76
  } from './demos/genAttributeBundledDemos';
77
  import { extractErrorMessage } from './utils/errorUtils';
78
  import { exportJsonFile } from './storage/localFileIO';
 
79
  import {
80
  GEN_ATTR_RAW_INPUT_HISTORY_KEY,
81
  GEN_ATTR_SYSTEM_INPUT_HISTORY_KEY,
 
113
  'info_radar_gen_attr_dag_linear_arc_adjacent_gap';
114
  const GEN_ATTR_DAG_COMPACTNESS_STORAGE_KEY = 'info_radar_gen_attr_dag_compactness';
115
  const GEN_ATTR_DAG_EDGE_TOP_P_COVERAGE_STORAGE_KEY = 'info_radar_gen_attr_dag_edge_top_p_coverage';
116
+ /** 仅此页:与 Attribution 的 `exclude_tokens` 无关。 */
117
+ const GEN_ATTR_EXCLUDE_PROMPT_PATTERNS_STORAGE_KEY = 'info_radar_gen_attr_exclude_prompt_patterns';
118
+ const GEN_ATTR_EXCLUDE_PROMPT_PATTERNS_ENABLED_STORAGE_KEY =
119
+ 'info_radar_gen_attr_exclude_prompt_patterns_enabled';
120
+ const GEN_ATTR_EXCLUDE_GENERATED_PATTERNS_STORAGE_KEY = 'info_radar_gen_attr_exclude_generated_patterns';
121
+ const GEN_ATTR_EXCLUDE_GENERATED_PATTERNS_ENABLED_STORAGE_KEY =
122
+ 'info_radar_gen_attr_exclude_generated_patterns_enabled';
123
 
124
  /** 步进回放节奏:`total`=整段剩余回放总时长内均分间隔;`step`=固定每步间隔(ms)。 */
125
  type DagReplayPacingMode = 'total' | 'step';
 
405
  const dagHideInactiveEdgesInput = document.getElementById(
406
  'gen_attr_dag_hide_inactive_edges'
407
  ) as HTMLInputElement | null;
408
+ const genAttrExcludePromptPatternsTa = document.getElementById(
409
+ 'gen_attr_exclude_prompt_patterns'
410
+ ) as HTMLTextAreaElement | null;
411
+ const genAttrExcludePromptPatternsEnable = document.getElementById(
412
+ 'gen_attr_exclude_prompt_patterns_enable'
413
+ ) as HTMLInputElement | null;
414
+ const genAttrExcludeGeneratedPatternsTa = document.getElementById(
415
+ 'gen_attr_exclude_generated_patterns'
416
+ ) as HTMLTextAreaElement | null;
417
+ const genAttrExcludeGeneratedPatternsEnable = document.getElementById(
418
+ 'gen_attr_exclude_generated_patterns_enable'
419
+ ) as HTMLInputElement | null;
420
  const completeReasonEl = d3.select('#gen_attr_complete_reason');
421
 
422
+ function syncGenAttrExcludePatternTextareasDisabled(): void {
423
+ if (genAttrExcludePromptPatternsTa) {
424
+ genAttrExcludePromptPatternsTa.disabled = !genAttrExcludePromptPatternsEnable?.checked;
425
+ }
426
+ if (genAttrExcludeGeneratedPatternsTa) {
427
+ genAttrExcludeGeneratedPatternsTa.disabled = !genAttrExcludeGeneratedPatternsEnable?.checked;
428
+ }
429
+ }
430
+
431
+ function hydrateGenAttrExcludePatternsFromGenAttrStorage(): void {
432
+ try {
433
+ const savedPrompt = localStorage.getItem(GEN_ATTR_EXCLUDE_PROMPT_PATTERNS_STORAGE_KEY);
434
+ if (genAttrExcludePromptPatternsTa) {
435
+ genAttrExcludePromptPatternsTa.value =
436
+ savedPrompt !== null ? savedPrompt : DEFAULT_EXCLUDE_PROMPT_PATTERNS_TEXT;
437
+ }
438
+ const promptEnRaw = localStorage.getItem(GEN_ATTR_EXCLUDE_PROMPT_PATTERNS_ENABLED_STORAGE_KEY);
439
+ if (genAttrExcludePromptPatternsEnable) {
440
+ genAttrExcludePromptPatternsEnable.checked = promptEnRaw === null ? true : promptEnRaw === '1';
441
+ }
442
+
443
+ const savedGen = localStorage.getItem(GEN_ATTR_EXCLUDE_GENERATED_PATTERNS_STORAGE_KEY);
444
+ if (genAttrExcludeGeneratedPatternsTa) {
445
+ genAttrExcludeGeneratedPatternsTa.value =
446
+ savedGen !== null ? savedGen : DEFAULT_EXCLUDE_GENERATED_PATTERNS_TEXT;
447
+ }
448
+ const genEnRaw = localStorage.getItem(GEN_ATTR_EXCLUDE_GENERATED_PATTERNS_ENABLED_STORAGE_KEY);
449
+ if (genAttrExcludeGeneratedPatternsEnable) {
450
+ genAttrExcludeGeneratedPatternsEnable.checked = genEnRaw === null ? true : genEnRaw === '1';
451
+ }
452
+ } catch {
453
+ /* ignore */
454
+ }
455
+ syncGenAttrExcludePatternTextareasDisabled();
456
+ }
457
+
458
+ hydrateGenAttrExcludePatternsFromGenAttrStorage();
459
+
460
+ /** 与 DAG 同源:DAG 预处理按当前控件即时读取,不读 Attribution 的 localStorage。 */
461
+ function genAttrEffectiveExcludePromptPatternsText(): string {
462
+ if (!genAttrExcludePromptPatternsEnable?.checked) return '';
463
+ return genAttrExcludePromptPatternsTa?.value ?? '';
464
+ }
465
+
466
+ function genAttrEffectiveExcludeGeneratedPatternsText(): string {
467
+ if (!genAttrExcludeGeneratedPatternsEnable?.checked) return '';
468
+ return genAttrExcludeGeneratedPatternsTa?.value ?? '';
469
+ }
470
+
471
  if (modelVariantSelect) modelVariantSelect.value = readStoredModelVariant();
472
  if (maxTokensInput) maxTokensInput.value = String(readStoredMaxTokens());
473
  const initialDagLayoutMode = readStoredDagLayoutMode();
 
699
  return t.length > 0 ? t : undefined;
700
  }
701
 
702
+ /** 与 IndexedDB `save` 使用同一快照逻辑(须在 `autoMoveFirstTeacherForcingTokenToPromptIfNeeded` 之后调用)。 */
703
+ function buildGenAttrRunDraftForCache(): GenAttrRunDraft {
704
+ const teacherForcingText = teacherForcingContinuationForRun();
705
+ const stopAfterTF = isStopAfterTeacherForcingOn();
706
+ const maxTokens = currentMaxTokens();
707
+ const tokenizeModel = currentModelVariant();
708
+ const tfDraftFields =
709
+ teacherForcingText !== undefined
710
+ ? { teacherForcing: teacherForcingText, stopAfterTeacherForcing: stopAfterTF }
711
+ : {};
712
+ return isSkipChatTemplate()
713
+ ? { mode: 'raw', model: tokenizeModel, maxTokens, ...tfDraftFields }
714
+ : {
715
+ mode: 'chat',
716
+ model: tokenizeModel,
717
+ maxTokens,
718
+ system: systemPromptTextarea?.value ?? '',
719
+ user: userPromptTextarea?.value ?? '',
720
+ useSystem: isGenAttrUseSystemPrompt(),
721
+ ...tfDraftFields,
722
+ };
723
+ }
724
+
725
  function syncTeacherForcingRow(): void {
726
  if (genAttrTeacherForcingBlock) {
727
  genAttrTeacherForcingBlock.hidden = !isGenAttrTeacherForcingUiOn();
 
1000
  hideExcludedTokens: initialDagHideExcludedTokens,
1001
  edgeTopPCoverage: initialDagEdgeTopPCoverage,
1002
  onFullscreenError: (message) => showToast(message, 'error'),
1003
+ getEffectiveExcludePromptPatternsText: genAttrEffectiveExcludePromptPatternsText,
1004
+ getEffectiveExcludeGeneratedPatternsText: genAttrEffectiveExcludeGeneratedPatternsText,
1005
  });
1006
 
1007
  dagLayoutModeSelect?.addEventListener('change', () => {
 
1093
  dagHandle.setLinearArcAdjacentGapPx(n, { skipRefit: isDagBusy() });
1094
  });
1095
 
1096
+ /** 读取当前演示用 UI(DAG 与排除正则等),供 Export demo 写入 `demoUiOptions`。 */
1097
+ function readGenAttrDemoUiOptionsFromControls(): GenAttrDemoUiOptions {
1098
+ const rawW = parseInt(dagMeasureWidthInput?.value ?? '', 10);
1099
+ const measureWidthPx = Number.isFinite(rawW)
1100
+ ? clampDagMeasureWidth(rawW)
1101
+ : GEN_ATTR_DAG_MEASURE_WIDTH_DEFAULT;
1102
+ const rawC = parseFloat(dagCompactnessInput?.value ?? '');
1103
+ const dagCompactness = Number.isFinite(rawC)
1104
+ ? clampDagCompactness(rawC)
1105
+ : DAG_COMPACTNESS_DEFAULT;
1106
+ const rawGap = parseInt(dagLinearArcIntervalInput?.value ?? '', 10);
1107
+ const linearArcAdjacentGapPx = Number.isFinite(rawGap)
1108
+ ? clampLinearArcAdjacentGap(rawGap)
1109
+ : LINEAR_ARC_ADJACENT_GAP_DEFAULT;
1110
+ const rawTop = parseFloat(dagEdgeTopPCoverageInput?.value ?? '');
1111
+ const edgeTopPCoverage = Number.isFinite(rawTop)
1112
+ ? clampDagEdgeTopPCoverage(rawTop)
1113
+ : DAG_EDGE_TOP_P_COVERAGE_DEFAULT;
1114
+ const rawTotal = parseInt(dagPlaybackTotalSInput?.value ?? '', 10);
1115
+ const playbackTotalS = Number.isFinite(rawTotal)
1116
+ ? clampDagPlaybackTotalS(rawTotal)
1117
+ : GEN_ATTR_DAG_PLAYBACK_TOTAL_S_DEFAULT;
1118
+ const rawStep = parseInt(dagPlaybackStepMsInput?.value ?? '', 10);
1119
+ const playbackStepMs = Number.isFinite(rawStep)
1120
+ ? clampDagPlaybackStepMs(rawStep)
1121
+ : GEN_ATTR_DAG_PLAYBACK_STEP_MS_DEFAULT;
1122
+ return {
1123
+ layoutMode: currentDagLayoutMode(),
1124
+ measureWidthPx,
1125
+ dagCompactness,
1126
+ linearArcAdjacentGapPx,
1127
+ hideExcludedTokens: dagHideExcludedTokensInput?.checked ?? false,
1128
+ edgeTopPCoverage,
1129
+ nodeCiVisualScaleEnabled: dagNodeCiVisualScaleInput?.checked ?? true,
1130
+ edgeWeakenHighSurprisalEnabled: dagEdgeWeakenHighSurprisalInput?.checked ?? true,
1131
+ hideInactiveEdges: dagHideInactiveEdgesInput?.checked ?? false,
1132
+ replayPacingMode: currentDagReplayPacingMode(),
1133
+ playbackTotalS,
1134
+ playbackStepMs,
1135
+ excludePromptPatternsEnabled: genAttrExcludePromptPatternsEnable?.checked ?? true,
1136
+ excludePromptPatternsText: genAttrExcludePromptPatternsTa?.value ?? '',
1137
+ excludeGeneratedPatternsEnabled: genAttrExcludeGeneratedPatternsEnable?.checked ?? true,
1138
+ excludeGeneratedPatternsText: genAttrExcludeGeneratedPatternsTa?.value ?? '',
1139
+ };
1140
+ }
1141
+
1142
+ /**
1143
+ * 从 `demoUiOptions` 还原排除控件(仅 DOM);与施加 DAG demo 不回写 GEN_ATTR_LS 的策略一致,`replay` 读当前控件生效。
1144
+ */
1145
+ function applyGenAttrExcludePatternsFromDemoUiSnap(snap: Partial<GenAttrDemoUiOptions>): void {
1146
+ const {
1147
+ excludePromptPatternsEnabled,
1148
+ excludePromptPatternsText,
1149
+ excludeGeneratedPatternsEnabled,
1150
+ excludeGeneratedPatternsText,
1151
+ } = snap;
1152
+ if (
1153
+ excludePromptPatternsEnabled === undefined &&
1154
+ excludePromptPatternsText === undefined &&
1155
+ excludeGeneratedPatternsEnabled === undefined &&
1156
+ excludeGeneratedPatternsText === undefined
1157
+ ) {
1158
+ return;
1159
+ }
1160
+ if (excludePromptPatternsEnabled !== undefined && genAttrExcludePromptPatternsEnable) {
1161
+ genAttrExcludePromptPatternsEnable.checked = excludePromptPatternsEnabled;
1162
+ }
1163
+ if (excludePromptPatternsText !== undefined && genAttrExcludePromptPatternsTa) {
1164
+ genAttrExcludePromptPatternsTa.value = excludePromptPatternsText;
1165
+ }
1166
+ if (excludeGeneratedPatternsEnabled !== undefined && genAttrExcludeGeneratedPatternsEnable) {
1167
+ genAttrExcludeGeneratedPatternsEnable.checked = excludeGeneratedPatternsEnabled;
1168
+ }
1169
+ if (excludeGeneratedPatternsText !== undefined && genAttrExcludeGeneratedPatternsTa) {
1170
+ genAttrExcludeGeneratedPatternsTa.value = excludeGeneratedPatternsText;
1171
+ }
1172
+ syncGenAttrExcludePatternTextareasDisabled();
1173
+ }
1174
+
1175
+ /**
1176
+ * 按记录中的 `demoUiOptions` 还原 DAG 面板与排除控件(后者仅 DOM)。
1177
+ */
1178
+ function applyGenAttrDemoUiOptionsFromRecord(rec: GenAttrCachedRun): void {
1179
+ const snap = rec.demoUiOptions;
1180
+ if (!snap) return;
1181
+ const mode = snap.layoutMode;
1182
+ if (mode) {
1183
+ if (dagLayoutModeSelect) {
1184
+ dagLayoutModeSelect.value = mode;
1185
+ }
1186
+ applyDagLayoutModeUi();
1187
+ dagHandle.setLayoutMode(mode);
1188
+ }
1189
+
1190
+ if (snap.measureWidthPx !== undefined) {
1191
+ const w = clampDagMeasureWidth(snap.measureWidthPx);
1192
+ if (dagMeasureWidthInput) dagMeasureWidthInput.value = String(w);
1193
+ dagHandle.setMeasureWidthPx(w);
1194
+ }
1195
+ if (snap.dagCompactness !== undefined) {
1196
+ const c = clampDagCompactness(snap.dagCompactness);
1197
+ if (dagCompactnessInput) dagCompactnessInput.value = String(c);
1198
+ dagHandle.setDagCompactness(c);
1199
+ }
1200
+ if (snap.linearArcAdjacentGapPx !== undefined) {
1201
+ const n = clampLinearArcAdjacentGap(snap.linearArcAdjacentGapPx);
1202
+ if (dagLinearArcIntervalInput) dagLinearArcIntervalInput.value = String(n);
1203
+ dagHandle.setLinearArcAdjacentGapPx(n);
1204
+ }
1205
+ if (snap.edgeTopPCoverage !== undefined) {
1206
+ const c = clampDagEdgeTopPCoverage(snap.edgeTopPCoverage);
1207
+ if (dagEdgeTopPCoverageInput) dagEdgeTopPCoverageInput.value = String(c);
1208
+ dagHandle.setEdgeTopPCoverage(c);
1209
+ }
1210
+ if (snap.hideExcludedTokens !== undefined) {
1211
+ if (dagHideExcludedTokensInput) dagHideExcludedTokensInput.checked = snap.hideExcludedTokens;
1212
+ dagHandle.setHideExcludedTokens(snap.hideExcludedTokens);
1213
+ }
1214
+ if (snap.nodeCiVisualScaleEnabled !== undefined) {
1215
+ if (dagNodeCiVisualScaleInput) dagNodeCiVisualScaleInput.checked = snap.nodeCiVisualScaleEnabled;
1216
+ setDagNodeCiVisualScaleEnabled(snap.nodeCiVisualScaleEnabled);
1217
+ }
1218
+ if (snap.edgeWeakenHighSurprisalEnabled !== undefined) {
1219
+ if (dagEdgeWeakenHighSurprisalInput) dagEdgeWeakenHighSurprisalInput.checked = snap.edgeWeakenHighSurprisalEnabled;
1220
+ setDagEdgeWeakenHighSurprisalEnabled(snap.edgeWeakenHighSurprisalEnabled);
1221
+ }
1222
+ if (snap.hideInactiveEdges !== undefined) {
1223
+ if (dagHideInactiveEdgesInput) dagHideInactiveEdgesInput.checked = snap.hideInactiveEdges;
1224
+ applyDagHideInactiveEdges(snap.hideInactiveEdges);
1225
+ }
1226
+ if (snap.replayPacingMode !== undefined) {
1227
+ if (dagReplayModeSelect) dagReplayModeSelect.value = snap.replayPacingMode;
1228
+ applyDagReplaySpeedUi();
1229
+ }
1230
+ if (snap.playbackTotalS !== undefined) {
1231
+ const s = clampDagPlaybackTotalS(snap.playbackTotalS);
1232
+ if (dagPlaybackTotalSInput) dagPlaybackTotalSInput.value = String(s);
1233
+ }
1234
+ if (snap.playbackStepMs !== undefined) {
1235
+ const ms = clampDagPlaybackStepMs(snap.playbackStepMs);
1236
+ if (dagPlaybackStepMsInput) dagPlaybackStepMsInput.value = String(ms);
1237
+ }
1238
+
1239
+ applyGenAttrExcludePatternsFromDemoUiSnap(snap);
1240
+ }
1241
+
1242
  window.addEventListener('pagehide', (ev) => {
1243
  if (ev.persisted) return;
1244
  dagHandle.detach();
 
1252
  dagHandle.clearNodeSelection();
1253
  }
1254
 
1255
+ function bindExcludePatternControls(
1256
+ enableEl: HTMLInputElement | null,
1257
+ textEl: HTMLTextAreaElement | null,
1258
+ textKey: string,
1259
+ enabledKey: string,
1260
+ ): void {
1261
+ enableEl?.addEventListener('change', () => {
1262
+ try {
1263
+ if (textEl) localStorage.setItem(textKey, textEl.value);
1264
+ localStorage.setItem(enabledKey, enableEl.checked ? '1' : '0');
1265
+ } catch { /* ignore */ }
1266
+ syncGenAttrExcludePatternTextareasDisabled();
1267
+ onExcludePatternsEffectiveChange();
1268
+ });
1269
+ textEl?.addEventListener('blur', () => {
1270
+ try {
1271
+ localStorage.setItem(textKey, textEl.value);
1272
+ } catch { /* ignore */ }
1273
+ onExcludePatternsEffectiveChange();
1274
+ });
1275
+ }
1276
+
1277
+ bindExcludePatternControls(
1278
+ genAttrExcludePromptPatternsEnable,
1279
+ genAttrExcludePromptPatternsTa,
1280
+ GEN_ATTR_EXCLUDE_PROMPT_PATTERNS_STORAGE_KEY,
1281
+ GEN_ATTR_EXCLUDE_PROMPT_PATTERNS_ENABLED_STORAGE_KEY,
1282
+ );
1283
+ bindExcludePatternControls(
1284
+ genAttrExcludeGeneratedPatternsEnable,
1285
+ genAttrExcludeGeneratedPatternsTa,
1286
+ GEN_ATTR_EXCLUDE_GENERATED_PATTERNS_STORAGE_KEY,
1287
+ GEN_ATTR_EXCLUDE_GENERATED_PATTERNS_ENABLED_STORAGE_KEY,
1288
+ );
1289
 
1290
  function currentModelVariant(): PredictionAttributeModelVariant {
1291
  const v = modelVariantSelect?.value;
 
1575
 
1576
  stopDagPlayback();
1577
  dagHandle.reset();
1578
+ applyGenAttrDemoUiOptionsFromRecord(rec);
1579
  runnerHandle = createHydratedTokenGenHandle(rec.steps);
1580
  lastRunInitialContext = rec.initialContext;
1581
  lastRunInputSnapshot = getInputSnapshotForRun();
 
1745
  try {
1746
  const rec = await fetchBundledGenAttributeDemoBySlug(demoRaw);
1747
  if (!isStaleGenAttrCachedApply(applyGen) && rec && isGenAttrRunPayloadValidForUi(rec)) {
1748
+ await applyGenAttrCachedRun(
1749
+ rec,
1750
+ { afterUrl: { kind: 'demo', slug: demoRaw } },
1751
+ applyGen
1752
+ );
1753
  if (!isStaleGenAttrCachedApply(applyGen)) {
1754
  applied = true;
1755
  }
 
1867
  const stopAfterTF = isStopAfterTeacherForcingOn();
1868
  const maxTokens = currentMaxTokens();
1869
  const tokenizeModel = currentModelVariant();
1870
+ const runDraft = buildGenAttrRunDraftForCache();
 
 
 
 
 
 
 
 
 
 
 
 
 
1871
  const prompt = getActivePromptValue();
1872
  analyzeProgressEl.text('Assembling prompt…').style('display', null);
1873
  initialContext = await resolveInitialContext(signal);
 
2034
  syncGenAttrExportDemoBtn();
2035
  adminManager.onAdminModeChange(() => syncGenAttrExportDemoBtn());
2036
  exportDemoBtn?.addEventListener('click', () => {
2037
+ void (async () => {
2038
+ const h = runnerHandle;
2039
+ const ic = lastRunInitialContext;
2040
+ if (!h || !ic || h.tokenCount < 1) {
2041
+ showToast(tr('No run to export'), 'error');
2042
+ return;
2043
+ }
2044
+ await autoMoveFirstTeacherForcingTokenToPromptIfNeeded();
2045
+ try {
2046
+ const payload = buildGenAttrExportedDemoPayload({
2047
+ initialContext: ic,
2048
+ steps: h.getAllSteps(),
2049
+ promptSpans: currentRunPromptSpans,
2050
+ completionReason: lastRunCompletionReason ?? undefined,
2051
+ draft: buildGenAttrRunDraftForCache(),
2052
+ demoUiOptions: readGenAttrDemoUiOptionsFromControls(),
2053
+ });
2054
+ void exportJsonFile(payload, `genattr-${Date.now()}.json`);
2055
+ } catch (e: unknown) {
2056
+ const msg = e instanceof Error ? e.message : String(e);
2057
+ showToast(msg, 'error');
2058
+ }
2059
+ })();
2060
  });
2061
 
2062
  initChatPanelLayout({ storageKey: PANEL_SPLIT_STORAGE_KEY_GEN_ATTRIBUTE });
client/src/ts/lang/translations.ts CHANGED
@@ -87,6 +87,10 @@ export const translations: Translations = {
87
  'Analyze&Upload': '分析并上传',
88
  'Analyze URL': '分析 URL',
89
  'Analyze URL content': '分析 URL 内容',
 
 
 
 
90
  'Cancel': '取消',
91
  'Clear': '清空',
92
  'Compare results': '对比结果',
 
87
  'Analyze&Upload': '分析并上传',
88
  'Analyze URL': '分析 URL',
89
  'Analyze URL content': '分析 URL 内容',
90
+ 'Please enter a URL.': '请输入 URL。',
91
+ 'Invalid input': '输入无效',
92
+ 'Invalid URL': 'URL 无效',
93
+ 'This does not look like a valid URL. Check for typos.': '这不像有效的网址,请检查是否输错。',
94
  'Cancel': '取消',
95
  'Clear': '清空',
96
  'Compare results': '对比结果',
client/src/ts/storage/genAttributeRunCache.ts CHANGED
@@ -1,8 +1,10 @@
 
1
  import type { TokenGenStep } from '../attribution/tokenGenAttributionRunner';
2
  import type { PromptTokenSpan } from '../attribution/genAttributeDagPreprocess';
3
  import {
4
  canonicalizeCompletionFinishReason,
5
  isCompletionFinishReason,
 
6
  type CompletionFinishReason,
7
  } from '../utils/generationEndReasonLabel';
8
  import {
@@ -37,7 +39,12 @@ export type GenAttrRunDraft = {
37
  stopAfterTeacherForcing?: boolean;
38
  };
39
 
40
- export type GenAttrCachedRun = {
 
 
 
 
 
41
  initialContext: string;
42
  steps: TokenGenStep[];
43
  /** 完整 prompt token spans(offset + raw),与 /api/tokenize 同源;旧缓存无此字段时由调用方从 step 0 归因降级。 */
@@ -49,7 +56,37 @@ export type GenAttrCachedRun = {
49
  };
50
 
51
  /**
52
- * 缓存业务 key:涵盖所有影响 steps 内容的生成参数
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
  * 原则:draft 中存储的可变参数均纳入 key,同参数不同结果不应互相覆盖。
54
  */
55
  export type GenAttrCacheKey = {
@@ -77,15 +114,15 @@ function keyHash(key: GenAttrCacheKey): string {
77
  return buildContentKeyFromBusinessKey(normalizeKey(key));
78
  }
79
 
80
- export async function save(
81
- key: GenAttrCacheKey,
82
- steps: TokenGenStep[],
83
- promptSpans: PromptTokenSpan[],
84
- status: 'partial' | 'complete' = steps.length > 0 ? 'partial' : 'complete',
85
- completionReason?: CompletionFinishReason,
86
- draft?: GenAttrRunDraft
87
- ): Promise<void> {
88
- const { initialContext } = key;
89
  let reasonToStore: CompletionFinishReason | undefined;
90
  if (completionReason !== undefined) {
91
  const c = canonicalizeCompletionFinishReason(completionReason);
@@ -94,13 +131,192 @@ export async function save(
94
  }
95
  reasonToStore = c;
96
  }
97
- const payload: GenAttrCachedRun = {
98
  initialContext,
99
  steps,
100
  ...(promptSpans.length > 0 ? { promptSpans } : {}),
101
  ...(reasonToStore !== undefined ? { completionReason: reasonToStore } : {}),
102
  ...(draft !== undefined ? { draft } : {}),
103
  };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
104
  await upsertEntry({
105
  namespace: NAMESPACE,
106
  businessKeyJson: JSON.stringify(normalizeKey(key)),
@@ -113,13 +329,15 @@ export async function save(
113
 
114
  export async function get(key: GenAttrCacheKey): Promise<GenAttrCachedRun | undefined> {
115
  const row = await getByContentKey<GenAttrCachedRun>(NAMESPACE, keyHash(key));
116
- return row?.payload;
 
117
  }
118
 
119
  export async function getCachedEntryByContentKey(raw: string): Promise<GenAttrCachedRun | undefined> {
120
  if (!raw) return undefined;
121
  const row = await getByContentKey<GenAttrCachedRun>(NAMESPACE, raw);
122
- return row?.payload;
 
123
  }
124
 
125
  export function buildCachedContentUrlParam(key: GenAttrCacheKey): string {
 
1
+ import type { DagLayoutMode } from '../attribution/genAttributeDagView';
2
  import type { TokenGenStep } from '../attribution/tokenGenAttributionRunner';
3
  import type { PromptTokenSpan } from '../attribution/genAttributeDagPreprocess';
4
  import {
5
  canonicalizeCompletionFinishReason,
6
  isCompletionFinishReason,
7
+ isKnownPersistedCompletionReason,
8
  type CompletionFinishReason,
9
  } from '../utils/generationEndReasonLabel';
10
  import {
 
39
  stopAfterTeacherForcing?: boolean;
40
  };
41
 
42
+ /**
43
+ * Payload 中与 **正文 / demo UI 选项** 中的 UI 无关的内容(一次 run 的 JSON 主体):
44
+ * - **Key 语义**(去重哈希)单独由 {@link GenAttrCacheKey} 表示并排他参与 `contentKey`;
45
+ * - UI 控件快照见可选字段 {@link GenAttrCachedRun.demoUiOptions}(仅导出 demo)。
46
+ */
47
+ export type GenAttrCachedRunContentFields = {
48
  initialContext: string;
49
  steps: TokenGenStep[];
50
  /** 完整 prompt token spans(offset + raw),与 /api/tokenize 同源;旧缓存无此字段时由调用方从 step 0 归因降级。 */
 
56
  };
57
 
58
  /**
59
+ * Gen Attribute **演示用 UI** 快照(DAG 几何与勾选、回放节奏、归因排除正则等;与正文 key 无关)
60
+ * **Export demo** 写入完整对象;加载时可为 {@link Partial},按存在字段逐项应用。
61
+ */
62
+ export type GenAttrDemoUiOptions = {
63
+ layoutMode: DagLayoutMode;
64
+ measureWidthPx: number;
65
+ dagCompactness: number;
66
+ linearArcAdjacentGapPx: number;
67
+ hideExcludedTokens: boolean;
68
+ edgeTopPCoverage: number;
69
+ nodeCiVisualScaleEnabled: boolean;
70
+ edgeWeakenHighSurprisalEnabled: boolean;
71
+ hideInactiveEdges: boolean;
72
+ replayPacingMode: 'total' | 'step';
73
+ playbackTotalS: number;
74
+ playbackStepMs: number;
75
+ /** 排除 prompt token 归因:使能与正则文本(仅 Gen Attribute,`info_radar_gen_attr_exclude_prompt_*`)。 */
76
+ excludePromptPatternsEnabled: boolean;
77
+ excludePromptPatternsText: string;
78
+ /** 排除生成 token 归因:使能与正则文本(`info_radar_gen_attr_exclude_generated_*`)。 */
79
+ excludeGeneratedPatternsEnabled: boolean;
80
+ excludeGeneratedPatternsText: string;
81
+ };
82
+
83
+ /** 单条记录 JSON:内容字段 + 可选 `demoUiOptions`(仅导出 demo 写入)。 */
84
+ export type GenAttrCachedRun = GenAttrCachedRunContentFields & {
85
+ demoUiOptions?: Partial<GenAttrDemoUiOptions>;
86
+ };
87
+
88
+ /**
89
+ * 缓存业务 **key 字段**:涵盖所有影响 `steps` 内容的生成参数(决定 `contentKey`)。
90
  * 原则:draft 中存储的可变参数均纳入 key,同参数不同结果不应互相覆盖。
91
  */
92
  export type GenAttrCacheKey = {
 
114
  return buildContentKeyFromBusinessKey(normalizeKey(key));
115
  }
116
 
117
+ /** 构造「内容字段」:供 IndexedDB `save` 与导出 demo 的共有主体(不含 demo UI)。 */
118
+ export function buildGenAttrCachedRunContentPayload(params: {
119
+ initialContext: string;
120
+ steps: TokenGenStep[];
121
+ promptSpans: PromptTokenSpan[];
122
+ completionReason?: CompletionFinishReason;
123
+ draft?: GenAttrRunDraft;
124
+ }): GenAttrCachedRunContentFields {
125
+ const { initialContext, steps, promptSpans, completionReason, draft } = params;
126
  let reasonToStore: CompletionFinishReason | undefined;
127
  if (completionReason !== undefined) {
128
  const c = canonicalizeCompletionFinishReason(completionReason);
 
131
  }
132
  reasonToStore = c;
133
  }
134
+ return {
135
  initialContext,
136
  steps,
137
  ...(promptSpans.length > 0 ? { promptSpans } : {}),
138
  ...(reasonToStore !== undefined ? { completionReason: reasonToStore } : {}),
139
  ...(draft !== undefined ? { draft } : {}),
140
  };
141
+ }
142
+
143
+ /**
144
+ * 仅 **Export demo** 使用:在内容字段上附加 **demoUiOptions** 全量(history 不得调用)。
145
+ */
146
+ export function buildGenAttrExportedDemoPayload(
147
+ params: {
148
+ initialContext: string;
149
+ steps: TokenGenStep[];
150
+ promptSpans: PromptTokenSpan[];
151
+ completionReason?: CompletionFinishReason;
152
+ draft?: GenAttrRunDraft;
153
+ demoUiOptions: GenAttrDemoUiOptions;
154
+ }
155
+ ): GenAttrCachedRun {
156
+ const { demoUiOptions, ...contentParams } = params;
157
+ return { ...buildGenAttrCachedRunContentPayload(contentParams), demoUiOptions };
158
+ }
159
+
160
+ function isValidPromptSpansPayload(v: unknown): boolean {
161
+ if (!Array.isArray(v)) return false;
162
+ for (const item of v) {
163
+ if (item == null || typeof item !== 'object') return false;
164
+ const o = item as Record<string, unknown>;
165
+ const off = o.offset;
166
+ if (!Array.isArray(off) || off.length !== 2) return false;
167
+ if (typeof off[0] !== 'number' || !Number.isFinite(off[0])) return false;
168
+ if (typeof off[1] !== 'number' || !Number.isFinite(off[1])) return false;
169
+ if (typeof o.raw !== 'string') return false;
170
+ if (o.token_id !== undefined && (typeof o.token_id !== 'number' || !Number.isFinite(o.token_id))) {
171
+ return false;
172
+ }
173
+ }
174
+ return true;
175
+ }
176
+
177
+ function isValidGenAttrRunDraftPayload(v: unknown): boolean {
178
+ if (v == null || typeof v !== 'object') return false;
179
+ const d = v as Record<string, unknown>;
180
+ if (d.mode !== 'raw' && d.mode !== 'chat') return false;
181
+ if (d.model !== undefined && typeof d.model !== 'string') return false;
182
+ if (d.maxTokens !== undefined && (typeof d.maxTokens !== 'number' || !Number.isFinite(d.maxTokens))) {
183
+ return false;
184
+ }
185
+ if (d.system !== undefined && typeof d.system !== 'string') return false;
186
+ if (d.user !== undefined && typeof d.user !== 'string') return false;
187
+ if (d.useSystem !== undefined && typeof d.useSystem !== 'boolean') return false;
188
+ if (d.teacherForcing !== undefined && typeof d.teacherForcing !== 'string') return false;
189
+ if (d.stopAfterTeacherForcing !== undefined && typeof d.stopAfterTeacherForcing !== 'boolean') {
190
+ return false;
191
+ }
192
+ return true;
193
+ }
194
+
195
+ function isDagLayoutModePayload(v: unknown): v is DagLayoutMode {
196
+ return v === 'text-flow' || v === 'linear-arc' || v === 'spiral';
197
+ }
198
+
199
+ function isValidDemoUiOptionsPayload(v: unknown): v is Partial<GenAttrDemoUiOptions> {
200
+ if (v == null || typeof v !== 'object') return false;
201
+ const d = v as Record<string, unknown>;
202
+ if (d.layoutMode !== undefined && !isDagLayoutModePayload(d.layoutMode)) return false;
203
+ if (d.measureWidthPx !== undefined && (typeof d.measureWidthPx !== 'number' || !Number.isFinite(d.measureWidthPx))) {
204
+ return false;
205
+ }
206
+ if (d.dagCompactness !== undefined && (typeof d.dagCompactness !== 'number' || !Number.isFinite(d.dagCompactness))) {
207
+ return false;
208
+ }
209
+ if (
210
+ d.linearArcAdjacentGapPx !== undefined &&
211
+ (typeof d.linearArcAdjacentGapPx !== 'number' || !Number.isFinite(d.linearArcAdjacentGapPx))
212
+ ) {
213
+ return false;
214
+ }
215
+ if (d.hideExcludedTokens !== undefined && typeof d.hideExcludedTokens !== 'boolean') return false;
216
+ if (
217
+ d.edgeTopPCoverage !== undefined &&
218
+ (typeof d.edgeTopPCoverage !== 'number' || !Number.isFinite(d.edgeTopPCoverage))
219
+ ) {
220
+ return false;
221
+ }
222
+ if (d.nodeCiVisualScaleEnabled !== undefined && typeof d.nodeCiVisualScaleEnabled !== 'boolean') {
223
+ return false;
224
+ }
225
+ if (d.edgeWeakenHighSurprisalEnabled !== undefined && typeof d.edgeWeakenHighSurprisalEnabled !== 'boolean') {
226
+ return false;
227
+ }
228
+ if (d.hideInactiveEdges !== undefined && typeof d.hideInactiveEdges !== 'boolean') return false;
229
+ if (d.replayPacingMode !== undefined && d.replayPacingMode !== 'total' && d.replayPacingMode !== 'step') {
230
+ return false;
231
+ }
232
+ if (d.playbackTotalS !== undefined && (typeof d.playbackTotalS !== 'number' || !Number.isFinite(d.playbackTotalS))) {
233
+ return false;
234
+ }
235
+ if (d.playbackStepMs !== undefined && (typeof d.playbackStepMs !== 'number' || !Number.isFinite(d.playbackStepMs))) {
236
+ return false;
237
+ }
238
+ if (
239
+ d.excludePromptPatternsEnabled !== undefined &&
240
+ typeof d.excludePromptPatternsEnabled !== 'boolean'
241
+ ) {
242
+ return false;
243
+ }
244
+ if (d.excludePromptPatternsText !== undefined && typeof d.excludePromptPatternsText !== 'string') {
245
+ return false;
246
+ }
247
+ if (
248
+ d.excludeGeneratedPatternsEnabled !== undefined &&
249
+ typeof d.excludeGeneratedPatternsEnabled !== 'boolean'
250
+ ) {
251
+ return false;
252
+ }
253
+ if (
254
+ d.excludeGeneratedPatternsText !== undefined &&
255
+ typeof d.excludeGeneratedPatternsText !== 'string'
256
+ ) {
257
+ return false;
258
+ }
259
+ return true;
260
+ }
261
+
262
+ /**
263
+ * 打包 demo JSON 与 Cached history 负载对齐:`steps` 仅要求非空数组(细粒度由运行时承担)。
264
+ */
265
+ export function isValidGenAttrCachedRunPayload(v: unknown): v is GenAttrCachedRun {
266
+ if (v == null || typeof v !== 'object') return false;
267
+ const o = v as Record<string, unknown>;
268
+ if (typeof o.initialContext !== 'string' || !Array.isArray(o.steps) || o.steps.length === 0) {
269
+ return false;
270
+ }
271
+ if (o.completionReason !== undefined) {
272
+ if (typeof o.completionReason !== 'string' || !isKnownPersistedCompletionReason(o.completionReason)) {
273
+ return false;
274
+ }
275
+ }
276
+ if (o.promptSpans !== undefined && !isValidPromptSpansPayload(o.promptSpans)) {
277
+ return false;
278
+ }
279
+ if (o.draft !== undefined && !isValidGenAttrRunDraftPayload(o.draft)) {
280
+ return false;
281
+ }
282
+ if (o.demoUiOptions !== undefined && !isValidDemoUiOptionsPayload(o.demoUiOptions)) {
283
+ return false;
284
+ }
285
+ return true;
286
+ }
287
+
288
+ /**
289
+ * 加载 demo 与加载 IndexedDB 历史共用的入口:`unknown` → 合法则返回记录,否则打日志并返回 `undefined`。
290
+ */
291
+ export function parseGenAttrCachedRunPayload(
292
+ raw: unknown,
293
+ contextForLog?: string
294
+ ): GenAttrCachedRun | undefined {
295
+ if (!isValidGenAttrCachedRunPayload(raw)) {
296
+ const suffix =
297
+ contextForLog !== undefined && contextForLog.length > 0 ? ` (${contextForLog})` : '';
298
+ console.warn(`[genAttributeRunCache] invalid GenAttrCachedRun payload${suffix}`);
299
+ return undefined;
300
+ }
301
+ return raw;
302
+ }
303
+
304
+ export async function save(
305
+ key: GenAttrCacheKey,
306
+ steps: TokenGenStep[],
307
+ promptSpans: PromptTokenSpan[],
308
+ status: 'partial' | 'complete' = steps.length > 0 ? 'partial' : 'complete',
309
+ completionReason?: CompletionFinishReason,
310
+ draft?: GenAttrRunDraft
311
+ ): Promise<void> {
312
+ const { initialContext } = key;
313
+ const payload = buildGenAttrCachedRunContentPayload({
314
+ initialContext,
315
+ steps,
316
+ promptSpans,
317
+ completionReason,
318
+ draft,
319
+ });
320
  await upsertEntry({
321
  namespace: NAMESPACE,
322
  businessKeyJson: JSON.stringify(normalizeKey(key)),
 
329
 
330
  export async function get(key: GenAttrCacheKey): Promise<GenAttrCachedRun | undefined> {
331
  const row = await getByContentKey<GenAttrCachedRun>(NAMESPACE, keyHash(key));
332
+ if (!row) return undefined;
333
+ return parseGenAttrCachedRunPayload(row.payload, 'get(GenAttrCacheKey)');
334
  }
335
 
336
  export async function getCachedEntryByContentKey(raw: string): Promise<GenAttrCachedRun | undefined> {
337
  if (!raw) return undefined;
338
  const row = await getByContentKey<GenAttrCachedRun>(NAMESPACE, raw);
339
+ if (!row) return undefined;
340
+ return parseGenAttrCachedRunPayload(row.payload, `contentKey=${raw}`);
341
  }
342
 
343
  export function buildCachedContentUrlParam(key: GenAttrCacheKey): string {
client/src/ts/ui/dialog.ts CHANGED
@@ -579,6 +579,34 @@ export function createNamePathTextContent(
579
  };
580
  }
581
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
582
  /**
583
  * 创建 URL 输入弹窗内容(URL 输入框)
584
  */
@@ -596,7 +624,8 @@ export function createUrlInputContent(
596
  .text(label);
597
 
598
  const input = container.append('input')
599
- .attr('type', 'url')
 
600
  .attr('class', 'dialog-input')
601
  .attr('value', defaultValue)
602
  .attr('placeholder', placeholder || 'https://example.com');
@@ -626,21 +655,21 @@ export function createUrlInputContent(
626
  return {
627
  getValue: () => {
628
  const inputNode = input.node() as HTMLInputElement;
629
- return inputNode?.value?.trim() || '';
 
630
  },
631
  validate: () => {
632
  const inputNode = input.node() as HTMLInputElement;
633
- const value = inputNode?.value?.trim() || '';
634
- // 基本的 URL 验证
635
- if (value.length === 0) {
636
  return false;
637
  }
638
- try {
639
- new URL(value);
640
  return true;
641
- } catch {
642
- return false;
643
  }
 
 
644
  },
645
  focus: () => {
646
  const inputNode = input.node() as HTMLInputElement;
 
579
  };
580
  }
581
 
582
+ /**
583
+ * 将地址栏风格的输入转为绝对 URL:缺协议时补 `https://`;`//host/path` 先去掉协议相对前缀再补全。
584
+ * 若无法解析则返回 null。
585
+ */
586
+ function normalizeUserFetchUrl(raw: string): string | null {
587
+ const trimmed = raw.trim();
588
+ if (!trimmed) {
589
+ return null;
590
+ }
591
+ try {
592
+ new URL(trimmed);
593
+ return trimmed;
594
+ } catch {
595
+ if (/^[a-zA-Z][a-zA-Z\d+\-.]*:/.test(trimmed)) {
596
+ return null;
597
+ }
598
+ const rest = trimmed.replace(/^\/+/, '');
599
+ if (!rest) {
600
+ return null;
601
+ }
602
+ try {
603
+ return new URL(`https://${rest}`).href;
604
+ } catch {
605
+ return null;
606
+ }
607
+ }
608
+ }
609
+
610
  /**
611
  * 创建 URL 输入弹窗内容(URL 输入框)
612
  */
 
624
  .text(label);
625
 
626
  const input = container.append('input')
627
+ .attr('type', 'text')
628
+ .attr('inputmode', 'url')
629
  .attr('class', 'dialog-input')
630
  .attr('value', defaultValue)
631
  .attr('placeholder', placeholder || 'https://example.com');
 
655
  return {
656
  getValue: () => {
657
  const inputNode = input.node() as HTMLInputElement;
658
+ const raw = inputNode?.value?.trim() || '';
659
+ return normalizeUserFetchUrl(raw) ?? '';
660
  },
661
  validate: () => {
662
  const inputNode = input.node() as HTMLInputElement;
663
+ const raw = inputNode?.value?.trim() || '';
664
+ if (raw.length === 0) {
665
+ showAlertDialog(tr('Invalid input'), tr('Please enter a URL.'));
666
  return false;
667
  }
668
+ if (normalizeUserFetchUrl(raw)) {
 
669
  return true;
 
 
670
  }
671
+ showAlertDialog(tr('Invalid URL'), tr('This does not look like a valid URL. Check for typos.'));
672
+ return false;
673
  },
674
  focus: () => {
675
  const inputNode = input.node() as HTMLInputElement;