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
|
| 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:
|
| 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 |
-
|
| 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 (
|
| 122 |
-
|
| 123 |
-
|
| 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 |
-
|
|
|
|
| 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 |
-
|
| 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
|
| 105 |
-
this.deps.textInputController.
|
| 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 |
-
|
| 266 |
-
|
| 267 |
-
// 更新模型显示(从分析结果中获取实际使用的模型)
|
| 268 |
const resultModel = data.request?.model || null;
|
| 269 |
-
this.
|
| 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 |
}
|