dqy08 commited on
Commit
9702df4
·
1 Parent(s): 9ee4267

Enhance model selection and demo management

Browse files

- Updated server.py to include help information for registered models in the argument parser.
- Adjusted max_token_length for local_mps in runtime_config.py to improve performance.
- Added char surprisal histogram support in compare.ts, including rendering and updating logic.
- Refactored demo file handling in start.ts and serverDemoController.ts to streamline filename management.
- Updated demo resource loading to use a new identifier format in demoResourceLoader.ts.
- Improved highlight functionality for char surprisal in highlightUtils.ts.
- Enhanced app state management to track current file names across various components.

backend/runtime_config.py CHANGED
@@ -59,7 +59,7 @@ RUNTIME_CONFIGS = {
59
  },
60
  # 本地 Apple Silicon(保守)
61
  "local_mps": {
62
- "max_token_length": 5000,
63
  "chunk_size": 512
64
  }
65
  },
@@ -70,7 +70,7 @@ RUNTIME_CONFIGS = {
70
  "chunk_size": 256
71
  },
72
  "local_mps": {
73
- "max_token_length": 1000,
74
  "chunk_size": 128
75
  }
76
  },
 
59
  },
60
  # 本地 Apple Silicon(保守)
61
  "local_mps": {
62
+ "max_token_length": 2000,
63
  "chunk_size": 512
64
  }
65
  },
 
70
  "chunk_size": 256
71
  },
72
  "local_mps": {
73
+ "max_token_length": 2000,
74
  "chunk_size": 128
75
  }
76
  },
client/src/ts/compare.ts CHANGED
@@ -110,6 +110,7 @@ type DemoColumnData = {
110
  lmfInstance?: GLTR_Text_Box; // LMF实例引用(对比模式下使用)
111
  histograms: {
112
  stats_frac: Histogram | null;
 
113
  stats_surprisal_progress: ScatterPlot | null;
114
  };
115
  };
@@ -218,6 +219,7 @@ window.onload = () => {
218
  const metricsId = `text_metrics_${safeId}`;
219
  const errorId = `error_${safeId}`;
220
  const statsFracId = `stats_frac_${safeId}`;
 
221
  const statsProgressId = `stats_surprisal_progress_${safeId}`;
222
  const textRenderId = `text_render_${safeId}`;
223
 
@@ -251,6 +253,10 @@ window.onload = () => {
251
  <div>token surprisal histogram</div>
252
  <svg id="${statsFracId}"></svg>
253
  </div>
 
 
 
 
254
  <div style="display:block;text-align: center;margin-bottom: 20px;">
255
  <div>surprisal vs token progress</div>
256
  <svg id="${statsProgressId}"></svg>
@@ -309,7 +315,7 @@ window.onload = () => {
309
 
310
  // 为单个列渲染统计图表(使用ID)
311
  const renderStatsForColumn = (id: string, columnData: DemoColumnData) => {
312
- if (!columnData.stats || !columnData.histograms.stats_frac || !columnData.histograms.stats_surprisal_progress) {
313
  return;
314
  }
315
 
@@ -317,7 +323,7 @@ window.onload = () => {
317
  const isDiffColumn = compareMode && columnData.diffStats && !isBaseColumn(id);
318
  const safeId = toSafeId(id);
319
 
320
- // 更新 token surprisal histogram(保持不变)
321
  columnData.histograms.stats_frac.update({
322
  data: stats.tokenSurprisals,
323
  label: "surprisal",
@@ -328,6 +334,56 @@ window.onload = () => {
328
  averageLabel: 'bits/token'
329
  });
330
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
331
  // 更新 surprisal progress scatter plot(保持不变)
332
  if (stats.tokenSurprisals && stats.tokenSurprisals.length > 0) {
333
  columnData.histograms.stats_surprisal_progress.update({
@@ -511,6 +567,7 @@ window.onload = () => {
511
  const initializeColumnVisualizations = (id: string, columnData: DemoColumnData): void => {
512
  const safeId = toSafeId(id);
513
  const statsFracId = `#stats_frac_${safeId}`;
 
514
  const statsProgressId = `#stats_surprisal_progress_${safeId}`;
515
 
516
  // 创建 Histogram 实例
@@ -520,6 +577,12 @@ window.onload = () => {
520
  { width: 400, height: 200 }
521
  );
522
 
 
 
 
 
 
 
523
  // 创建 ScatterPlot 实例
524
  columnData.histograms.stats_surprisal_progress = new ScatterPlot(
525
  d3.select(statsProgressId),
@@ -579,13 +642,19 @@ window.onload = () => {
579
  };
580
 
581
  // 根据 histogram source 解析出列的 safeId 和直方图类型
582
- const parseHistogramSource = (source?: string): { safeId: string; histogramType: 'token' } | null => {
583
  if (!source) {
584
  return null;
585
  }
586
 
 
587
  const tokenPrefix = 'stats_frac';
588
 
 
 
 
 
 
589
  if (source.startsWith(tokenPrefix)) {
590
  const safeId = source.substring(tokenPrefix.length).replace(/^_/, '');
591
  return safeId ? { safeId, histogramType: 'token' } : null;
@@ -628,7 +697,7 @@ window.onload = () => {
628
  return;
629
  }
630
 
631
- const { stats_frac } = columnData.histograms;
632
 
633
  let enhancedResult = columnData.enhancedResult;
634
  if (!enhancedResult && columnData.data) {
@@ -643,12 +712,17 @@ window.onload = () => {
643
  // binIndex 为 -1 表示取消高亮
644
  if (ev.binIndex === -1) {
645
  stats_frac?.clearSelection();
 
646
  columnData.lmfInstance.clearHighlight();
647
  return;
648
  }
649
 
650
- // 只有 token 直方图
651
- stats_frac?.clearSelection();
 
 
 
 
652
 
653
  // 使用通用的高亮计算函数
654
  const { x0, x1 } = ev;
@@ -942,6 +1016,7 @@ window.onload = () => {
942
  lmfInstance: undefined,
943
  histograms: {
944
  stats_frac: null,
 
945
  stats_surprisal_progress: null
946
  }
947
  };
@@ -1278,13 +1353,23 @@ window.onload = () => {
1278
  if (compareMode) {
1279
  recalculateAllDiffStats();
1280
 
1281
- // 重新渲染所有列的统计图表和指标
1282
  columnsData.forEach((columnData, id) => {
1283
  if (columnData.stats) {
1284
  const resultModel = columnData.data?.request?.model || null;
1285
  updateMetricsForColumn(id, columnData.stats, resultModel);
1286
  renderStatsForColumn(id, columnData);
1287
  }
 
 
 
 
 
 
 
 
 
 
1288
  });
1289
  }
1290
  };
 
110
  lmfInstance?: GLTR_Text_Box; // LMF实例引用(对比模式下使用)
111
  histograms: {
112
  stats_frac: Histogram | null;
113
+ stats_char_frac: Histogram | null;
114
  stats_surprisal_progress: ScatterPlot | null;
115
  };
116
  };
 
219
  const metricsId = `text_metrics_${safeId}`;
220
  const errorId = `error_${safeId}`;
221
  const statsFracId = `stats_frac_${safeId}`;
222
+ const statsCharFracId = `stats_char_frac_${safeId}`;
223
  const statsProgressId = `stats_surprisal_progress_${safeId}`;
224
  const textRenderId = `text_render_${safeId}`;
225
 
 
253
  <div>token surprisal histogram</div>
254
  <svg id="${statsFracId}"></svg>
255
  </div>
256
+ <div style="display:block;text-align: center;margin-bottom: 20px;">
257
+ <div id="char_histogram_title_${safeId}">char surprisal histogram</div>
258
+ <svg id="${statsCharFracId}"></svg>
259
+ </div>
260
  <div style="display:block;text-align: center;margin-bottom: 20px;">
261
  <div>surprisal vs token progress</div>
262
  <svg id="${statsProgressId}"></svg>
 
315
 
316
  // 为单个列渲染统计图表(使用ID)
317
  const renderStatsForColumn = (id: string, columnData: DemoColumnData) => {
318
+ if (!columnData.stats || !columnData.histograms.stats_frac || !columnData.histograms.stats_char_frac || !columnData.histograms.stats_surprisal_progress) {
319
  return;
320
  }
321
 
 
323
  const isDiffColumn = compareMode && columnData.diffStats && !isBaseColumn(id);
324
  const safeId = toSafeId(id);
325
 
326
+ // 更新 token surprisal histogram(保持不变,不显示差分
327
  columnData.histograms.stats_frac.update({
328
  data: stats.tokenSurprisals,
329
  label: "surprisal",
 
334
  averageLabel: 'bits/token'
335
  });
336
 
337
+ // 更新 char surprisal histogram(Diff列显示差分)
338
+ if (isDiffColumn && columnData.diffStats) {
339
+ // Diff列:显示Δchar surprisal histogram
340
+ const deltaCharSurprisals = columnData.diffStats.deltaCharSurprisals;
341
+
342
+ // 使用统一的差分颜色配置
343
+ const deltaColorScale = createDiffColorScale();
344
+ const D = DIFF_CONFIG.THRESHOLD;
345
+
346
+ // 计算平均差分
347
+ const deltaAverage = deltaCharSurprisals.length > 0
348
+ ? deltaCharSurprisals.reduce((sum, val) => sum + val, 0) / deltaCharSurprisals.length
349
+ : 0;
350
+
351
+ const label = "Δchar surprisal histogram";
352
+ columnData.histograms.stats_char_frac.update({
353
+ data: deltaCharSurprisals,
354
+ label: label,
355
+ no_bins: 20,
356
+ extent: [-D, D],
357
+ colorScale: deltaColorScale,
358
+ averageValue: deltaAverage,
359
+ averageLabel: 'Δ bits/char'
360
+ });
361
+
362
+ // 更新标题文本
363
+ const titleElement = document.getElementById(`char_histogram_title_${safeId}`);
364
+ if (titleElement) {
365
+ titleElement.textContent = label;
366
+ }
367
+ } else {
368
+ // Base列或非对比模式:显示原始char surprisal histogram
369
+ const label = "char surprisal histogram";
370
+ columnData.histograms.stats_char_frac.update({
371
+ data: stats.charSurprisals,
372
+ label: label,
373
+ no_bins: 20,
374
+ extent: [0, 20],
375
+ colorScale: surprisalColorScale,
376
+ averageValue: stats.charAverage ?? undefined,
377
+ averageLabel: 'bits/char'
378
+ });
379
+
380
+ // 更新标题文本
381
+ const titleElement = document.getElementById(`char_histogram_title_${safeId}`);
382
+ if (titleElement) {
383
+ titleElement.textContent = label;
384
+ }
385
+ }
386
+
387
  // 更新 surprisal progress scatter plot(保持不变)
388
  if (stats.tokenSurprisals && stats.tokenSurprisals.length > 0) {
389
  columnData.histograms.stats_surprisal_progress.update({
 
567
  const initializeColumnVisualizations = (id: string, columnData: DemoColumnData): void => {
568
  const safeId = toSafeId(id);
569
  const statsFracId = `#stats_frac_${safeId}`;
570
+ const statsCharFracId = `#stats_char_frac_${safeId}`;
571
  const statsProgressId = `#stats_surprisal_progress_${safeId}`;
572
 
573
  // 创建 Histogram 实例
 
577
  { width: 400, height: 200 }
578
  );
579
 
580
+ columnData.histograms.stats_char_frac = new Histogram(
581
+ d3.select(statsCharFracId),
582
+ eventHandler,
583
+ { width: 400, height: 200 }
584
+ );
585
+
586
  // 创建 ScatterPlot 实例
587
  columnData.histograms.stats_surprisal_progress = new ScatterPlot(
588
  d3.select(statsProgressId),
 
642
  };
643
 
644
  // 根据 histogram source 解析出列的 safeId 和直方图类型
645
+ const parseHistogramSource = (source?: string): { safeId: string; histogramType: 'token' | 'char' } | null => {
646
  if (!source) {
647
  return null;
648
  }
649
 
650
+ const charPrefix = 'stats_char_frac';
651
  const tokenPrefix = 'stats_frac';
652
 
653
+ if (source.startsWith(charPrefix)) {
654
+ const safeId = source.substring(charPrefix.length).replace(/^_/, '');
655
+ return safeId ? { safeId, histogramType: 'char' } : null;
656
+ }
657
+
658
  if (source.startsWith(tokenPrefix)) {
659
  const safeId = source.substring(tokenPrefix.length).replace(/^_/, '');
660
  return safeId ? { safeId, histogramType: 'token' } : null;
 
697
  return;
698
  }
699
 
700
+ const { stats_frac, stats_char_frac } = columnData.histograms;
701
 
702
  let enhancedResult = columnData.enhancedResult;
703
  if (!enhancedResult && columnData.data) {
 
712
  // binIndex 为 -1 表示取消高亮
713
  if (ev.binIndex === -1) {
714
  stats_frac?.clearSelection();
715
+ stats_char_frac?.clearSelection();
716
  columnData.lmfInstance.clearHighlight();
717
  return;
718
  }
719
 
720
+ // 同一列内仅保持一个直方图的选中状态
721
+ if (parsed.histogramType === 'char') {
722
+ stats_frac?.clearSelection();
723
+ } else {
724
+ stats_char_frac?.clearSelection();
725
+ }
726
 
727
  // 使用通用的高亮计算函数
728
  const { x0, x1 } = ev;
 
1016
  lmfInstance: undefined,
1017
  histograms: {
1018
  stats_frac: null,
1019
+ stats_char_frac: null,
1020
  stats_surprisal_progress: null
1021
  }
1022
  };
 
1353
  if (compareMode) {
1354
  recalculateAllDiffStats();
1355
 
1356
+ // 重新渲染所有列的统计图表和指标,并更新 LMF 实例的差分模式
1357
  columnsData.forEach((columnData, id) => {
1358
  if (columnData.stats) {
1359
  const resultModel = columnData.data?.request?.model || null;
1360
  updateMetricsForColumn(id, columnData.stats, resultModel);
1361
  renderStatsForColumn(id, columnData);
1362
  }
1363
+
1364
+ // 更新 LMF 实例的差分模式(如果存在)
1365
+ if (columnData.lmfInstance) {
1366
+ const isDiffColumn = columnData.diffStats && !isBaseColumn(id);
1367
+ if (isDiffColumn && columnData.diffStats) {
1368
+ columnData.lmfInstance.setDiffMode(true, columnData.diffStats.deltaCharSurprisals);
1369
+ } else {
1370
+ columnData.lmfInstance.setDiffMode(false, []);
1371
+ }
1372
+ }
1373
  });
1374
  }
1375
  };
client/src/ts/controllers/serverDemoController.ts CHANGED
@@ -107,6 +107,10 @@ export type ServerDemoSaveOptions = {
107
  * 如果不提供,将创建新的存储实例
108
  */
109
  serverStorage?: IDemoStorage;
 
 
 
 
110
  onSaveStart: () => void;
111
  onSaveSuccess: (name: string) => void;
112
  onSaveError: (error: Error) => void;
@@ -128,6 +132,7 @@ export const handleServerDemoSave = async (options: ServerDemoSaveOptions): Prom
128
  presetSaveInfo = null,
129
  showSuccessToast = true,
130
  serverStorage: providedStorage,
 
131
  onSaveStart,
132
  onSaveSuccess,
133
  onSaveError,
@@ -152,7 +157,7 @@ export const handleServerDemoSave = async (options: ServerDemoSaveOptions): Prom
152
  localStorage.setItem(LAST_SAVE_PATH_KEY, normalizedPath);
153
  }
154
  } else {
155
- const defaultName = getDefaultDemoName(currentData, textFieldValue);
156
  result = await showDemoNameInput(api, defaultName);
157
  }
158
 
 
107
  * 如果不提供,将创建新的存储实例
108
  */
109
  serverStorage?: IDemoStorage;
110
+ /**
111
+ * 当前文件名(如果有,将作为默认文件名)
112
+ */
113
+ currentFileName?: string | null;
114
  onSaveStart: () => void;
115
  onSaveSuccess: (name: string) => void;
116
  onSaveError: (error: Error) => void;
 
132
  presetSaveInfo = null,
133
  showSuccessToast = true,
134
  serverStorage: providedStorage,
135
+ currentFileName = null,
136
  onSaveStart,
137
  onSaveSuccess,
138
  onSaveError,
 
157
  localStorage.setItem(LAST_SAVE_PATH_KEY, normalizedPath);
158
  }
159
  } else {
160
+ const defaultName = getDefaultDemoName(currentData, textFieldValue, currentFileName);
161
  result = await showDemoNameInput(api, defaultName);
162
  }
163
 
client/src/ts/start.ts CHANGED
@@ -270,14 +270,9 @@ window.onload = () => {
270
  currentFilename?: string,
271
  textValue?: string
272
  ): Promise<void> => {
273
- // 生成文件名:如果已经有本地文件名,使用它;否则使用统一的文件名生成函数
274
- let filename: string;
275
- if (currentFilename && currentFilename !== '未选择文件') {
276
- filename = currentFilename;
277
- } else {
278
- const defaultName = getDefaultDemoName(data, textValue || '');
279
- filename = ensureJsonExtension(defaultName);
280
- }
281
 
282
  appStateManager.setGlobalLoading(true);
283
  appStateManager.updateState({ isSaving: true });
@@ -387,9 +382,9 @@ window.onload = () => {
387
  containerSelector: '.demos',
388
  loaderSelector: '#demos_loading',
389
  refreshSelector: '#refresh_demo_btn',
390
- onDemoLoaded: (data, disableAnimation, isNewDemo = false) => {
391
- // 使用统一渲染函数
392
- demoBusinessLogic.renderDemo(data, 'server', undefined, { disableAnimation, isNewDemo });
393
  },
394
  onTextPrefill: (text) => {
395
  textInputController.setTextValue(text);
@@ -408,42 +403,47 @@ window.onload = () => {
408
  hasProcessedUrlDemo = true;
409
  const urlDemoPath = URLHandler.parameters['demo'];
410
  if (urlDemoPath && typeof urlDemoPath === 'string') {
411
- // 使用统一的资源加载器处理所有 URL 恢复
412
  appStateManager.setGlobalLoading(true);
413
  try {
414
- const result = await demoResourceLoader.load(urlDemoPath);
415
-
416
- if (result.success && result.data) {
417
- // 判断资源类型并渲染
418
- if (DemoResourceLoader.isLocalResource(urlDemoPath)) {
419
- // 本地资源:从 URL 提取文件名和哈希
420
  try {
421
  const localInfo = DemoResourceLoader.extractLocalInfo(urlDemoPath);
422
- demoBusinessLogic.renderDemo(result.data, 'local', localInfo.filename, { disableAnimation: true, isNewDemo: true });
423
- showToast(`已从缓存恢复: ${localInfo.filename}`, 'success');
 
 
424
  } catch (error) {
425
- // URL 格式无效(缺少哈希或格式错误)
426
  const errorMessage = extractErrorMessage(error, 'URL格式无效');
427
  console.error('解析本地资源标识符失败:', error);
428
  handleLoadFailure(urlDemoPath, errorMessage);
429
  }
430
  } else {
431
- // 服务器资源
432
- demoBusinessLogic.renderDemo(result.data, 'server', undefined, { disableAnimation: true, isNewDemo: true });
433
- // 高亮对应的 demo
 
 
 
 
 
 
 
 
434
  if (demoManager) {
435
- demoManager.highlightDemo(urlDemoPath);
436
  }
 
 
437
  }
438
- } else {
439
- // 加载失败,统一处理
440
- console.warn('加载失败:', result.message);
441
- handleLoadFailure(urlDemoPath, result.message || '加载失败');
442
  }
443
  } catch (error) {
444
- console.error('加载失败:', error);
445
- const message = extractErrorMessage(error, '加载失败');
446
- handleLoadFailure(urlDemoPath, message);
447
  } finally {
448
  appStateManager.setGlobalLoading(false);
449
  }
@@ -591,6 +591,7 @@ window.onload = () => {
591
  // Save按钮点击事件(使用 serverDemoController)
592
  saveBtn.on('click', async () => {
593
  try {
 
594
  await handleServerDemoSave({
595
  api,
596
  currentData: visualizationUpdater.getCurrentData(),
@@ -599,6 +600,7 @@ window.onload = () => {
599
  enableDemo: current.demo,
600
  demoManager: demoManager || null,
601
  serverStorage,
 
602
  onSaveStart: () => {
603
  appStateManager.updateState({ isSaving: true });
604
  },
@@ -627,9 +629,12 @@ window.onload = () => {
627
  return;
628
  }
629
 
 
 
 
630
  await handleLocalDemoSave(
631
  rawApiResponse,
632
- openLocalFilename.text(),
633
  textInputController.getTextValue()
634
  );
635
  });
 
270
  currentFilename?: string,
271
  textValue?: string
272
  ): Promise<void> => {
273
+ // 生成文件名:使用统一的文件名生成函数(会自动处理现有文件名)
274
+ const defaultName = getDefaultDemoName(data, textValue || '', currentFilename);
275
+ const filename = ensureJsonExtension(defaultName);
 
 
 
 
 
276
 
277
  appStateManager.setGlobalLoading(true);
278
  appStateManager.updateState({ isSaving: true });
 
382
  containerSelector: '.demos',
383
  loaderSelector: '#demos_loading',
384
  refreshSelector: '#refresh_demo_btn',
385
+ onDemoLoaded: (data, disableAnimation, isNewDemo = false, path?: string) => {
386
+ // 使用统一渲染函数,传递路径以便提取文件名
387
+ demoBusinessLogic.renderDemo(data, 'server', path, { disableAnimation, isNewDemo });
388
  },
389
  onTextPrefill: (text) => {
390
  textInputController.setTextValue(text);
 
403
  hasProcessedUrlDemo = true;
404
  const urlDemoPath = URLHandler.parameters['demo'];
405
  if (urlDemoPath && typeof urlDemoPath === 'string') {
 
406
  appStateManager.setGlobalLoading(true);
407
  try {
408
+ // 判断资源类型
409
+ if (DemoResourceLoader.isLocalResource(urlDemoPath)) {
410
+ // 本地资源:加载并渲染(不需要导航)
411
+ const result = await demoResourceLoader.load(urlDemoPath);
412
+ if (result.success && result.data) {
 
413
  try {
414
  const localInfo = DemoResourceLoader.extractLocalInfo(urlDemoPath);
415
+ demoBusinessLogic.renderDemo(result.data, 'local', localInfo.filename, {
416
+ disableAnimation: true,
417
+ isNewDemo: true
418
+ });
419
  } catch (error) {
 
420
  const errorMessage = extractErrorMessage(error, 'URL格式无效');
421
  console.error('解析本地资源标识符失败:', error);
422
  handleLoadFailure(urlDemoPath, errorMessage);
423
  }
424
  } else {
425
+ handleLoadFailure(urlDemoPath, result.message || '加载失败');
426
+ }
427
+ } else {
428
+ // 服务器资源:统一使用 DemoResourceLoader 加载,然后导航并高亮
429
+ const result = await demoResourceLoader.load(urlDemoPath);
430
+ if (result.success && result.data) {
431
+ demoBusinessLogic.renderDemo(result.data, 'server', urlDemoPath, {
432
+ disableAnimation: true,
433
+ isNewDemo: true
434
+ });
435
+ // 导航到demo所在文件夹并高亮
436
  if (demoManager) {
437
+ await demoManager.navigateToDemoAndHighlight(urlDemoPath);
438
  }
439
+ } else {
440
+ handleLoadFailure(urlDemoPath, result.message || '加载失败');
441
  }
 
 
 
 
442
  }
443
  } catch (error) {
444
+ const errorMessage = extractErrorMessage(error, '恢复失败');
445
+ console.error('从URL恢复demo失败:', error);
446
+ handleLoadFailure(urlDemoPath, errorMessage);
447
  } finally {
448
  appStateManager.setGlobalLoading(false);
449
  }
 
591
  // Save按钮点击事件(使用 serverDemoController)
592
  saveBtn.on('click', async () => {
593
  try {
594
+ const state = appStateManager.getState();
595
  await handleServerDemoSave({
596
  api,
597
  currentData: visualizationUpdater.getCurrentData(),
 
600
  enableDemo: current.demo,
601
  demoManager: demoManager || null,
602
  serverStorage,
603
+ currentFileName: state.currentFileName,
604
  onSaveStart: () => {
605
  appStateManager.updateState({ isSaving: true });
606
  },
 
629
  return;
630
  }
631
 
632
+ // 使用 AppState 中的文件名(单一真相来源)
633
+ const state = appStateManager.getState();
634
+
635
  await handleLocalDemoSave(
636
  rawApiResponse,
637
+ state.currentFileName || undefined,
638
  textInputController.getTextValue()
639
  );
640
  });
client/src/ts/storage/demoResourceLoader.ts CHANGED
@@ -19,8 +19,8 @@ export type ResourceIdentifier = string; // "/path/file.json" 或 "local://file.
19
 
20
  /**
21
  * 解析资源标识符
22
- * 本地资源格式: local://filename.json#hash
23
- * 使用最后一个 # 作为分隔符,解决文件名中包含 # 的冲突
24
  */
25
  function parseResourceIdentifier(identifier: ResourceIdentifier): {
26
  type: 'server' | 'local';
@@ -30,8 +30,8 @@ function parseResourceIdentifier(identifier: ResourceIdentifier): {
30
  } {
31
  if (identifier.startsWith('local://')) {
32
  const rest = identifier.substring('local://'.length);
33
- // 使用最后一个 # 作为分隔符,解决文件名中包含 # 的冲突
34
- const hashIndex = rest.lastIndexOf('#');
35
 
36
  if (hashIndex >= 0) {
37
  const filename = rest.substring(0, hashIndex);
@@ -51,7 +51,7 @@ function parseResourceIdentifier(identifier: ResourceIdentifier): {
51
  }
52
 
53
  // 没有 hash,视为无效(不再兼容)
54
- throw new Error(`本地资源标识符缺少哈希值: "${identifier}",格式应为 local://filename.json#hash`);
55
  }
56
 
57
  return {
@@ -75,7 +75,7 @@ export class DemoResourceLoader {
75
 
76
  /**
77
  * 加载资源(统一入口,包含验证)
78
- * @param identifier 资源标识符("/path/file.json" 或 "local://file.json#hash")
79
  */
80
  async load(identifier: ResourceIdentifier): Promise<LoadResult> {
81
  const { type, path, hash, filename } = parseResourceIdentifier(identifier);
@@ -84,10 +84,10 @@ export class DemoResourceLoader {
84
  ? this.localDemoCache
85
  : this.serverStorage;
86
 
87
- // 对于本地资源,构造完整的 key: filename#hash
88
  // 如果解析失败(缺少 hash 或格式无效),parseResourceIdentifier 会抛出错误
89
  const loadKey = type === 'local' && filename && hash
90
- ? `${filename}#${hash}`
91
  : path;
92
 
93
  // 调用底层存储加载数据
@@ -138,7 +138,7 @@ export class DemoResourceLoader {
138
  */
139
  static createLocalIdentifier(filename: string, hash: string): ResourceIdentifier {
140
  const name = ensureJsonExtension(filename);
141
- return `local://${name}#${hash}`;
142
  }
143
 
144
  /**
 
19
 
20
  /**
21
  * 解析资源标识符
22
+ * 本地资源格式: local://filename.json~hash
23
+ * 使用最后一个 ~ 作为分隔符,解决文件名中包含 ~ 的冲突
24
  */
25
  function parseResourceIdentifier(identifier: ResourceIdentifier): {
26
  type: 'server' | 'local';
 
30
  } {
31
  if (identifier.startsWith('local://')) {
32
  const rest = identifier.substring('local://'.length);
33
+ // 使用最后一个 ~ 作为分隔符,解决文件名中包含 ~ 的冲突
34
+ const hashIndex = rest.lastIndexOf('~');
35
 
36
  if (hashIndex >= 0) {
37
  const filename = rest.substring(0, hashIndex);
 
51
  }
52
 
53
  // 没有 hash,视为无效(不再兼容)
54
+ throw new Error(`本地资源标识符缺少哈希值: "${identifier}",格式应为 local://filename.json~hash`);
55
  }
56
 
57
  return {
 
75
 
76
  /**
77
  * 加载资源(统一入口,包含验证)
78
+ * @param identifier 资源标识符("/path/file.json" 或 "local://file.json~hash")
79
  */
80
  async load(identifier: ResourceIdentifier): Promise<LoadResult> {
81
  const { type, path, hash, filename } = parseResourceIdentifier(identifier);
 
84
  ? this.localDemoCache
85
  : this.serverStorage;
86
 
87
+ // 对于本地资源,构造完整的 key: filename~hash
88
  // 如果解析失败(缺少 hash 或格式无效),parseResourceIdentifier 会抛出错误
89
  const loadKey = type === 'local' && filename && hash
90
+ ? `${filename}~${hash}`
91
  : path;
92
 
93
  // 调用底层存储加载数据
 
138
  */
139
  static createLocalIdentifier(filename: string, hash: string): ResourceIdentifier {
140
  const name = ensureJsonExtension(filename);
141
+ return `local://${name}~${hash}`;
142
  }
143
 
144
  /**
client/src/ts/storage/localDemoCache.ts CHANGED
@@ -82,7 +82,7 @@ export class LocalDemoCache implements IDemoStorage {
82
  const store = transaction.objectStore(STORE_NAME);
83
 
84
  const filename = ensureJsonExtension(options.name);
85
- const key = `${filename}#${hash}`; // 使用 filename#hash 作为 key
86
 
87
  const record = {
88
  key,
@@ -138,7 +138,7 @@ export class LocalDemoCache implements IDemoStorage {
138
 
139
  /**
140
  * 从缓存加载 demo
141
- * @param key 完整的 key,格式为 "filename#hash"
142
  */
143
  async load(key?: string): Promise<LoadResult> {
144
  if (!key) {
@@ -189,7 +189,7 @@ export class LocalDemoCache implements IDemoStorage {
189
 
190
  /**
191
  * 删除指定的 demo
192
- * @param key 完整的 key,格式为 "filename#hash"
193
  */
194
  async delete(key: string): Promise<boolean> {
195
  try {
 
82
  const store = transaction.objectStore(STORE_NAME);
83
 
84
  const filename = ensureJsonExtension(options.name);
85
+ const key = `${filename}~${hash}`; // 使用 filename~hash 作为 key
86
 
87
  const record = {
88
  key,
 
138
 
139
  /**
140
  * 从缓存加载 demo
141
+ * @param key 完整的 key,格式为 "filename~hash"
142
  */
143
  async load(key?: string): Promise<LoadResult> {
144
  if (!key) {
 
189
 
190
  /**
191
  * 删除指定的 demo
192
+ * @param key 完整的 key,格式为 "filename~hash"
193
  */
194
  async delete(key: string): Promise<boolean> {
195
  try {
client/src/ts/ui/demoManager.ts CHANGED
@@ -18,7 +18,7 @@ export type DemoManagerOptions = {
18
  containerSelector: string;
19
  loaderSelector: string;
20
  refreshSelector: string;
21
- onDemoLoaded: (data: AnalyzeResponse, disableAnimation: boolean, isNewDemo?: boolean) => void;
22
  onTextPrefill?: (text: string) => void;
23
  onDemoLoading?: (loading: boolean) => void;
24
  onRefreshStart?: () => void;
@@ -32,6 +32,7 @@ export type DemoManagerOptions = {
32
  export type DemoManager = {
33
  refresh: () => Promise<void>;
34
  highlightDemo: (fullPath: string | null) => void;
 
35
  loadDemoByPath: (fullPath: string) => Promise<boolean>;
36
  getSelectedPaths: () => string[]; // 获取选中的demo路径
37
  };
@@ -71,6 +72,7 @@ export function initDemoManager(options: DemoManagerOptions): DemoManager {
71
  return {
72
  refresh: () => Promise.resolve(),
73
  highlightDemo: () => {},
 
74
  loadDemoByPath: () => Promise.resolve(false),
75
  getSelectedPaths: () => [],
76
  };
@@ -216,6 +218,44 @@ export function initDemoManager(options: DemoManagerOptions): DemoManager {
216
  applyActiveState();
217
  };
218
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
219
  const setActiveDemo = (fullPath: string | null) => {
220
  highlightDemo(fullPath);
221
 
@@ -542,11 +582,11 @@ export function initDemoManager(options: DemoManagerOptions): DemoManager {
542
  }
543
 
544
  onTextPrefill?.(data.request.text);
545
- onDemoLoaded(data, true, isNewDemo);
546
  setActiveDemo(demoPath);
547
  };
548
 
549
- // 根据完整路径加载demo(用于URL参数自动加载)
550
  const loadDemoByPath = async (fullPath: string): Promise<boolean> => {
551
  const normalizedPath = normalizeFullPath(fullPath);
552
  if (!normalizedPath) {
@@ -554,37 +594,9 @@ export function initDemoManager(options: DemoManagerOptions): DemoManager {
554
  }
555
 
556
  try {
557
- // 从完整路径中提取文件夹路径和文件名
558
- // 例如:/文件夹名/文件名.json -> 文件夹路径: /文件夹名, 文件名: 文件名.json
559
- // 例如:/文件名.json -> 文件夹路径: /, 文件名: 文件名.json
560
- const pathParts = normalizedPath.split('/').filter(p => p);
561
- let targetFolderPath = '/';
562
- let targetFileName = '';
563
-
564
- if (pathParts.length === 0) {
565
- return false;
566
- }
567
-
568
- // 最后一个部分是文件名(可能包含.json)
569
- targetFileName = pathParts[pathParts.length - 1];
570
- // 如果文件名没有.json后缀,添加它
571
- if (!targetFileName.endsWith('.json')) {
572
- targetFileName += '.json';
573
- }
574
-
575
- // 前面的部分是文件夹路径
576
- if (pathParts.length > 1) {
577
- targetFolderPath = '/' + pathParts.slice(0, -1).join('/');
578
- }
579
-
580
- // 如果当前路径不匹配,先切换到正确的路径并加载列表
581
- if (currentPath !== targetFolderPath) {
582
- currentPath = targetFolderPath;
583
- if (pathNavigator) {
584
- pathNavigator.update(targetFolderPath);
585
- }
586
- await fetchDemoList();
587
- }
588
 
589
  // 在当前列表中查找匹配的文件
590
  const result = await api.list_demos(currentPath);
@@ -629,6 +641,7 @@ export function initDemoManager(options: DemoManagerOptions): DemoManager {
629
  return {
630
  refresh: fetchDemoList,
631
  highlightDemo: highlightDemo,
 
632
  loadDemoByPath: loadDemoByPath,
633
  getSelectedPaths: () => multiSelect ? multiSelect.getSelectedPaths() : [],
634
  };
 
18
  containerSelector: string;
19
  loaderSelector: string;
20
  refreshSelector: string;
21
+ onDemoLoaded: (data: AnalyzeResponse, disableAnimation: boolean, isNewDemo?: boolean, path?: string) => void;
22
  onTextPrefill?: (text: string) => void;
23
  onDemoLoading?: (loading: boolean) => void;
24
  onRefreshStart?: () => void;
 
32
  export type DemoManager = {
33
  refresh: () => Promise<void>;
34
  highlightDemo: (fullPath: string | null) => void;
35
+ navigateToDemoAndHighlight: (fullPath: string) => Promise<void>; // 导航到demo所在文件夹并高亮(不加载数据)
36
  loadDemoByPath: (fullPath: string) => Promise<boolean>;
37
  getSelectedPaths: () => string[]; // 获取选中的demo路径
38
  };
 
72
  return {
73
  refresh: () => Promise.resolve(),
74
  highlightDemo: () => {},
75
+ navigateToDemoAndHighlight: () => Promise.resolve(),
76
  loadDemoByPath: () => Promise.resolve(false),
77
  getSelectedPaths: () => [],
78
  };
 
218
  applyActiveState();
219
  };
220
 
221
+ // ============ 辅助函数:路径提取和导航 ============
222
+
223
+ // 从完整路径中提取文件夹路径
224
+ const extractFolderPath = (fullPath: string): string => {
225
+ const pathParts = fullPath.split('/').filter(p => p);
226
+ if (pathParts.length <= 1) {
227
+ return '/';
228
+ }
229
+ return '/' + pathParts.slice(0, -1).join('/');
230
+ };
231
+
232
+ // 导航到指定文件夹并刷新列表(如果当前不在该文件夹)
233
+ const navigateToFolder = async (targetFolderPath: string): Promise<void> => {
234
+ if (currentPath !== targetFolderPath) {
235
+ currentPath = targetFolderPath;
236
+ if (pathNavigator) {
237
+ pathNavigator.update(targetFolderPath);
238
+ }
239
+ await fetchDemoList();
240
+ }
241
+ };
242
+
243
+ // 导航到demo所在文件夹并高亮(不加载数据,用于URL恢复等场景)
244
+ const navigateToDemoAndHighlight = async (fullPath: string): Promise<void> => {
245
+ const normalizedPath = normalizeFullPath(fullPath);
246
+ if (!normalizedPath) {
247
+ return;
248
+ }
249
+
250
+ try {
251
+ const targetFolderPath = extractFolderPath(normalizedPath);
252
+ await navigateToFolder(targetFolderPath);
253
+ highlightDemo(normalizedPath);
254
+ } catch (error) {
255
+ console.error('导航到demo失败:', error);
256
+ }
257
+ };
258
+
259
  const setActiveDemo = (fullPath: string | null) => {
260
  highlightDemo(fullPath);
261
 
 
582
  }
583
 
584
  onTextPrefill?.(data.request.text);
585
+ onDemoLoaded(data, true, isNewDemo, demoPath);
586
  setActiveDemo(demoPath);
587
  };
588
 
589
+ // 根据完整路径加载demo(用于保存后重新加载等场景
590
  const loadDemoByPath = async (fullPath: string): Promise<boolean> => {
591
  const normalizedPath = normalizeFullPath(fullPath);
592
  if (!normalizedPath) {
 
594
  }
595
 
596
  try {
597
+ // 使用辅助函数:导航到文件夹
598
+ const targetFolderPath = extractFolderPath(normalizedPath);
599
+ await navigateToFolder(targetFolderPath);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
600
 
601
  // 在当前列表中查找匹配的文件
602
  const result = await api.list_demos(currentPath);
 
641
  return {
642
  refresh: fetchDemoList,
643
  highlightDemo: highlightDemo,
644
+ navigateToDemoAndHighlight: navigateToDemoAndHighlight,
645
  loadDemoByPath: loadDemoByPath,
646
  getSelectedPaths: () => multiSelect ? multiSelect.getSelectedPaths() : [],
647
  };
client/src/ts/utils/analyzeFlow.ts CHANGED
@@ -104,11 +104,12 @@ export class AnalyzeFlowManager {
104
  // 分析前滚动到顶部
105
  this.scrollToTop();
106
 
107
- // 重置为新分析状态(数据来源为null,保存标志为false)
108
  this.deps.appStateManager.updateState({
109
  dataSource: null,
110
  isSavedToLocal: false,
111
- isSavedToServer: false
 
112
  });
113
 
114
  this.deps.appStateManager.setIsAnalyzing(true);
 
104
  // 分析前滚动到顶部
105
  this.scrollToTop();
106
 
107
+ // 重置为新分析状态(数据来源为null,保存标志为false,清除文件名
108
  this.deps.appStateManager.updateState({
109
  dataSource: null,
110
  isSavedToLocal: false,
111
+ isSavedToServer: false,
112
+ currentFileName: null
113
  });
114
 
115
  this.deps.appStateManager.setIsAnalyzing(true);
client/src/ts/utils/appStateManager.ts CHANGED
@@ -17,6 +17,7 @@ export interface AppState {
17
  dataSource: 'local' | 'server' | null; // 数据来源:null表示新分析的内容
18
  isSavedToLocal: boolean; // 是否已保存到本地
19
  isSavedToServer: boolean; // 是否已保存到服务器
 
20
  }
21
 
22
  /**
@@ -45,7 +46,8 @@ export class AppStateManager {
45
  hasValidData: false,
46
  dataSource: null,
47
  isSavedToLocal: false,
48
- isSavedToServer: false
 
49
  };
50
  this.deps = deps;
51
  }
 
17
  dataSource: 'local' | 'server' | null; // 数据来源:null表示新分析的内容
18
  isSavedToLocal: boolean; // 是否已保存到本地
19
  isSavedToServer: boolean; // 是否已保存到服务器
20
+ currentFileName: string | null; // 当前文件名(本地或服务器)
21
  }
22
 
23
  /**
 
46
  hasValidData: false,
47
  dataSource: null,
48
  isSavedToLocal: false,
49
+ isSavedToServer: false,
50
+ currentFileName: null
51
  };
52
  this.deps = deps;
53
  }
client/src/ts/utils/demoBusinessLogic.ts CHANGED
@@ -9,6 +9,7 @@ import type { DemoManager } from '../ui/demoManager';
9
  import type { AppState } from './appStateManager';
10
  import { LocalDemoCache } from '../storage/localDemoCache';
11
  import URLHandler from './URLHandler';
 
12
 
13
  /**
14
  * Demo 渲染选项
@@ -75,7 +76,7 @@ export class DemoBusinessLogic {
75
  *
76
  * @param data 要渲染的数据
77
  * @param source 数据来源:'local' | 'server'(用于判断是否需要显示文件名等本地文件特殊处理)
78
- * @param filename 文件名(本地需要
79
  * @param options 渲染选项
80
  */
81
  renderDemo(
@@ -91,20 +92,28 @@ export class DemoBusinessLogic {
91
  // 2. 然后设置文本值(匹配分析结果的文本填入,不会清除hasValidData)
92
  this.deps.textInputController.setTextValue(data.request.text, true);
93
 
94
- // 3. 更新数据来源状态重置保存标志
95
- this.deps.updateAppState({
96
- dataSource: source,
97
- isSavedToLocal: false,
98
- isSavedToServer: false
99
- });
100
-
101
- // 4. 本地文件特殊处理
102
  if (source === 'local' && filename) {
 
 
103
  this.deps.updateFileNameDisplay(filename);
104
  // 清除 demo 高亮(本地文件不在 demo 列表中)
105
  this.deps.demoManager?.highlightDemo(null);
 
 
 
 
106
  }
107
 
 
 
 
 
 
 
 
 
108
  // 5. 确保系统已启动
109
  this.deps.ensureSystemStarted();
110
 
 
9
  import type { AppState } from './appStateManager';
10
  import { LocalDemoCache } from '../storage/localDemoCache';
11
  import URLHandler from './URLHandler';
12
+ import { getDemoName } from './pathUtils';
13
 
14
  /**
15
  * Demo 渲染选项
 
76
  *
77
  * @param data 要渲染的数据
78
  * @param source 数据来源:'local' | 'server'(用于判断是否需要显示文件名等本地文件特殊处理)
79
+ * @param filename 文件名(本地文件或路径(服务器文件,用于提取文件名)
80
  * @param options 渲染选项
81
  */
82
  renderDemo(
 
92
  // 2. 然后设置文本值(匹配分析结果的文本填入,不会清除hasValidData)
93
  this.deps.textInputController.setTextValue(data.request.text, true);
94
 
95
+ // 3. 提取并保存文件名
96
+ let currentFileName: string | null = null;
 
 
 
 
 
 
97
  if (source === 'local' && filename) {
98
+ // 本地文件:直接使用文件名
99
+ currentFileName = filename;
100
  this.deps.updateFileNameDisplay(filename);
101
  // 清除 demo 高亮(本地文件不在 demo 列表中)
102
  this.deps.demoManager?.highlightDemo(null);
103
+ } else if (source === 'server' && filename) {
104
+ // 服务器文件:使用工具函数提取文件名(不含扩展名),并添加 .json
105
+ const name = getDemoName(filename);
106
+ currentFileName = `${name}.json`;
107
  }
108
 
109
+ // 4. 更新数据来源状态并重置保存标志,同时保存文件名
110
+ this.deps.updateAppState({
111
+ dataSource: source,
112
+ isSavedToLocal: false,
113
+ isSavedToServer: false,
114
+ currentFileName
115
+ });
116
+
117
  // 5. 确保系统已启动
118
  this.deps.ensureSystemStarted();
119
 
client/src/ts/utils/demoPathUtils.ts CHANGED
@@ -93,13 +93,31 @@ export const validateFileName = (fileName: string): { valid: boolean; message?:
93
  };
94
 
95
  /**
96
- * 生成 demo 默认名称:取分析文本第一行的前50个字符
97
  *
98
  * @param currentData 当前分析数据
99
  * @param textFieldValue 文本输入框的值
 
100
  * @returns 默认名称
101
  */
102
- export const getDefaultDemoName = (currentData: AnalyzeResponse | null, textFieldValue: string): string => {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
103
  const rawText = (currentData?.request?.text || textFieldValue || '').trim();
104
  if (!rawText) {
105
  return '新Demo';
 
93
  };
94
 
95
  /**
96
+ * 生成 demo 默认名称:优先使用现有文件名,否则取分析文本第一行的前50个字符
97
  *
98
  * @param currentData 当前分析数据
99
  * @param textFieldValue 文本输入框的值
100
+ * @param existingFileName 可选的现有文件名(如果提供且有效,则优先使用)
101
  * @returns 默认名称
102
  */
103
+ export const getDefaultDemoName = (
104
+ currentData: AnalyzeResponse | null,
105
+ textFieldValue: string,
106
+ existingFileName?: string | null
107
+ ): string => {
108
+ // 如果提供了现有文件名且有效,则使用它(去掉 .json 后缀)
109
+ if (existingFileName && existingFileName.trim() && existingFileName !== '未选择文件') {
110
+ const trimmed = existingFileName.trim();
111
+ // 去掉 .json 后缀(如果存在)
112
+ const nameWithoutExt = trimmed.toLowerCase().endsWith('.json')
113
+ ? trimmed.slice(0, -5)
114
+ : trimmed;
115
+ if (nameWithoutExt) {
116
+ return nameWithoutExt;
117
+ }
118
+ }
119
+
120
+ // 否则,使用第一行逻辑
121
  const rawText = (currentData?.request?.text || textFieldValue || '').trim();
122
  if (!rawText) {
123
  return '新Demo';
client/src/ts/utils/highlightUtils.ts CHANGED
@@ -1,5 +1,5 @@
1
  import type { FrontendAnalyzeResult } from '../api/GLTR_API';
2
- import { calculateSurprisal } from './Util';
3
  import { extractRealTopkFromTokens } from './tokenUtils';
4
 
5
  /**
@@ -40,10 +40,44 @@ export function calculateTokenSurprisalHighlights(
40
  return highlightedIndices;
41
  }
42
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
  /**
44
  * 直方图类型
45
  */
46
- export type HistogramType = 'token';
47
 
48
  /**
49
  * 根据直方图类型和 bin 范围计算需要高亮的 token 索引集合
@@ -59,9 +93,16 @@ export function calculateHighlights(
59
  x1: number,
60
  result: FrontendAnalyzeResult
61
  ): { indices: Set<number>; style: 'border' | 'underline' } {
62
- return {
63
- indices: calculateTokenSurprisalHighlights(x0, x1, result),
64
- style: 'border'
65
- };
 
 
 
 
 
 
 
66
  }
67
 
 
1
  import type { FrontendAnalyzeResult } from '../api/GLTR_API';
2
+ import { calculateSurprisal, calculateSurprisalPerCharacter } from './Util';
3
  import { extractRealTopkFromTokens } from './tokenUtils';
4
 
5
  /**
 
40
  return highlightedIndices;
41
  }
42
 
43
+ /**
44
+ * 根据直方图 bin 的范围计算需要高亮的 token 索引集合(基于 char surprisal)
45
+ * @param x0 bin 起始值
46
+ * @param x1 bin 结束值
47
+ * @param result 前端分析结果(包含 mergedTokens)
48
+ * @returns 需要高亮的 merged token 索引集合
49
+ */
50
+ export function calculateCharSurprisalHighlights(
51
+ x0: number,
52
+ x1: number,
53
+ result: FrontendAnalyzeResult
54
+ ): Set<number> {
55
+ const highlightedIndices = new Set<number>();
56
+ const mergedTokens = result.mergedTokens;
57
+ const mergedRealTopk = extractRealTopkFromTokens(mergedTokens);
58
+
59
+ // 判断是否为最后一个 bin([19, 20])
60
+ const isLastBin = Math.abs(x0 - 19) < 0.001 && Math.abs(x1 - 20) < 0.001;
61
+
62
+ // 遍历 merged token,找到 char surprisal 在范围内的 token
63
+ for (let i = 0; i < mergedTokens.length; i++) {
64
+ const surprisal = calculateSurprisal(mergedRealTopk[i]?.[1] ?? 0);
65
+ const tokenText = mergedTokens[i]?.raw || '';
66
+ const charSurprisal = calculateSurprisalPerCharacter(surprisal, tokenText);
67
+ const inRange = isLastBin ? charSurprisal >= 19 : (charSurprisal >= x0 && charSurprisal < x1);
68
+
69
+ if (inRange) {
70
+ highlightedIndices.add(i);
71
+ }
72
+ }
73
+
74
+ return highlightedIndices;
75
+ }
76
+
77
  /**
78
  * 直方图类型
79
  */
80
+ export type HistogramType = 'token' | 'char';
81
 
82
  /**
83
  * 根据直方图类型和 bin 范围计算需要高亮的 token 索引集合
 
93
  x1: number,
94
  result: FrontendAnalyzeResult
95
  ): { indices: Set<number>; style: 'border' | 'underline' } {
96
+ if (histogramType === 'char') {
97
+ return {
98
+ indices: calculateCharSurprisalHighlights(x0, x1, result),
99
+ style: 'underline'
100
+ };
101
+ } else {
102
+ return {
103
+ indices: calculateTokenSurprisalHighlights(x0, x1, result),
104
+ style: 'border'
105
+ };
106
+ }
107
  }
108
 
server.py CHANGED
@@ -90,10 +90,16 @@ app.add_api('server.yaml')
90
  parser = argparse.ArgumentParser()
91
  # 导入默认模型配置(在参数解析后导入以避免循环依赖)
92
  from backend.model_manager import DEFAULT_MODEL
 
 
 
 
 
 
93
  parser.add_argument("--force-cpu", action='store_true', help='强制使用 CPU 模式')
94
  parser.add_argument("--cpu-force-bfloat16", action='store_true',
95
  help='强制 CPU 使用 bfloat16(需确认硬件支持 AVX-512_BF16 或 AMX,否则可能极慢)')
96
- parser.add_argument("--model", default=DEFAULT_MODEL)
97
  parser.add_argument("--address",
98
  default="0.0.0.0") # 0.0.0.0 for nonlocal use (accessible from LAN)
99
  parser.add_argument("--port", default="5001")
 
90
  parser = argparse.ArgumentParser()
91
  # 导入默认模型配置(在参数解析后导入以避免循环依赖)
92
  from backend.model_manager import DEFAULT_MODEL
93
+ from backend import REGISTERED_MODELS
94
+
95
+ # 获取已注册的模型列表用于help信息
96
+ registered_models_list = sorted(REGISTERED_MODELS.keys())
97
+ models_help = f"模型名称 (默认: {DEFAULT_MODEL})。可用模型: {', '.join(registered_models_list)}"
98
+
99
  parser.add_argument("--force-cpu", action='store_true', help='强制使用 CPU 模式')
100
  parser.add_argument("--cpu-force-bfloat16", action='store_true',
101
  help='强制 CPU 使用 bfloat16(需确认硬件支持 AVX-512_BF16 或 AMX,否则可能极慢)')
102
+ parser.add_argument("--model", default=DEFAULT_MODEL, help=models_help)
103
  parser.add_argument("--address",
104
  default="0.0.0.0") # 0.0.0.0 for nonlocal use (accessible from LAN)
105
  parser.add_argument("--port", default="5001")