| import * as d3 from 'd3'; |
| import type { TextStats } from '../utils/textStatistics'; |
| import { calculateTextStats } from '../utils/textStatistics'; |
| import { countTokenCharacters } from '../utils/Util'; |
| import type { FrontendAnalyzeResult } from '../api/GLTR_API'; |
| import { |
| updateBasicMetrics, |
| updateTotalSurprisal, |
| updateModel, |
| updateApiUsageDisplay, |
| validateMetricsElements, |
| type ApiTokenUsage |
| } from '../utils/textMetricsUpdater'; |
| import { tr } from '../lang/i18n-lite'; |
|
|
| |
| |
| |
| |
| export interface ExtendedInputEvent extends Event { |
| isMatchingAnalysis?: boolean; |
| } |
|
|
| export type TextInputControllerOptions = { |
| textField: d3.Selection<any, unknown, any, any>; |
| textCountValue: d3.Selection<any, unknown, any, any>; |
| |
| textMetrics?: d3.Selection<any, unknown, any, any>; |
| |
| metricBytes?: d3.Selection<any, unknown, any, any>; |
| metricChars?: d3.Selection<any, unknown, any, any>; |
| metricTokens?: d3.Selection<any, unknown, any, any>; |
| metricTotalSurprisal?: d3.Selection<any, unknown, any, any>; |
| |
| metricUsage?: d3.Selection<any, unknown, any, any>; |
| metricModel?: d3.Selection<any, unknown, any, any>; |
| clearBtn: d3.Selection<any, unknown, any, any>; |
| submitBtn: d3.Selection<any, unknown, any, any>; |
| saveBtn: d3.Selection<any, unknown, any, any>; |
| pasteBtn: d3.Selection<any, unknown, any, any>; |
| totalSurprisalFormat: (value: number | null) => string; |
| showAlertDialog: (title: string, message: string) => void; |
| }; |
|
|
| export class TextInputController { |
| private options: TextInputControllerOptions; |
|
|
| constructor(options: TextInputControllerOptions) { |
| this.options = options; |
| this.initialize(); |
| } |
|
|
| private initialize(): void { |
| |
| this.updateButtonStates(); |
|
|
| |
| |
| |
| const textFieldNode = this.options.textField.node() as HTMLTextAreaElement | null; |
| if (textFieldNode) { |
| textFieldNode.addEventListener('input', () => { |
| this.updateButtonStates(); |
| }); |
| } |
|
|
| |
| this.options.clearBtn.on('click', () => { |
| this.handleClear(); |
| }); |
|
|
| |
| this.options.pasteBtn.on('click', async () => { |
| await this.handlePaste(); |
| }); |
| } |
|
|
| |
| |
| |
| |
| |
| private updateButtonStates(): void { |
| const textValue = this.options.textField.property('value') || ''; |
| const hasText = textValue.length > 0; |
| |
| |
| this.options.clearBtn.classed('inactive', !hasText); |
| |
| |
| |
| if (!this.options.textCountValue.empty()) { |
| const charCount = countTokenCharacters(textValue); |
| this.options.textCountValue.text(charCount.toString()); |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| public updateTextMetrics( |
| stats: TextStats | null, |
| modelName?: string | null | undefined, |
| apiUsage?: ApiTokenUsage | null |
| ): void { |
| const { |
| metricBytes, |
| metricChars, |
| metricTokens, |
| metricTotalSurprisal, |
| metricUsage, |
| metricModel, |
| totalSurprisalFormat |
| } = this.options; |
|
|
| |
| if (metricUsage && !metricUsage.empty()) { |
| if ( |
| !metricModel || |
| metricModel.empty() || |
| !validateMetricsElements(metricUsage, metricModel) |
| ) { |
| return; |
| } |
| updateApiUsageDisplay(metricUsage, apiUsage ?? null); |
| updateModel(metricModel, modelName); |
| return; |
| } |
|
|
| if ( |
| !metricBytes || |
| !metricChars || |
| !metricTokens || |
| !metricTotalSurprisal || |
| !metricModel || |
| metricModel.empty() || |
| !validateMetricsElements( |
| metricBytes, |
| metricChars, |
| metricTokens, |
| metricTotalSurprisal, |
| metricModel |
| ) |
| ) { |
| return; |
| } |
|
|
| if (stats) { |
| updateBasicMetrics(metricBytes, metricChars, metricTokens, stats, apiUsage); |
| updateTotalSurprisal(metricTotalSurprisal, stats, totalSurprisalFormat); |
| } |
|
|
| updateModel(metricModel, modelName); |
| } |
|
|
| |
| |
| |
| private handleClear(): void { |
| const textValue = this.options.textField.property('value') || ''; |
| if (textValue.length === 0) { |
| return; |
| } |
| this.options.textField.property('value', ''); |
| |
| this.options.textField.node()?.dispatchEvent(new Event('input', { bubbles: true })); |
| } |
|
|
| |
| |
| |
| private async handlePaste(): Promise<void> { |
| try { |
| const text = await navigator.clipboard.readText(); |
| if (text) { |
| const currentValue = this.options.textField.property('value') || ''; |
| |
| const textarea = this.options.textField.node() as HTMLTextAreaElement; |
| if (textarea) { |
| const start = textarea.selectionStart || currentValue.length; |
| const end = textarea.selectionEnd || currentValue.length; |
| const newValue = currentValue.substring(0, start) + text + currentValue.substring(end); |
| this.options.textField.property('value', newValue); |
| |
| textarea.setSelectionRange(start + text.length, start + text.length); |
| } else { |
| this.options.textField.property('value', currentValue + text); |
| } |
| |
| this.options.textField.node()?.dispatchEvent(new Event('input', { bubbles: true })); |
| } |
| } catch (error) { |
| console.error('粘贴失败:', error); |
| |
| this.options.showAlertDialog(tr('Info'), tr('Failed to read clipboard, please paste manually')); |
| } |
| } |
|
|
| |
| |
| |
| public getTextValue(): string { |
| return this.options.textField.property('value') || ''; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| public setTextValue(value: string, isMatchingAnalysis: boolean = false): void { |
| this.options.textField.property('value', value); |
| |
| const event = new Event('input', { bubbles: true }) as ExtendedInputEvent; |
| event.isMatchingAnalysis = isMatchingAnalysis; |
| this.options.textField.node()?.dispatchEvent(event); |
| } |
| } |
|
|
| |
| |
| |
| export const calculateTextStatsForController = ( |
| result: FrontendAnalyzeResult, |
| originalText: string |
| ): TextStats => { |
| return calculateTextStats(result, originalText); |
| }; |
|
|
|
|