InfoLens / client /src /ts /controllers /highlightController.ts
dqy08's picture
initial beta release
494c9e4
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 = <HTMLElement>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();
}
});
};