import * as d3 from 'd3'; import type { GLTR_Text_Box } from '../vis/GLTR_Text_Box'; import type { Histogram } from '../vis/Histogram'; import type { HistogramBinClickEvent } from '../vis/Histogram'; import type { ScatterPlot, ScatterChunkClickEvent } from '../vis/ScatterPlot'; import type { FrontendAnalyzeResult } from '../api/GLTR_API'; import { calculateHighlights, type HistogramType, type HighlightData } from '../utils/highlightUtils'; export type HighlightCurrentData = { result: FrontendAnalyzeResult; signalProbs?: number[]; pPwValues?: number[]; pwScores?: number[] } | null; export type HighlightControllerOptions = { stats_frac: Histogram; stats_raw_score_normed?: Histogram; stats_match_score_progress?: ScatterPlot; lmf: GLTR_Text_Box; currentData: HighlightCurrentData; }; export class HighlightController { private options: HighlightControllerOptions; constructor(options: HighlightControllerOptions) { this.options = options; } /** * 清除所有高亮(文本与直方图) */ public clearHighlights(): void { this.options.stats_frac.clearSelection(); this.options.stats_raw_score_normed?.clearSelection(); this.options.stats_match_score_progress?.clearSelection(); this.options.lmf.clearHighlight(); } /** * 处理直方图 bin 点击事件 */ public handleHistogramBinClick(ev: HistogramBinClickEvent): void { const { currentData } = this.options; if (!currentData) return; // 如果 binIndex 为 -1,表示用户取消选择,清除所有高亮 if (ev.binIndex === -1) { this.clearHighlights(); return; } const { x0, x1, binIndex, no_bins, source } = ev; const highlightData: HighlightData = { ...currentData.result, signalProbs: currentData.signalProbs, pPwValues: currentData.pPwValues, pwScores: currentData.pwScores }; let histogramType: HistogramType = 'token'; if (source === 'stats_raw_score_normed') histogramType = 'raw_score_normed'; if (histogramType === 'raw_score_normed') { this.options.stats_frac.clearSelection(); } else { this.options.stats_raw_score_normed?.clearSelection(); } this.options.stats_match_score_progress?.clearSelection(); const { indices, style } = calculateHighlights(histogramType, x0, x1, binIndex, no_bins, highlightData); this.options.lmf.setHighlightedIndices(indices, style); } /** * 处理 match score per chunk 进度图 chunk 区域点击 */ public handleMatchScoreChunkClick(ev: ScatterChunkClickEvent): void { if (ev.source !== 'stats_match_score_progress') return; const { currentData } = this.options; if (!currentData) return; if (ev.chunkIndex === -1) { this.options.lmf.clearHighlight(); return; } this.options.stats_frac.clearSelection(); this.options.stats_raw_score_normed?.clearSelection(); this.options.lmf.setChunkCharRangeHighlight(ev.x0, ev.x1); this.options.lmf.scrollToUnicodeCharOffset(ev.x0); } /** 获取当前高亮数据 */ public getCurrentData(): HighlightCurrentData { return (this.options as { currentData: HighlightCurrentData }).currentData; } /** * 更新当前数据(当数据变化时调用) */ public updateCurrentData(currentData: HighlightCurrentData): void { (this.options as { currentData: HighlightCurrentData }).currentData = currentData; } } /** * 初始化高亮清除事件监听(点击空白处和 ESC 键) */ export const initHighlightClearListeners = ( clearHighlights: () => void ): void => { // 点击页面空白处清除高亮(通用解决方案) // 监听整个文档的点击事件,但排除可交互元素 d3.select('body').on('click.clearHighlight', (event: MouseEvent) => { const target = event.target; if (!target) return; // 排除可交互元素:token、按钮、输入框、直方图bin等 const isInteractive = target.closest('.token') || // token元素 target.closest('button') || // 按钮 target.closest('input') || // 输入框 target.closest('textarea') || // 文本域 target.closest('select') || // 下拉框 target.closest('.bar') || // 直方图bar target.closest('.hover-area') || // 直方图悬停区域 target.closest('a') || // 链接 target.closest('[role="button"]') || // 有button角色的元素 target.closest('[onclick]'); // 有onclick属性的元素 // 如果点击的不是可交互元素,则清除高亮 if (!isInteractive) { clearHighlights(); } }); // 按下 ESC 键清除高亮 d3.select(window).on('keydown.clearHighlight', (event: KeyboardEvent) => { if (event.key === 'Escape') { clearHighlights(); } }); };