| | 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, validateMetricsElements } 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>; |
| | 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.trim().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): void { |
| | const { |
| | metricBytes, |
| | metricChars, |
| | metricTokens, |
| | metricTotalSurprisal, |
| | metricModel, |
| | totalSurprisalFormat |
| | } = this.options; |
| |
|
| | |
| | if (!validateMetricsElements(metricBytes, metricChars, metricTokens, metricTotalSurprisal, metricModel)) { |
| | return; |
| | } |
| |
|
| | |
| | if (stats) { |
| | updateBasicMetrics(metricBytes, metricChars, metricTokens, stats); |
| | updateTotalSurprisal(metricTotalSurprisal, stats, totalSurprisalFormat); |
| | } |
| |
|
| | |
| | |
| | updateModel(metricModel, modelName); |
| | } |
| |
|
| | |
| | |
| | |
| | private handleClear(): void { |
| | const textValue = this.options.textField.property('value') || ''; |
| | if (!textValue.trim()) { |
| | 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); |
| | }; |
| |
|
| |
|