| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| import * as d3 from 'd3'; |
| import { processCandidateText } from './tokenDisplayUtils'; |
|
|
| |
| |
| |
| |
| export function formatTopkTooltipProbabilityPercent(v: number): string { |
| return d3.format('.3g')(v * 100) + '%'; |
| } |
|
|
| |
| const MAX_BAR_WIDTH = 60; |
| |
| const SEMANTIC_DEBUG_MAX_BAR = 100; |
| const SEMANTIC_DEBUG_BAR_CELL = 180; |
|
|
| |
| export const TOPK_SEP = '\0__TOPK_SEP__\0'; |
|
|
| |
| function probabilitiesEffectivelyEqual(a: number, b: number): boolean { |
| if (a === b) return true; |
| const d = Math.abs(a - b); |
| return d < 1e-12 || d / Math.max(Math.abs(a), Math.abs(b), 1e-15) < 1e-9; |
| } |
|
|
| export interface TopkChartOptions { |
| |
| selectedToken?: string; |
| |
| |
| |
| |
| selectedProb?: number; |
| |
| maxBarWidth?: number; |
| |
| barCellWidth?: number; |
| numFormat?: (n: number) => string; |
| |
| interactivePickable?: boolean; |
| } |
|
|
| |
| function mergeTopkRowsWithActualTokenIfAbsent( |
| rows: Array<{ token: string; prob: number }>, |
| actualToken: string, |
| actualProb: number |
| ): Array<{ token: string; prob: number }> { |
| if (!rows.length || actualToken === '') return rows; |
| if (!Number.isFinite(actualProb)) return rows; |
| if (rows.some((d) => d.token === actualToken)) return rows; |
| return [...rows, { token: TOPK_SEP, prob: 0 }, { token: actualToken, prob: actualProb }]; |
| } |
|
|
| export type TopkDisplaySelection = { token: string; prob: number }; |
|
|
| |
| export function topkDisplaySelection( |
| token: string | undefined | null, |
| prob: number | null | undefined |
| ): TopkDisplaySelection | undefined { |
| if (token == null || token === '') return undefined; |
| if (prob == null || !Number.isFinite(prob)) return undefined; |
| return { token, prob }; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| export function prepareTopkDisplayRows( |
| baseRows: Array<{ token: string; prob: number }>, |
| selection: TopkDisplaySelection | undefined |
| ): Array<{ token: string; prob: number }> { |
| if (!selection || !baseRows.length) return baseRows; |
| const { token, prob } = selection; |
| if (token === '' || !Number.isFinite(prob)) return baseRows; |
| return mergeTopkRowsWithActualTokenIfAbsent(baseRows, token, prob); |
| } |
|
|
| |
| function resolveSelectedRowIndex( |
| data: Array<{ token: string; prob: number }>, |
| selectedToken: string | undefined, |
| selectedProb: number | undefined |
| ): number { |
| if (selectedToken === undefined || selectedToken === '') return -1; |
| if (selectedProb != null && Number.isFinite(selectedProb)) { |
| const i = data.findIndex( |
| (d) => |
| d.token !== TOPK_SEP && |
| d.token === selectedToken && |
| probabilitiesEffectivelyEqual(d.prob, selectedProb) |
| ); |
| if (i >= 0) return i; |
| } |
| return data.findIndex((d) => d.token !== TOPK_SEP && d.token === selectedToken); |
| } |
|
|
| |
| export function renderTopkChartHtml( |
| data: Array<{ token: string; prob: number }>, |
| options?: TopkChartOptions |
| ): string { |
| if (!data.length) return ''; |
|
|
| const maxBar = options?.maxBarWidth ?? MAX_BAR_WIDTH; |
| const numF = options?.numFormat ?? formatTopkTooltipProbabilityPercent; |
|
|
| |
| const scale = d3.scaleLinear().domain([0, 1]).range([0, maxBar]); |
| const barCellW = options?.barCellWidth ?? 110; |
|
|
| const pickable = options?.interactivePickable === true; |
| const highlightRowIndex = resolveSelectedRowIndex(data, options?.selectedToken, options?.selectedProb); |
|
|
| const rows = data.map((d, i) => { |
| if (d.token === TOPK_SEP) { |
| return `<div class="row topk-chart-row topk-chart-row--ellipsis">⋮</div>`; |
| } |
| const isSelected = highlightRowIndex >= 0 && i === highlightRowIndex; |
| const rowClasses = [ |
| 'row', |
| 'topk-chart-row', |
| pickable ? 'topk-chart-row--pickable' : '', |
| isSelected ? 'topk-chart-row--selected' : '', |
| ] |
| .filter(Boolean) |
| .join(' '); |
| const bar = `<div class="topk-chart-bar-cell" style="width:${barCellW}px;">` + |
| `<span class="topk-chart-bar-fill" style="width: ${scale(d.prob)}px;"></span>` + |
| ` <span class="topk-chart-prob">${numF(d.prob)}</span></div>`; |
| const text = `<div class="topk-chart-token-cell">${processCandidateText(d.token)}</div>`; |
| const pickAttr = pickable ? ` data-topk-pick="${encodeURIComponent(d.token)}"` : ''; |
| return `<div class="${rowClasses}"${pickAttr}>${bar}${text}</div>`; |
| }); |
|
|
| return rows.join(''); |
| } |
|
|
| |
| export function renderTopkChartFullHtml(data: Array<{ token: string; prob: number }>, options?: TopkChartOptions): string { |
| const opts = options ?? {}; |
| const semanticOpts = { |
| ...opts, |
| maxBarWidth: opts.maxBarWidth ?? SEMANTIC_DEBUG_MAX_BAR, |
| barCellWidth: opts.barCellWidth ?? SEMANTIC_DEBUG_BAR_CELL, |
| }; |
| const rows = renderTopkChartHtml(data, semanticOpts); |
| return rows ? `<div class="semantic-debug-topk-chart predictions predictions-table">${rows}</div>` : ''; |
| } |
|
|