dqy08 commited on
Commit
a0b7722
·
1 Parent(s): c911b05

prediction attribute 统计和log改进. history下拉高度改进;某些demo从14b模型改为1.7b模型,更符合直觉

Browse files
backend/access_log.py CHANGED
@@ -213,6 +213,9 @@ def log_prediction_attribute_request(
213
  target_prediction: Optional[str],
214
  target_token_id: Optional[int],
215
  model: str,
 
 
 
216
  client_ip: str = None,
217
  ) -> int:
218
  """
@@ -227,19 +230,30 @@ def log_prediction_attribute_request(
227
  _request_counter += 1
228
  request_id = _request_counter
229
 
230
- context_preview = 150
231
  c_preview = _log_str_preview(context, context_preview)
232
  if target_token_id is not None:
233
  target_show = f"<token_id:{target_token_id}>"
234
  else:
235
  target_show = "<top-1>" if target_prediction is None else target_prediction
236
  details = (
237
- f"req_id={request_id}, model={model!r}, context='{c_preview}', target='{target_show}', "
238
- f"context_chars={len(context)}"
239
  )
240
- _log_request("📥 prediction_attribute 请求", details, client_ip)
241
-
242
- _hit_api("prediction_attribute")
 
 
 
 
 
 
 
 
 
 
 
243
  return request_id
244
 
245
 
 
213
  target_prediction: Optional[str],
214
  target_token_id: Optional[int],
215
  model: str,
216
+ source_page: str,
217
+ flow_id: Optional[str] = None,
218
+ flow_step: Optional[int] = None,
219
  client_ip: str = None,
220
  ) -> int:
221
  """
 
230
  _request_counter += 1
231
  request_id = _request_counter
232
 
233
+ context_preview = 200
234
  c_preview = _log_str_preview(context, context_preview)
235
  if target_token_id is not None:
236
  target_show = f"<token_id:{target_token_id}>"
237
  else:
238
  target_show = "<top-1>" if target_prediction is None else target_prediction
239
  details = (
240
+ f"req_id={request_id}, model={model!r}, source_page={source_page!r}, "
241
+ f"context='{c_preview}', target='{target_show}', context_chars={len(context)}"
242
  )
243
+ if flow_id is not None:
244
+ details += f", flow_id={flow_id!r}, flow_step={flow_step}"
245
+
246
+ # 连续 flow 第 1 步后不再打印入站请求,避免日志噪声。
247
+ if flow_id is None or flow_step == 0:
248
+ _log_request("📥 prediction_attribute 请求", details, client_ip)
249
+
250
+ is_flow_request = source_page == "gen_attribute.html"
251
+ if is_flow_request:
252
+ if flow_step == 0:
253
+ _hit_api("causal_flow")
254
+ _hit_api("prediction_attribute")
255
+ else:
256
+ _hit_api(f"prediction_attribute__{source_page}")
257
  return request_id
258
 
259
 
backend/api/prediction_attribute.py CHANGED
@@ -26,6 +26,9 @@ def prediction_attribute(attribution_request):
26
  target_prediction = attribution_request.get("target_prediction")
27
  target_token_id = attribution_request.get("target_token_id")
28
  model = attribution_request.get("model")
 
 
 
29
 
30
  if context is None:
31
  return {"success": False, "message": "Missing required field: context"}, 400
@@ -52,6 +55,46 @@ def prediction_attribute(attribution_request):
52
  if model not in ("base", "instruct"):
53
  return {"success": False, "message": 'model must be "base" or "instruct"'}, 400
54
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
  client_ip = get_client_ip()
56
  start_time = time.perf_counter()
57
  request_id = log_prediction_attribute_request(
@@ -59,6 +102,9 @@ def prediction_attribute(attribution_request):
59
  target_prediction=target_prediction,
60
  target_token_id=target_token_id,
61
  model=model,
 
 
 
62
  client_ip=client_ip,
63
  )
64
 
@@ -93,9 +139,16 @@ def prediction_attribute(attribution_request):
93
  elapsed = time.perf_counter() - start_time
94
  tokens = len(result.get("token_attribution", []))
95
  target_token = result.get("target_token")
96
- print(
97
- f"\t📤 API prediction_attribute response: req_id={request_id}, "
98
- f"target={target_token!r}, tokens={tokens}, response_time={elapsed:.4f}s"
99
- )
 
 
 
 
 
 
 
100
 
101
  return {"success": True, **result}, 200
 
26
  target_prediction = attribution_request.get("target_prediction")
27
  target_token_id = attribution_request.get("target_token_id")
28
  model = attribution_request.get("model")
29
+ source_page = attribution_request.get("source_page")
30
+ flow_id = attribution_request.get("flow_id")
31
+ flow_step = attribution_request.get("flow_step")
32
 
33
  if context is None:
34
  return {"success": False, "message": "Missing required field: context"}, 400
 
55
  if model not in ("base", "instruct"):
56
  return {"success": False, "message": 'model must be "base" or "instruct"'}, 400
57
 
58
+ allowed_source_pages = {
59
+ "analysis.html",
60
+ "chat.html",
61
+ "attribution.html",
62
+ "gen_attribute.html",
63
+ }
64
+
65
+ if source_page is None:
66
+ return {"success": False, "message": "Missing required field: source_page"}, 400
67
+ if not isinstance(source_page, str):
68
+ return {"success": False, "message": "source_page must be a string"}, 400
69
+ if source_page == "":
70
+ return {"success": False, "message": "source_page must not be empty"}, 400
71
+ if source_page not in allowed_source_pages:
72
+ return {
73
+ "success": False,
74
+ "message": "source_page must be one of: analysis.html, chat.html, attribution.html, gen_attribute.html",
75
+ }, 400
76
+
77
+ if flow_id is not None and not isinstance(flow_id, str):
78
+ return {"success": False, "message": "flow_id must be a string"}, 400
79
+ if flow_id == "":
80
+ return {"success": False, "message": "flow_id must not be empty"}, 400
81
+ if flow_step is not None and not isinstance(flow_step, int):
82
+ return {"success": False, "message": "flow_step must be an integer"}, 400
83
+ if flow_step is not None and flow_step < 0:
84
+ return {"success": False, "message": "flow_step must be >= 0"}, 400
85
+
86
+ is_causal_flow = source_page == "gen_attribute.html"
87
+ if is_causal_flow:
88
+ if flow_id is None:
89
+ return {"success": False, "message": "Missing required field: flow_id for causal flow"}, 400
90
+ if flow_step is None:
91
+ return {"success": False, "message": "Missing required field: flow_step for causal flow"}, 400
92
+ elif flow_id is not None or flow_step is not None:
93
+ return {
94
+ "success": False,
95
+ "message": "flow_id/flow_step are only allowed when source_page is gen_attribute.html",
96
+ }, 400
97
+
98
  client_ip = get_client_ip()
99
  start_time = time.perf_counter()
100
  request_id = log_prediction_attribute_request(
 
102
  target_prediction=target_prediction,
103
  target_token_id=target_token_id,
104
  model=model,
105
+ source_page=source_page,
106
+ flow_id=flow_id,
107
+ flow_step=flow_step,
108
  client_ip=client_ip,
109
  )
110
 
 
139
  elapsed = time.perf_counter() - start_time
140
  tokens = len(result.get("token_attribution", []))
141
  target_token = result.get("target_token")
142
+ if flow_id is None:
143
+ print(
144
+ f"\t📤 API prediction_attribute response: req_id={request_id}, "
145
+ f"target={target_token!r}, tokens={tokens}, response_time={elapsed:.4f}s"
146
+ )
147
+ else:
148
+ print(
149
+ f"\t📤 API prediction_attribute response: req_id={request_id}, "
150
+ f"flow_id={flow_id!r}, flow_step={flow_step}, "
151
+ f"target={target_token!r}, tokens={tokens}, response_time={elapsed:.4f}s"
152
+ )
153
 
154
  return {"success": True, **result}, 200
client/src/css/attribution.scss CHANGED
@@ -14,11 +14,9 @@
14
  }
15
  }
16
 
17
- // Cached history 下拉:覆盖全局 query-history(mixin max-height:200px,须显式 max-height:none 才取消上限)原生竖向 resize
18
  #attribution_cached_history_dropdown.semantic-search-history-dropdown {
19
- height: min(32vh, 360px);
20
- min-height: 120px;
21
- max-height: none;
22
  resize: vertical;
23
  overflow-y: auto;
24
  }
 
14
  }
15
  }
16
 
17
+ // Cached history 下拉:覆盖 mixin max-height:200px;高度随条目、封顶 min(32vh,360px),并可竖向 resize
18
  #attribution_cached_history_dropdown.semantic-search-history-dropdown {
19
+ max-height: min(32vh, 360px);
 
 
20
  resize: vertical;
21
  overflow-y: auto;
22
  }
client/src/css/gen_attribute.scss CHANGED
@@ -32,11 +32,10 @@
32
  position: static;
33
  }
34
 
 
35
  #gen_attr_cached_history_dropdown.semantic-search-history-dropdown,
36
  #gen_attr_cached_demos_dropdown.semantic-search-history-dropdown {
37
- height: min(32vh, 360px);
38
- min-height: 120px;
39
- max-height: none;
40
  resize: vertical;
41
  overflow-y: auto;
42
  }
 
32
  position: static;
33
  }
34
 
35
+ // Cached history / demos:随内容变高,封顶 min(32vh, 360px),竖向 resize;覆盖 chat mixin 的 max-height:200px
36
  #gen_attr_cached_history_dropdown.semantic-search-history-dropdown,
37
  #gen_attr_cached_demos_dropdown.semantic-search-history-dropdown {
38
+ max-height: min(32vh, 360px);
 
 
39
  resize: vertical;
40
  overflow-y: auto;
41
  }
client/src/ts/attribution.ts CHANGED
@@ -272,6 +272,7 @@ async function runAnalyze(options?: { forceRefresh?: boolean }): Promise<void> {
272
  context,
273
  targetPrediction: target,
274
  model: currentAttributionModelVariant(),
 
275
  forceRefresh,
276
  });
277
  applyAttributionResponse(context, json);
 
272
  context,
273
  targetPrediction: target,
274
  model: currentAttributionModelVariant(),
275
+ sourcePage: 'attribution.html',
276
  forceRefresh,
277
  });
278
  applyAttributionResponse(context, json);
client/src/ts/attribution/densityAttributionSidebar.ts CHANGED
@@ -72,6 +72,7 @@ export type DensityAttributionSidebarOptions = {
72
  getContextPrefix?: () => string;
73
  /** 首页 base;Chat instruct */
74
  predictionModelVariant: PredictionAttributeModelVariant;
 
75
  };
76
 
77
  /**
@@ -299,6 +300,7 @@ export function initDensityAttributionSidebar(options: DensityAttributionSidebar
299
  context,
300
  targetPrediction: selectedTarget,
301
  model: options.predictionModelVariant,
 
302
  forceRefresh: false,
303
  });
304
  finish(json);
 
72
  getContextPrefix?: () => string;
73
  /** 首页 base;Chat instruct */
74
  predictionModelVariant: PredictionAttributeModelVariant;
75
+ sourcePage: 'analysis.html' | 'chat.html';
76
  };
77
 
78
  /**
 
300
  context,
301
  targetPrediction: selectedTarget,
302
  model: options.predictionModelVariant,
303
+ sourcePage: options.sourcePage,
304
  forceRefresh: false,
305
  });
306
  finish(json);
client/src/ts/attribution/predictionAttributeClient.ts CHANGED
@@ -12,21 +12,35 @@ import {
12
  } from './attributionResultCache';
13
 
14
  const JSON_ERROR_SNIPPET_MAX = 160;
 
 
 
 
 
15
 
16
  export async function fetchPredictionAttribute(
17
  apiBaseForRequests: string,
18
  context: string,
19
  targetPrediction: string | null,
20
  model: PredictionAttributeModelVariant,
21
- targetTokenId?: number
 
 
 
22
  ): Promise<AttributionApiResponse> {
23
- const bodyObj: Record<string, unknown> = { context, model };
24
  if (targetPrediction !== null) {
25
  bodyObj.target_prediction = targetPrediction;
26
  }
27
  if (typeof targetTokenId === 'number' && Number.isInteger(targetTokenId) && targetTokenId >= 0) {
28
  bodyObj.target_token_id = targetTokenId;
29
  }
 
 
 
 
 
 
30
  const res = await fetch(`${apiBaseForRequests}/api/prediction-attribute`, {
31
  method: 'POST',
32
  headers: { 'Content-Type': 'application/json' },
@@ -57,6 +71,7 @@ export type LoadPredictionAttributeWithCacheOptions = {
57
  context: string;
58
  targetPrediction: string;
59
  model: PredictionAttributeModelVariant;
 
60
  /** 与归因页「Force retry」一致:先按 entry 删缓存再请求 */
61
  forceRefresh?: boolean;
62
  };
@@ -67,7 +82,7 @@ export type LoadPredictionAttributeWithCacheOptions = {
67
  export async function loadPredictionAttributeWithCache(
68
  options: LoadPredictionAttributeWithCacheOptions
69
  ): Promise<AttributionApiResponse> {
70
- const { apiBaseForRequests, context, targetPrediction, model, forceRefresh } = options;
71
  if (forceRefresh) {
72
  await removeCachedEntryByContentKey(entryKey(context, targetPrediction));
73
  }
@@ -77,7 +92,7 @@ export async function loadPredictionAttributeWithCache(
77
  return hit;
78
  }
79
  }
80
- const json = await fetchPredictionAttribute(apiBaseForRequests, context, targetPrediction, model);
81
  await save({ context, targetPrediction }, json, 'complete');
82
  return json;
83
  }
 
12
  } from './attributionResultCache';
13
 
14
  const JSON_ERROR_SNIPPET_MAX = 160;
15
+ export type PredictionAttributeSourcePage =
16
+ | 'analysis.html'
17
+ | 'chat.html'
18
+ | 'attribution.html'
19
+ | 'gen_attribute.html';
20
 
21
  export async function fetchPredictionAttribute(
22
  apiBaseForRequests: string,
23
  context: string,
24
  targetPrediction: string | null,
25
  model: PredictionAttributeModelVariant,
26
+ sourcePage: PredictionAttributeSourcePage,
27
+ targetTokenId?: number,
28
+ flowId?: string,
29
+ flowStep?: number,
30
  ): Promise<AttributionApiResponse> {
31
+ const bodyObj: Record<string, unknown> = { context, model, source_page: sourcePage };
32
  if (targetPrediction !== null) {
33
  bodyObj.target_prediction = targetPrediction;
34
  }
35
  if (typeof targetTokenId === 'number' && Number.isInteger(targetTokenId) && targetTokenId >= 0) {
36
  bodyObj.target_token_id = targetTokenId;
37
  }
38
+ if (typeof flowId === 'string' && flowId.length > 0) {
39
+ bodyObj.flow_id = flowId;
40
+ }
41
+ if (typeof flowStep === 'number' && Number.isInteger(flowStep) && flowStep >= 0) {
42
+ bodyObj.flow_step = flowStep;
43
+ }
44
  const res = await fetch(`${apiBaseForRequests}/api/prediction-attribute`, {
45
  method: 'POST',
46
  headers: { 'Content-Type': 'application/json' },
 
71
  context: string;
72
  targetPrediction: string;
73
  model: PredictionAttributeModelVariant;
74
+ sourcePage: PredictionAttributeSourcePage;
75
  /** 与归因页「Force retry」一致:先按 entry 删缓存再请求 */
76
  forceRefresh?: boolean;
77
  };
 
82
  export async function loadPredictionAttributeWithCache(
83
  options: LoadPredictionAttributeWithCacheOptions
84
  ): Promise<AttributionApiResponse> {
85
+ const { apiBaseForRequests, context, targetPrediction, model, sourcePage, forceRefresh } = options;
86
  if (forceRefresh) {
87
  await removeCachedEntryByContentKey(entryKey(context, targetPrediction));
88
  }
 
92
  return hit;
93
  }
94
  }
95
+ const json = await fetchPredictionAttribute(apiBaseForRequests, context, targetPrediction, model, sourcePage);
96
  await save({ context, targetPrediction }, json, 'complete');
97
  return json;
98
  }
client/src/ts/attribution/tokenGenAttributionRunner.ts CHANGED
@@ -7,6 +7,9 @@ import type { PromptTokenSpan } from './genAttributeDagPreprocess';
7
  import type { CompletionFinishReason } from '../utils/generationEndReasonLabel';
8
  import { fetchPredictionAttribute, fetchTokenize } from './predictionAttributeClient';
9
 
 
 
 
10
  function splitCodePointPrefix(text: string, prefixLength: number): { prefix: string; rest: string } | null {
11
  if (prefixLength < 0) return null;
12
  const chars = Array.from(text);
@@ -45,12 +48,14 @@ export type TokenGenAttributionOptions = {
45
  * `true`:停止;`false`(默认):切换为 top-1 继续生成,直到 maxTokens 或 EOS。
46
  */
47
  stopAfterTeacherForcing?: boolean;
48
- /** 最大生成 token 数,默认 200 */
49
  maxTokens?: number;
50
  /** 每生成一个 token 后的回调;`stepIndex` 从 0 起,与 {@link TokenGenAttributionHandle.getAllSteps} 下标一致 */
51
  onStep: (step: TokenGenStep, stepIndex: number) => void;
52
  onComplete: (reason: CompletionFinishReason) => void;
53
  onError: (err: Error) => void;
 
 
54
  };
55
 
56
  export type TokenGenAttributionHandle = {
@@ -62,7 +67,14 @@ export type TokenGenAttributionHandle = {
62
  };
63
 
64
  export function startTokenGenAttribution(opts: TokenGenAttributionOptions): TokenGenAttributionHandle {
65
- const { initialContext, apiPrefix, model, maxTokens = 200, stopAfterTeacherForcing = false } = opts;
 
 
 
 
 
 
 
66
  const tfOpt = opts.teacherForcingContinuation;
67
  const forcingEnabled = typeof tfOpt === 'string' && tfOpt.length > 0;
68
  const promptRegionEnd = initialContext.length;
@@ -164,7 +176,16 @@ export function startTokenGenAttribution(opts: TokenGenAttributionOptions): Toke
164
 
165
  let response: AttributionApiResponse;
166
  try {
167
- response = await fetchPredictionAttribute(apiPrefix, context, null, model, targetTokenId);
 
 
 
 
 
 
 
 
 
168
  } catch (err) {
169
  const error = err instanceof Error ? err : new Error(String(err));
170
  opts.onError(error);
 
7
  import type { CompletionFinishReason } from '../utils/generationEndReasonLabel';
8
  import { fetchPredictionAttribute, fetchTokenize } from './predictionAttributeClient';
9
 
10
+ /** 与生成归因页(含 DAG)「Max tokens」输入框默认值一致 */
11
+ export const TOKEN_GEN_MAX_TOKENS_DEFAULT = 100;
12
+
13
  function splitCodePointPrefix(text: string, prefixLength: number): { prefix: string; rest: string } | null {
14
  if (prefixLength < 0) return null;
15
  const chars = Array.from(text);
 
48
  * `true`:停止;`false`(默认):切换为 top-1 继续生成,直到 maxTokens 或 EOS。
49
  */
50
  stopAfterTeacherForcing?: boolean;
51
+ /** 最大生成 token 数,默认 {@link TOKEN_GEN_MAX_TOKENS_DEFAULT} */
52
  maxTokens?: number;
53
  /** 每生成一个 token 后的回调;`stepIndex` 从 0 起,与 {@link TokenGenAttributionHandle.getAllSteps} 下标一致 */
54
  onStep: (step: TokenGenStep, stepIndex: number) => void;
55
  onComplete: (reason: CompletionFinishReason) => void;
56
  onError: (err: Error) => void;
57
+ /** 单次连续生成归因会话 ID;用于后端日志压缩与统计归类。 */
58
+ flowId: string;
59
  };
60
 
61
  export type TokenGenAttributionHandle = {
 
67
  };
68
 
69
  export function startTokenGenAttribution(opts: TokenGenAttributionOptions): TokenGenAttributionHandle {
70
+ const {
71
+ initialContext,
72
+ apiPrefix,
73
+ model,
74
+ maxTokens = TOKEN_GEN_MAX_TOKENS_DEFAULT,
75
+ stopAfterTeacherForcing = false,
76
+ flowId,
77
+ } = opts;
78
  const tfOpt = opts.teacherForcingContinuation;
79
  const forcingEnabled = typeof tfOpt === 'string' && tfOpt.length > 0;
80
  const promptRegionEnd = initialContext.length;
 
176
 
177
  let response: AttributionApiResponse;
178
  try {
179
+ response = await fetchPredictionAttribute(
180
+ apiPrefix,
181
+ context,
182
+ null,
183
+ model,
184
+ 'gen_attribute.html',
185
+ targetTokenId,
186
+ flowId,
187
+ steps.length,
188
+ );
189
  } catch (err) {
190
  const error = err instanceof Error ? err : new Error(String(err));
191
  opts.onError(error);
client/src/ts/chat.ts CHANGED
@@ -701,4 +701,5 @@ initDensityAttributionSidebar({
701
  showToast,
702
  getContextPrefix: () => currentPromptUsed,
703
  predictionModelVariant: 'instruct',
 
704
  });
 
701
  showToast,
702
  getContextPrefix: () => currentPromptUsed,
703
  predictionModelVariant: 'instruct',
704
+ sourcePage: 'chat.html',
705
  });
client/src/ts/gen_attribute.ts CHANGED
@@ -34,6 +34,7 @@ import {
34
  import {
35
  createHydratedTokenGenHandle,
36
  startTokenGenAttribution,
 
37
  type TokenGenAttributionHandle,
38
  type TokenGenStep,
39
  } from './attribution/tokenGenAttributionRunner';
@@ -90,7 +91,7 @@ const showToast = createToast('#toast').show;
90
 
91
  const GEN_ATTR_MODEL_VARIANT_STORAGE_KEY = 'info_radar_gen_attr_model_variant';
92
  const GEN_ATTR_MAX_TOKENS_STORAGE_KEY = 'info_radar_gen_attr_max_tokens';
93
- const GEN_ATTR_MAX_TOKENS_DEFAULT = 100;
94
  const GEN_ATTR_DAG_MEASURE_WIDTH_STORAGE_KEY = 'info_radar_gen_attr_dag_measure_width';
95
  const GEN_ATTR_DAG_LAYOUT_MODE_STORAGE_KEY = 'info_radar_gen_attr_dag_layout_mode';
96
  const GEN_ATTR_DAG_PLAYBACK_STEP_MS_STORAGE_KEY = 'info_radar_gen_attr_dag_playback_step_ms';
@@ -121,6 +122,12 @@ const GEN_ATTR_DAG_PLAYBACK_TOTAL_S_MAX = 3600;
121
  const GENERATE_BTN_LABEL = 'Start';
122
  const STOP_BTN_LABEL = 'Stop';
123
 
 
 
 
 
 
 
124
  function readStoredModelVariant(): PredictionAttributeModelVariant {
125
  try {
126
  const v = localStorage.getItem(GEN_ATTR_MODEL_VARIANT_STORAGE_KEY);
@@ -1595,6 +1602,7 @@ async function runGeneration(): Promise<void> {
1595
  apiPrefix: apiBaseForRequests,
1596
  model: tokenizeModel,
1597
  maxTokens,
 
1598
  teacherForcingContinuation: teacherForcingText,
1599
  stopAfterTeacherForcing: stopAfterTF,
1600
  onStep(step, stepIndex) {
 
34
  import {
35
  createHydratedTokenGenHandle,
36
  startTokenGenAttribution,
37
+ TOKEN_GEN_MAX_TOKENS_DEFAULT,
38
  type TokenGenAttributionHandle,
39
  type TokenGenStep,
40
  } from './attribution/tokenGenAttributionRunner';
 
91
 
92
  const GEN_ATTR_MODEL_VARIANT_STORAGE_KEY = 'info_radar_gen_attr_model_variant';
93
  const GEN_ATTR_MAX_TOKENS_STORAGE_KEY = 'info_radar_gen_attr_max_tokens';
94
+ const GEN_ATTR_MAX_TOKENS_DEFAULT = TOKEN_GEN_MAX_TOKENS_DEFAULT;
95
  const GEN_ATTR_DAG_MEASURE_WIDTH_STORAGE_KEY = 'info_radar_gen_attr_dag_measure_width';
96
  const GEN_ATTR_DAG_LAYOUT_MODE_STORAGE_KEY = 'info_radar_gen_attr_dag_layout_mode';
97
  const GEN_ATTR_DAG_PLAYBACK_STEP_MS_STORAGE_KEY = 'info_radar_gen_attr_dag_playback_step_ms';
 
122
  const GENERATE_BTN_LABEL = 'Start';
123
  const STOP_BTN_LABEL = 'Stop';
124
 
125
+ function createFlowId(): string {
126
+ const timePart = Date.now().toString(36).slice(-6);
127
+ const randPart = Math.random().toString(36).slice(2, 6);
128
+ return `${timePart}-${randPart}`;
129
+ }
130
+
131
  function readStoredModelVariant(): PredictionAttributeModelVariant {
132
  try {
133
  const v = localStorage.getItem(GEN_ATTR_MODEL_VARIANT_STORAGE_KEY);
 
1602
  apiPrefix: apiBaseForRequests,
1603
  model: tokenizeModel,
1604
  maxTokens,
1605
+ flowId: createFlowId(),
1606
  teacherForcingContinuation: teacherForcingText,
1607
  stopAfterTeacherForcing: stopAfterTF,
1608
  onStep(step, stepIndex) {
client/src/ts/start.ts CHANGED
@@ -1007,6 +1007,7 @@ window.onload = () => {
1007
  apiPrefix: api_prefix,
1008
  showToast,
1009
  predictionModelVariant: 'base',
 
1010
  });
1011
 
1012
  // 高亮清除事件监听已由 initHighlightClearListeners 处理
 
1007
  apiPrefix: api_prefix,
1008
  showToast,
1009
  predictionModelVariant: 'base',
1010
+ sourcePage: 'analysis.html',
1011
  });
1012
 
1013
  // 高亮清除事件监听已由 initHighlightClearListeners 处理
client/src/ts/utils/settingsMenuManager.ts CHANGED
@@ -383,7 +383,16 @@ export class SettingsMenuManager {
383
  'attribution.html',
384
  'gen_attribute.html',
385
  ] as const;
386
- const API_ORDER = ['analyze', 'analyze_semantic', 'chat', 'prediction_attribute'] as const;
 
 
 
 
 
 
 
 
 
387
  const OS_ORDER = ['ios', 'android', 'windows', 'macos', 'linux', 'unknown'] as const;
388
 
389
  type VisitStatsRow = NonNullable<Awaited<ReturnType<TextAnalysisAPI['getVisitStats']>>>;
 
383
  'attribution.html',
384
  'gen_attribute.html',
385
  ] as const;
386
+ const API_ORDER = [
387
+ 'analyze',
388
+ 'analyze_semantic',
389
+ 'chat',
390
+ 'causal_flow',
391
+ 'prediction_attribute',
392
+ 'prediction_attribute__attribution.html',
393
+ 'prediction_attribute__chat.html',
394
+ 'prediction_attribute__analysis.html',
395
+ ] as const;
396
  const OS_ORDER = ['ios', 'android', 'windows', 'macos', 'linux', 'unknown'] as const;
397
 
398
  type VisitStatsRow = NonNullable<Awaited<ReturnType<TextAnalysisAPI['getVisitStats']>>>;
data/demo/public/CN/百科 克里斯蒂亚诺·罗纳尔多_qwen3-1.7b.json ADDED
The diff for this file is too large to render. See raw diff
 
data/demo/public/GPT-2 large unicorn text.json CHANGED
The diff for this file is too large to render. See raw diff
 
data/demo/public/Wiki - Cristiano Ronaldo.json CHANGED
The diff for this file is too large to render. See raw diff
 
server.yaml CHANGED
@@ -505,9 +505,19 @@ paths:
505
  type: string
506
  enum: [base, instruct]
507
  description: base 使用主槽位(--model),instruct 使用语义槽位(--semantic_model)
 
 
 
 
 
 
 
 
 
508
  required:
509
  - context
510
  - model
 
511
  responses:
512
  200:
513
  description: 返回各输入 token 对目标预测的归因分
 
505
  type: string
506
  enum: [base, instruct]
507
  description: base 使用主槽位(--model),instruct 使用语义槽位(--semantic_model)
508
+ source_page:
509
+ type: string
510
+ description: 发起页面名(analysis.html / chat.html / attribution.html / gen_attribute.html)
511
+ flow_id:
512
+ type: string
513
+ description: 连续生成归因会话 ID;仅 source_page=gen_attribute.html 时允许
514
+ flow_step:
515
+ type: integer
516
+ description: 连续生成归因步骤(从 0 开始);仅 source_page=gen_attribute.html 时允许
517
  required:
518
  - context
519
  - model
520
+ - source_page
521
  responses:
522
  200:
523
  description: 返回各输入 token 对目标预测的归因分