|
|
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); |
|
|
}; |
|
|
|
|
|
|