| import * as d3 from 'd3'; |
| import './utils/d3-polyfill'; |
| import '../css/start.scss'; |
| import '../css/chat.scss'; |
| import '../css/attribution.scss'; |
|
|
| import { initThemeManager } from './ui/theme'; |
| import { initLanguageManager } from './ui/language'; |
| import { initI18n, tr, trf } from './lang/i18n-lite'; |
| import { AdminManager } from './utils/adminManager'; |
| import { SettingsMenuManager } from './utils/settingsMenuManager'; |
| import { initChatPanelLayout } from './chat/chatPanelLayout'; |
| import { PANEL_SPLIT_STORAGE_KEY_ATTRIBUTION } from './utils/panelSplitStorage'; |
| import { TextInputController } from './controllers/textInputController'; |
| import { initializeCommonApp } from './appInitializer'; |
| import { showAlertDialog } from './ui/dialog'; |
| import URLHandler from './utils/URLHandler'; |
| import { initCachedHistoryQueryDropdown, type CachedHistorySelectContext } from './utils/cachedHistoryUi'; |
| import { |
| DEFAULT_CONTENT_URL_PARAM, |
| readContentUrlParam, |
| replaceContentUrlParam, |
| runContentUrlHydrate, |
| } from './utils/contentUrl'; |
| import { initQueryHistoryDropdown, saveHistory } from './utils/queryHistory'; |
| import { createToast } from './ui/toast'; |
| import { translateApiErrorMessage } from './utils/errorUtils'; |
| import { createAttributionInspector } from './attribution/attributionInspector'; |
| import type { AttributionDisplayOptions } from './attribution/attributionDisplayModel'; |
| import { |
| buildCachedContentUrlParam, |
| getCachedEntryByContentKey, |
| listCachedHistoryRows, |
| removeCachedEntryByContentKey, |
| takeSuccessfulAttributionFromCache, |
| touchCachedEntryByContentKey, |
| type AttributionApiResponse, |
| type AttributionCachedEntry, |
| type PredictionAttributeModelVariant, |
| } from './attribution/attributionResultCache'; |
| import { loadPredictionAttributeWithCache } from './attribution/predictionAttributeClient'; |
| import { readStoredEffectiveExcludePromptPatternsText } from './attribution/attributionExcludePromptPatternsStorage'; |
| import { bindExcludePromptPatternsUi } from './attribution/excludePromptPatternsUi'; |
| import { syncDraftCommittedButtonPair } from './utils/syncDraftCommittedButtonPair'; |
|
|
| d3.selectAll('.loadersmall').style('display', 'none'); |
|
|
| initI18n(); |
|
|
| const showToast = createToast('#toast').show; |
|
|
| const CONTEXT_HISTORY_KEY = 'info_radar_attribution_context_history'; |
| const TARGET_HISTORY_KEY = 'info_radar_attribution_target_history'; |
| const ATTRIBUTION_MODEL_VARIANT_STORAGE_KEY = 'info_radar_attribution_model_variant'; |
|
|
| function readStoredAttributionPageModelVariant(): PredictionAttributeModelVariant { |
| try { |
| const v = localStorage.getItem(ATTRIBUTION_MODEL_VARIANT_STORAGE_KEY); |
| if (v === 'base' || v === 'instruct') return v; |
| } catch { |
| |
| } |
| return 'instruct'; |
| } |
|
|
| const apiPrefix = URLHandler.parameters['api'] || ''; |
| const bodyElement = d3.select('body').node() as Element; |
| const { eventHandler, totalSurprisalFormat, api } = initializeCommonApp(apiPrefix, bodyElement); |
| |
| const apiBaseForRequests = apiPrefix === '' ? '' : String(apiPrefix); |
|
|
| const adminManager = AdminManager.getInstance(); |
| api.setAdminToken(adminManager.isInAdminMode() ? adminManager.getAdminToken() : null); |
|
|
| |
| const contextField = d3.select('#context_text'); |
| const contextCountValue = d3.select('#context_count_value'); |
| const clearContextBtn = d3.select('#clear_context_btn'); |
| const pasteContextBtn = d3.select('#paste_context_btn'); |
| const contextHistoryBtn = document.getElementById('context_history_btn'); |
|
|
| const targetField = d3.select('#target_text'); |
| const targetCountValue = d3.select('#target_count_value'); |
| const clearTargetBtn = d3.select('#clear_target_btn'); |
| const pasteTargetBtn = d3.select('#paste_target_btn'); |
| const targetHistoryBtn = document.getElementById('target_history_btn'); |
|
|
| const analyzeBtn = d3.select('#analyze_btn'); |
| const modelVariantSelect = document.getElementById('attribution_model_variant') as HTMLSelectElement | null; |
| const forceRetryBtn = d3.select('#force_retry_btn'); |
| const loaderSmall = d3.select('.loadersmall'); |
| const resultInfoEl = d3.select('#attribution_result_info'); |
| const useMappingCheckbox = document.getElementById('attribution_use_mapping') as HTMLInputElement | null; |
| const maxScoreRange = document.getElementById('attribution_max_score_range') as HTMLInputElement | null; |
| const maxScoreValueEl = document.getElementById('attribution_max_score_value'); |
| if (modelVariantSelect) { |
| modelVariantSelect.value = readStoredAttributionPageModelVariant(); |
| } |
|
|
| function currentAttributionModelVariant(): PredictionAttributeModelVariant { |
| const v = modelVariantSelect?.value; |
| return v === 'base' || v === 'instruct' ? v : 'instruct'; |
| } |
|
|
| modelVariantSelect?.addEventListener('change', () => { |
| try { |
| localStorage.setItem(ATTRIBUTION_MODEL_VARIANT_STORAGE_KEY, currentAttributionModelVariant()); |
| } catch { |
| |
| } |
| }); |
|
|
| |
| new TextInputController({ |
| textField: contextField, |
| textCountValue: contextCountValue, |
| clearBtn: clearContextBtn, |
| submitBtn: analyzeBtn, |
| saveBtn: d3.select(null), |
| pasteBtn: pasteContextBtn, |
| totalSurprisalFormat, |
| showAlertDialog, |
| }); |
|
|
| new TextInputController({ |
| textField: targetField, |
| textCountValue: targetCountValue, |
| clearBtn: clearTargetBtn, |
| submitBtn: analyzeBtn, |
| saveBtn: d3.select(null), |
| pasteBtn: pasteTargetBtn, |
| totalSurprisalFormat, |
| showAlertDialog, |
| }); |
|
|
| const attributionInspector = createAttributionInspector({ |
| resultsRoot: d3.select('#results'), |
| eventHandler, |
| debugParentId: 'attribution_debug_container', |
| }); |
|
|
| function readAttributionDisplayOptions(): AttributionDisplayOptions { |
| return { |
| colorRangeMax: readAttributionColorRangeMax(), |
| excludePromptPatternsText: readStoredEffectiveExcludePromptPatternsText(), |
| }; |
| } |
|
|
| |
| let analyzeInFlight = false; |
| |
| let lastCommittedInputs: { context: string; target: string } | null = null; |
|
|
| function syncAnalyzeButtonState(): void { |
| const context = (contextField.node() as HTMLTextAreaElement | null)?.value ?? ''; |
| const target = (targetField.node() as HTMLTextAreaElement | null)?.value ?? ''; |
| const idleInputsReady = context.length > 0 && target.length > 0; |
| const hasUncommittedDraft = |
| lastCommittedInputs === null || |
| context !== lastCommittedInputs.context || |
| target !== lastCommittedInputs.target; |
| syncDraftCommittedButtonPair({ |
| primaryBtn: analyzeBtn, |
| forceRetryBtn, |
| inFlight: analyzeInFlight, |
| primaryInFlightMode: 'freeze', |
| primaryIdleLabel: tr('Analyze attribution'), |
| idleInputsReady, |
| hasUncommittedDraft, |
| }); |
| } |
|
|
| function setAnalyzeLoading(loading: boolean): void { |
| analyzeInFlight = loading; |
| loaderSmall.style('display', loading ? null : 'none'); |
| syncAnalyzeButtonState(); |
| } |
|
|
| |
| [contextField, targetField].forEach((field) => { |
| (field.node() as HTMLTextAreaElement | null)?.addEventListener('input', syncAnalyzeButtonState); |
| }); |
| syncAnalyzeButtonState(); |
|
|
| function syncMaxScoreRangeUiEnabled(): void { |
| const on = !!useMappingCheckbox?.checked; |
| if (maxScoreRange) maxScoreRange.disabled = !on; |
| } |
|
|
| function updateMaxScoreValueLabel(): void { |
| if (!maxScoreRange || !maxScoreValueEl) return; |
| const v = Number(maxScoreRange.value); |
| maxScoreValueEl.textContent = Number.isFinite(v) ? v.toFixed(2) : '—'; |
| } |
|
|
| syncMaxScoreRangeUiEnabled(); |
| updateMaxScoreValueLabel(); |
|
|
| useMappingCheckbox?.addEventListener('change', () => { |
| syncMaxScoreRangeUiEnabled(); |
| reapplyAttributionColorsIfPossible(); |
| }); |
|
|
| maxScoreRange?.addEventListener('input', () => { |
| updateMaxScoreValueLabel(); |
| reapplyAttributionColorsIfPossible(); |
| }); |
|
|
| |
| function readAttributionColorRangeMax(): number | null { |
| if (!useMappingCheckbox?.checked) return null; |
| if (!maxScoreRange) return null; |
| const n = Number(maxScoreRange.value); |
| if (!Number.isFinite(n) || n <= 0 || n > 1) return null; |
| return n; |
| } |
|
|
| |
| function applyAttributionResponse(context: string, response: AttributionApiResponse): void { |
| attributionInspector.apply(context, response, readAttributionDisplayOptions()); |
| updateResultInfo(response); |
| lastCommittedInputs = { |
| context: (contextField.node() as HTMLTextAreaElement | null)?.value ?? '', |
| target: (targetField.node() as HTMLTextAreaElement | null)?.value ?? '', |
| }; |
| syncAnalyzeButtonState(); |
| replaceContentUrlParam( |
| buildCachedContentUrlParam(lastCommittedInputs.context, lastCommittedInputs.target), |
| DEFAULT_CONTENT_URL_PARAM, |
| 'attribution' |
| ); |
| } |
|
|
| function reapplyAttributionColorsIfPossible(): void { |
| attributionInspector.reapply(readAttributionDisplayOptions()); |
| } |
|
|
| bindExcludePromptPatternsUi({ |
| textInput: document.getElementById('attribution_exclude_prompt_patterns') as HTMLTextAreaElement | null, |
| enableCheckbox: document.getElementById('attribution_exclude_prompt_patterns_enable') as HTMLInputElement | null, |
| onEffectiveChange: reapplyAttributionColorsIfPossible, |
| }); |
|
|
| function updateResultInfo(response: AttributionApiResponse): void { |
| const n = response.token_attribution?.length ?? 0; |
| const model = response.model ?? '–'; |
| resultInfoEl.classed('is-hidden', false).text(`${trf('{count} tokens', { count: n })}\n${tr('model')}: ${model}`); |
| } |
|
|
| |
| async function runAnalyze(options?: { forceRefresh?: boolean }): Promise<void> { |
| const context = (contextField.node() as HTMLTextAreaElement | null)?.value ?? ''; |
| const target = (targetField.node() as HTMLTextAreaElement | null)?.value ?? ''; |
| if (analyzeInFlight || !context || !target) return; |
|
|
| const forceRefresh = options?.forceRefresh === true; |
|
|
| if (!forceRefresh) { |
| const hit = await takeSuccessfulAttributionFromCache(context, target); |
| if (hit) { |
| applyAttributionResponse(context, hit); |
| saveHistory(context, CONTEXT_HISTORY_KEY); |
| saveHistory(target, TARGET_HISTORY_KEY); |
| return; |
| } |
| } |
|
|
| setAnalyzeLoading(true); |
| try { |
| const json = await loadPredictionAttributeWithCache({ |
| apiBaseForRequests, |
| context, |
| targetPrediction: target, |
| model: currentAttributionModelVariant(), |
| sourcePage: 'attribution.html', |
| forceRefresh, |
| }); |
| applyAttributionResponse(context, json); |
| saveHistory(context, CONTEXT_HISTORY_KEY); |
| saveHistory(target, TARGET_HISTORY_KEY); |
| } catch (err: unknown) { |
| const msg = err instanceof Error ? err.message : String(err); |
| showAlertDialog(tr('Context Attribution'), translateApiErrorMessage(msg)); |
| } finally { |
| setAnalyzeLoading(false); |
| } |
| } |
|
|
| analyzeBtn.on('click', () => void runAnalyze()); |
| forceRetryBtn.on('click', () => void runAnalyze({ forceRefresh: true })); |
|
|
| |
| (contextField.node() as HTMLTextAreaElement | null)?.addEventListener('keydown', (e) => { |
| if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) void runAnalyze(); |
| }); |
|
|
| |
| const contextTextarea = contextField.node() as HTMLTextAreaElement | null; |
| const targetTextarea = targetField.node() as HTMLTextAreaElement | null; |
|
|
| initQueryHistoryDropdown({ |
| input: contextTextarea, |
| dropdownId: 'context_history_dropdown', |
| storageKey: CONTEXT_HISTORY_KEY, |
| openDropdownOnFocusInput: false, |
| filterHistoryByInput: false, |
| onSelect: syncAnalyzeButtonState, |
| historyButton: contextHistoryBtn, |
| applyHistoryOnHover: true, |
| }); |
|
|
| initQueryHistoryDropdown({ |
| input: targetTextarea, |
| dropdownId: 'target_history_dropdown', |
| storageKey: TARGET_HISTORY_KEY, |
| openDropdownOnFocusInput: false, |
| filterHistoryByInput: false, |
| onSelect: syncAnalyzeButtonState, |
| historyButton: targetHistoryBtn, |
| applyHistoryOnHover: true, |
| }); |
|
|
| async function restoreAttributionFromCachedEntry( |
| entry: AttributionCachedEntry, |
| options: { shouldTouch: boolean; ctx?: CachedHistorySelectContext; contentKey: string } |
| ): Promise<void> { |
| try { |
| contextField.property('value', entry.context); |
| targetField.property('value', entry.targetPrediction); |
| contextTextarea?.dispatchEvent(new Event('input', { bubbles: true })); |
| targetTextarea?.dispatchEvent(new Event('input', { bubbles: true })); |
| syncAnalyzeButtonState(); |
| applyAttributionResponse(entry.context, entry.response); |
| if (options.shouldTouch && options.ctx) { |
| await touchCachedEntryByContentKey(options.contentKey); |
| await options.ctx.refreshList(); |
| } |
| } catch (e: unknown) { |
| const msg = e instanceof Error ? e.message : String(e); |
| showToast(translateApiErrorMessage(msg), 'error'); |
| } |
| } |
|
|
| |
| const cachedHistoryBtn = document.getElementById('attribution_cached_history_btn'); |
| void initCachedHistoryQueryDropdown({ |
| dropdownId: 'attribution_cached_history_dropdown', |
| historyButton: cachedHistoryBtn, |
| clickOutsideRoot: document.getElementById('attribution_cached_history_dropdown'), |
| listMru: listCachedHistoryRows, |
| onSelectEntry: async (contentKey, shouldTouch, ctx) => { |
| const entry = await getCachedEntryByContentKey(contentKey); |
| if (!entry) { |
| showToast(tr('Cached result not found'), 'error'); |
| return; |
| } |
| await restoreAttributionFromCachedEntry(entry, { |
| shouldTouch: Boolean(shouldTouch), |
| ctx, |
| contentKey, |
| }); |
| }, |
| onRemove: removeCachedEntryByContentKey, |
| onPromote: touchCachedEntryByContentKey, |
| }); |
|
|
| void runContentUrlHydrate({ |
| readRaw: readContentUrlParam, |
| fetchEntry: getCachedEntryByContentKey, |
| apply: async (entry, rawContentKey) => { |
| await restoreAttributionFromCachedEntry(entry, { shouldTouch: false, contentKey: rawContentKey }); |
| }, |
| onMissing: async () => { |
| showToast(tr('Cached result not found (link may be expired)'), 'error'); |
| replaceContentUrlParam(null, DEFAULT_CONTENT_URL_PARAM, 'attribution'); |
| }, |
| onApplyError: (e: unknown) => { |
| const msg = e instanceof Error ? e.message : String(e); |
| showToast(translateApiErrorMessage(msg), 'error'); |
| replaceContentUrlParam(null, DEFAULT_CONTENT_URL_PARAM, 'attribution'); |
| }, |
| }); |
|
|
| initChatPanelLayout({ storageKey: PANEL_SPLIT_STORAGE_KEY_ATTRIBUTION }); |
|
|
| const themeManager = initThemeManager( |
| { |
| onThemeChange: () => { |
| reapplyAttributionColorsIfPossible(); |
| }, |
| }, |
| '#theme_dropdown' |
| ); |
|
|
| const languageManager = initLanguageManager({}, '#language_dropdown'); |
|
|
| void new SettingsMenuManager( |
| '#settings_btn', |
| '#settings_menu', |
| '#admin_mode_btn', |
| adminManager, |
| api, |
| undefined, |
| undefined, |
| themeManager, |
| languageManager, |
| 'common' |
| ); |
|
|