File size: 5,243 Bytes
494c9e4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
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();
        }
    });
};