dqy08 commited on
Commit
6284eeb
·
1 Parent(s): fc062b2

Refactor text metrics handling and improve state management

Browse files
client/src/index.html CHANGED
@@ -68,7 +68,7 @@
68
  <span id="metric_tokens">0 tokens</span>
69
  </div>
70
  <div id="metric_total_surprisal" class="text-metrics-secondary">总surprisal = 0 bits</div>
71
- <div id="metric_model" class="text-metrics-secondary is-hidden">model: </div>
72
  </div>
73
  <div class="button-right">
74
  <button id="save_demo_btn" class="primary-btn inactive">Upload</button>
 
68
  <span id="metric_tokens">0 tokens</span>
69
  </div>
70
  <div id="metric_total_surprisal" class="text-metrics-secondary">总surprisal = 0 bits</div>
71
+ <div id="metric_model" class="text-metrics-secondary">model: </div>
72
  </div>
73
  <div class="button-right">
74
  <button id="save_demo_btn" class="primary-btn inactive">Upload</button>
client/src/ts/appInitializer.ts CHANGED
@@ -16,7 +16,7 @@ export interface CommonAppContext {
16
  api: TextAnalysisAPI;
17
  surprisalColorScale: (value: number) => string;
18
  textEncoder: TextEncoder;
19
- totalSurprisalFormat: (n: number) => string;
20
  }
21
 
22
  /**
@@ -29,12 +29,13 @@ export function initializeCommonApp(apiPrefix: string = '', element?: Element):
29
  // 使用传入的元素或默认 body 元素
30
  const targetElement = element || document.body;
31
 
 
32
  return {
33
  eventHandler: new SimpleEventHandler(targetElement),
34
  api: new TextAnalysisAPI(apiPrefix),
35
  surprisalColorScale: createSurprisalColorScale(),
36
  textEncoder: new TextEncoder(),
37
- totalSurprisalFormat: d3.format('.2f')
38
  };
39
  }
40
 
 
16
  api: TextAnalysisAPI;
17
  surprisalColorScale: (value: number) => string;
18
  textEncoder: TextEncoder;
19
+ totalSurprisalFormat: (n: number | null) => string;
20
  }
21
 
22
  /**
 
29
  // 使用传入的元素或默认 body 元素
30
  const targetElement = element || document.body;
31
 
32
+ const format = d3.format('.2f');
33
  return {
34
  eventHandler: new SimpleEventHandler(targetElement),
35
  api: new TextAnalysisAPI(apiPrefix),
36
  surprisalColorScale: createSurprisalColorScale(),
37
  textEncoder: new TextEncoder(),
38
+ totalSurprisalFormat: (n: number | null) => n !== null && Number.isFinite(n) ? format(n) : String(n)
39
  };
40
  }
41
 
client/src/ts/controllers/textInputController.ts CHANGED
@@ -25,7 +25,7 @@ export type TextInputControllerOptions = {
25
  saveBtn: d3.Selection<any, unknown, any, any>;
26
  pasteBtn: d3.Selection<any, unknown, any, any>;
27
  textEncoder: TextEncoder | null;
28
- totalSurprisalFormat: (value: number) => string;
29
  showAlertDialog: (title: string, message: string) => void;
30
  };
31
 
@@ -83,74 +83,42 @@ export class TextInputController {
83
  }
84
 
85
  /**
86
- * 隐藏文本指标
87
- */
88
- public hideTextMetrics(): void {
89
- if (!this.options.textMetrics.empty()) {
90
- this.options.textMetrics.classed('is-hidden', true);
91
- }
92
- // 同时隐藏模型显示
93
- this.updateModelDisplay(null);
94
- }
95
-
96
- /**
97
- * 更新文本指标显示
98
  */
99
- public updateTextMetrics(stats: TextStats | null): void {
100
  const {
101
- textMetrics,
102
  metricBytes,
103
  metricChars,
104
  metricTokens,
105
  metricTotalSurprisal,
 
106
  totalSurprisalFormat
107
  } = this.options;
108
 
109
  // 检查必要的元素是否存在
110
  if (
111
- textMetrics.empty() ||
112
  metricBytes.empty() ||
113
  metricChars.empty() ||
114
  metricTokens.empty() ||
115
- metricTotalSurprisal.empty()
 
116
  ) {
117
  return;
118
  }
119
 
120
- // 如果有统计数据,隐藏整个指标容器
121
- if (!stats) {
122
- this.hideTextMetrics();
123
- return;
124
- }
125
-
126
- // 更新各个指标的内容
127
- metricBytes.text(`${stats.byteCount} B`);
128
- metricChars.text(`${stats.charCount} 字`);
129
- metricTokens.text(`${stats.tokenCount} tokens`);
130
- if (stats.totalSurprisal !== null && Number.isFinite(stats.totalSurprisal)) {
131
  metricTotalSurprisal.text(`总surprisal = ${totalSurprisalFormat(stats.totalSurprisal)} bits`);
132
- } else {
133
- metricTotalSurprisal.text(`总surprisal = -- bits`);
134
- }
135
-
136
- // 显示指标容器
137
- textMetrics.classed('is-hidden', false);
138
- }
139
-
140
- /**
141
- * 更新模型显示
142
- * @param modelName 模型名称,始终显示以反映原始情况
143
- */
144
- public updateModelDisplay(modelName: string | null | undefined): void {
145
- const { metricModel } = this.options;
146
-
147
- // 检查元素是否存在
148
- if (metricModel.empty()) {
149
- return;
150
  }
151
 
152
- // 始终显示模型信息,反映原始情况
153
- metricModel.text(`model: ${modelName}`).classed('is-hidden', false);
 
154
  }
155
 
156
  /**
 
25
  saveBtn: d3.Selection<any, unknown, any, any>;
26
  pasteBtn: d3.Selection<any, unknown, any, any>;
27
  textEncoder: TextEncoder | null;
28
+ totalSurprisalFormat: (value: number | null) => string;
29
  showAlertDialog: (title: string, message: string) => void;
30
  };
31
 
 
83
  }
84
 
85
  /**
86
+ * 更新文本指标内容(包括模型显示,不控制显示/隐藏,显示/隐藏由 AppStateManager 统一管理)
87
+ * @param stats 统计数据,为 null 时不更新统计内容
88
+ * @param modelName 模型名称,始终显示以反映原始情况
 
 
 
 
 
 
 
 
 
89
  */
90
+ public updateTextMetrics(stats: TextStats | null, modelName?: string | null | undefined): void {
91
  const {
 
92
  metricBytes,
93
  metricChars,
94
  metricTokens,
95
  metricTotalSurprisal,
96
+ metricModel,
97
  totalSurprisalFormat
98
  } = this.options;
99
 
100
  // 检查必要的元素是否存在
101
  if (
 
102
  metricBytes.empty() ||
103
  metricChars.empty() ||
104
  metricTokens.empty() ||
105
+ metricTotalSurprisal.empty() ||
106
+ metricModel.empty()
107
  ) {
108
  return;
109
  }
110
 
111
+ // 更新统计指标内容(如果有统计数据
112
+ if (stats) {
113
+ metricBytes.text(`${stats.byteCount} B`);
114
+ metricChars.text(`${stats.charCount} 字`);
115
+ metricTokens.text(`${stats.tokenCount} tokens`);
 
 
 
 
 
 
116
  metricTotalSurprisal.text(`总surprisal = ${totalSurprisalFormat(stats.totalSurprisal)} bits`);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
117
  }
118
 
119
+ // 更新模型显示(始终显示反映原始情况
120
+ // 显示/隐藏由 AppStateManager 通过 textMetrics 容器统一管理
121
+ metricModel.text(`model: ${modelName}`);
122
  }
123
 
124
  /**
client/src/ts/start.ts CHANGED
@@ -130,7 +130,8 @@ window.onload = () => {
130
  submitBtn: submitBtn as d3.Selection<HTMLElement, unknown, HTMLElement, unknown>,
131
  saveBtn: saveBtn as d3.Selection<HTMLElement, unknown, HTMLElement, unknown>,
132
  saveLocalBtn: saveLocalBtn as d3.Selection<HTMLElement, unknown, HTMLElement, unknown>,
133
- textField: textField as d3.Selection<HTMLElement, unknown, HTMLElement, unknown>
 
134
  });
135
 
136
  // 创建GLTR文本可视化实例
@@ -394,9 +395,7 @@ window.onload = () => {
394
  },
395
  onDemoLoading: (loading) => {
396
  // loading 状态已经通过 setGlobalLoading 更新,会自动触发按钮状态更新
397
- if (loading) {
398
- textInputController.hideTextMetrics();
399
- }
400
  // Clear按钮状态由TextInputController内部自动管理,不需要手动更新
401
  appStateManager.setGlobalLoading(loading);
402
  },
@@ -480,6 +479,7 @@ window.onload = () => {
480
 
481
  if (!isMatchingAnalysis) {
482
  // 单方面的文本修改(用户输入、预填充等),清除数据标记并重置状态(视为新的分析阶段)
 
483
  appStateManager.updateState({
484
  hasValidData: false,
485
  dataSource: null,
@@ -488,9 +488,8 @@ window.onload = () => {
488
  });
489
  }
490
  // 如果是匹配分析结果的文本填入,不清除hasValidData(因为updateFromRequest已经重新设置了)
 
491
 
492
- // 隐藏文本指标
493
- textInputController.hideTextMetrics();
494
  // 清除文件名显示(如果是程序加载,会在 setTextValue 后立即恢复)
495
  updateFileNameDisplay(null);
496
  });
 
130
  submitBtn: submitBtn as d3.Selection<HTMLElement, unknown, HTMLElement, unknown>,
131
  saveBtn: saveBtn as d3.Selection<HTMLElement, unknown, HTMLElement, unknown>,
132
  saveLocalBtn: saveLocalBtn as d3.Selection<HTMLElement, unknown, HTMLElement, unknown>,
133
+ textField: textField as d3.Selection<HTMLElement, unknown, HTMLElement, unknown>,
134
+ textMetrics: textMetrics as d3.Selection<HTMLElement, unknown, HTMLElement, unknown>
135
  });
136
 
137
  // 创建GLTR文本可视化实例
 
395
  },
396
  onDemoLoading: (loading) => {
397
  // loading 状态已经通过 setGlobalLoading 更新,会自动触发按钮状态更新
398
+ // 注意:TextMetrics 的显示/隐藏由 AppStateManager 统一管理,不需要手动调用 hideTextMetrics
 
 
399
  // Clear按钮状态由TextInputController内部自动管理,不需要手动更新
400
  appStateManager.setGlobalLoading(loading);
401
  },
 
479
 
480
  if (!isMatchingAnalysis) {
481
  // 单方面的文本修改(用户输入、预填充等),清除数据标记并重置状态(视为新的分析阶段)
482
+ // AppStateManager 会自动隐藏 TextMetrics(因为 hasValidData = false),包括模型显示
483
  appStateManager.updateState({
484
  hasValidData: false,
485
  dataSource: null,
 
488
  });
489
  }
490
  // 如果是匹配分析结果的文本填入,不清除hasValidData(因为updateFromRequest已经重新设置了)
491
+ // 也不隐藏统计信息(因为updateFromRequest已经显示了统计信息)
492
 
 
 
493
  // 清除文件名显示(如果是程序加载,会在 setTextValue 后立即恢复)
494
  updateFileNameDisplay(null);
495
  });
client/src/ts/utils/analyzeFlow.ts CHANGED
@@ -140,10 +140,9 @@ export class AnalyzeFlowManager {
140
  // 完善错误恢复:恢复所有状态
141
  this.deps.appStateManager.setIsAnalyzing(false);
142
  this.deps.appStateManager.setGlobalLoading(false);
143
- // 分析失败,清除数据标记(数据无效
144
  // updateState 内部会自动调用 updateButtonStatesFromAppState,不需要额外调用
145
  this.deps.appStateManager.updateState({ hasValidData: false });
146
- this.deps.textInputController.hideTextMetrics();
147
  const message = error instanceof Error ? error.message : '分析失败';
148
  showAlertDialog('错误', `分析失败:${message}`);
149
  return null;
 
140
  // 完善错误恢复:恢复所有状态
141
  this.deps.appStateManager.setIsAnalyzing(false);
142
  this.deps.appStateManager.setGlobalLoading(false);
143
+ // 分析失败,清除数据标记(AppStateManager 会自动隐藏 TextMetrics,包括模型显示
144
  // updateState 内部会自动调用 updateButtonStatesFromAppState,不需要额外调用
145
  this.deps.appStateManager.updateState({ hasValidData: false });
 
146
  const message = error instanceof Error ? error.message : '分析失败';
147
  showAlertDialog('错误', `分析失败:${message}`);
148
  return null;
client/src/ts/utils/appStateManager.ts CHANGED
@@ -27,6 +27,7 @@ export interface ButtonStateDependencies {
27
  saveBtn: d3.Selection<HTMLElement, unknown, HTMLElement, unknown>;
28
  saveLocalBtn: d3.Selection<HTMLElement, unknown, HTMLElement, unknown>;
29
  textField: d3.Selection<HTMLElement, unknown, HTMLElement, unknown>;
 
30
  }
31
 
32
  /**
@@ -78,7 +79,7 @@ export class AppStateManager {
78
  }
79
 
80
  /**
81
- * 根据应用状态计算并更新所有按钮状态
82
  */
83
  private updateButtonStatesFromAppState(): void {
84
  const hasText = (this.deps.textField.property('value') || '').trim().length > 0;
@@ -102,6 +103,11 @@ export class AppStateManager {
102
  this.state.dataSource === 'local' ||
103
  this.state.isSavedToLocal
104
  );
 
 
 
 
 
105
  }
106
 
107
  /**
 
27
  saveBtn: d3.Selection<HTMLElement, unknown, HTMLElement, unknown>;
28
  saveLocalBtn: d3.Selection<HTMLElement, unknown, HTMLElement, unknown>;
29
  textField: d3.Selection<HTMLElement, unknown, HTMLElement, unknown>;
30
+ textMetrics: d3.Selection<HTMLElement, unknown, HTMLElement, unknown>;
31
  }
32
 
33
  /**
 
79
  }
80
 
81
  /**
82
+ * 根据应用状态计算并更新所有按钮状态和UI元素状态
83
  */
84
  private updateButtonStatesFromAppState(): void {
85
  const hasText = (this.deps.textField.property('value') || '').trim().length > 0;
 
103
  this.state.dataSource === 'local' ||
104
  this.state.isSavedToLocal
105
  );
106
+
107
+ // TextMetrics显示:有有效数据(hasValidData = true 时,stats 一定不为 null)
108
+ if (!this.deps.textMetrics.empty()) {
109
+ this.deps.textMetrics.classed('is-hidden', !this.state.hasValidData);
110
+ }
111
  }
112
 
113
  /**
client/src/ts/utils/visualizationUpdater.ts CHANGED
@@ -99,17 +99,10 @@ export class VisualizationUpdater {
99
  }
100
 
101
  /**
102
- * 隐藏文本指标
103
  */
104
- private hideTextMetrics(): void {
105
- this.deps.textInputController.hideTextMetrics();
106
- }
107
-
108
- /**
109
- * 更新文本指标
110
- */
111
- private updateTextMetrics(stats: TextStats | null): void {
112
- this.deps.textInputController.updateTextMetrics(stats);
113
  }
114
 
115
  /**
@@ -172,9 +165,8 @@ export class VisualizationUpdater {
172
  const abortDueToInvalidResponse = (message: string) => {
173
  console.error(message);
174
  showAlertDialog('错误', message);
175
- // 数据无效,清除数据标记
176
  this.deps.appStateManager.updateState({ hasValidData: false });
177
- this.hideTextMetrics();
178
  };
179
 
180
  try {
@@ -262,11 +254,9 @@ export class VisualizationUpdater {
262
  this.currentState.currentTokenAvg = textStats.tokenAverage;
263
  this.currentState.currentTotalSurprisal = textStats.totalSurprisal;
264
 
265
- this.updateTextMetrics(textStats);
266
-
267
- // 更新模型显示(从分析结果中获取实际使用的模型)
268
  const resultModel = data.request?.model || null;
269
- this.deps.textInputController.updateModelDisplay(resultModel);
270
 
271
  // Analyze 渲染完成后关闭动画,避免拖拽等二次渲染再次播放
272
  if (!disableAnimation) {
@@ -284,9 +274,8 @@ export class VisualizationUpdater {
284
  console.error('Error updating visualization:', error);
285
  this.deps.appStateManager.setIsAnalyzing(false);
286
  this.deps.appStateManager.setGlobalLoading(false);
287
- // analyze失败时,清除数据标记(数据无效
288
  this.deps.appStateManager.updateState({ hasValidData: false });
289
- this.hideTextMetrics();
290
  showAlertDialog('错误', 'Error rendering visualization. Check console for details.');
291
  return;
292
  }
 
99
  }
100
 
101
  /**
102
+ * 更新文本指标(包括模型显示)
103
  */
104
+ private updateTextMetrics(stats: TextStats | null, modelName?: string | null | undefined): void {
105
+ this.deps.textInputController.updateTextMetrics(stats, modelName);
 
 
 
 
 
 
 
106
  }
107
 
108
  /**
 
165
  const abortDueToInvalidResponse = (message: string) => {
166
  console.error(message);
167
  showAlertDialog('错误', message);
168
+ // 数据无效,清除数据标记(AppStateManager 会自动隐藏 TextMetrics,包括模型显示)
169
  this.deps.appStateManager.updateState({ hasValidData: false });
 
170
  };
171
 
172
  try {
 
254
  this.currentState.currentTokenAvg = textStats.tokenAverage;
255
  this.currentState.currentTotalSurprisal = textStats.totalSurprisal;
256
 
257
+ // 更新文本指标和模型显示(从分析结果中获取实际使用的模型)
 
 
258
  const resultModel = data.request?.model || null;
259
+ this.updateTextMetrics(textStats, resultModel);
260
 
261
  // Analyze 渲染完成后关闭动画,避免拖拽等二次渲染再次播放
262
  if (!disableAnimation) {
 
274
  console.error('Error updating visualization:', error);
275
  this.deps.appStateManager.setIsAnalyzing(false);
276
  this.deps.appStateManager.setGlobalLoading(false);
277
+ // analyze失败时,清除数据标记(AppStateManager 会自动隐藏 TextMetrics,包括模型显示
278
  this.deps.appStateManager.updateState({ hasValidData: false });
 
279
  showAlertDialog('错误', 'Error rendering visualization. Check console for details.');
280
  return;
281
  }