| import * as d3 from 'd3'; |
| import "./utils/d3-polyfill"; |
|
|
| import '../css/start.scss' |
| import {SimpleEventHandler} from "./utils/SimpleEventHandler"; |
| import {TextAnalysisAPI} from "./api/GLTR_API"; |
| import type {AnalyzeResponse, FrontendAnalyzeResult, FrontendToken} from "./api/GLTR_API"; |
| import {GLTR_HoverEvent, GLTR_Mode, GLTR_Text_Box} from "./vis/GLTR_Text_Box"; |
| import {ToolTip} from "./vis/ToolTip"; |
| import URLHandler from "./utils/URLHandler"; |
| import {Histogram, HistogramBinClickEvent} from './vis/Histogram'; |
| import {ScatterPlot, type ScatterChunkClickEvent} from './vis/ScatterPlot'; |
| import {initThemeManager} from './ui/theme'; |
| import {initLanguageManager} from './ui/language'; |
| import {createToast} from './ui/toast'; |
| import {initDemoManager, type DemoManager} from './ui/demoManager'; |
| import {showAlertDialog, showDialog, createCombinedContent, createNamePathTextContent, createUrlInputContent} from './ui/dialog'; |
| |
| import {tr, initI18n, toggleLanguage, getCurrentLanguage} from './lang/i18n-lite'; |
| import {loadHomeContent} from './lang/contentLoader'; |
| |
| import { ServerStorage } from './storage/demoStorage'; |
| import { DemoStorageController } from './controllers/demoStorageController'; |
| import { LocalFileIO } from './storage/localFileIO'; |
| import { LocalDemoCache } from './storage/localDemoCache'; |
| import { DemoResourceLoader } from './storage/demoResourceLoader'; |
| |
| import {TextInputController, calculateTextStatsForController, type ExtendedInputEvent} from './controllers/textInputController'; |
| import {HighlightController, initHighlightClearListeners} from './controllers/highlightController'; |
| import {LayoutController} from './controllers/layoutController'; |
| import {PANEL_SPLIT_STORAGE_KEY_START} from './utils/panelSplitStorage'; |
| import {handleServerDemoSave} from './controllers/serverDemoController'; |
| |
| import {initializeCommonApp} from './appInitializer'; |
| |
| import {ensureJsonExtension} from './utils/localFileUtils'; |
| import {extractErrorMessage} from './utils/errorUtils'; |
| import {CryptoSubtleUnavailableError} from './utils/hashUtils'; |
| import type { TextStats } from './utils/textStatistics'; |
| import {composeDemoFullPath, getDefaultDemoName, normalizeFolderPath, buildFolderOptions} from './utils/demoPathUtils'; |
| |
| import { AppStateManager } from './utils/appStateManager'; |
| import { DemoBusinessLogic } from './utils/demoBusinessLogic'; |
| import { VisualizationUpdater } from './utils/visualizationUpdater'; |
| import { addDigitsMergeRenderListener } from './utils/digitsMergeManager'; |
| import { AnalyzeFlowManager } from './utils/analyzeFlow'; |
| import { isMobileDevice } from './utils/responsive'; |
| import { isValidUrl, extractUrl, isPureUrl } from './utils/urlUtils'; |
| import { AdminManager } from './utils/adminManager'; |
| import { SettingsMenuManager } from './utils/settingsMenuManager'; |
| import { saveHistory, initQueryHistoryDropdown } from './utils/queryHistory'; |
| import { removeByQuery as removeSemanticCacheByQuery } from './utils/semanticResultCache'; |
| import { playAnalysisCompleteSound } from './utils/soundNotification'; |
| import { getSemanticMatchThreshold, setSemanticMatchThreshold } from './utils/semanticThresholdManager'; |
| import { SEMANTIC_MATCH_THRESHOLD } from './constants'; |
| import { SemanticSearchController } from './controllers/semanticSearchController'; |
| import { initDensityAttributionSidebar } from './attribution/densityAttributionSidebar'; |
|
|
| const current = { |
| sidebar: { |
| width: 400, |
| visible: false |
| }, |
| demo: true, |
| model_name: 'default' |
| }; |
|
|
| |
|
|
|
|
| const mapIDtoEnum = { |
| mode_frac_p: GLTR_Mode.fract_p |
| }; |
|
|
|
|
| window.onload = () => { |
| |
| const api_prefix = URLHandler.parameters['api'] || ''; |
| const bodyElement = <Element>d3.select('body').node(); |
| const { eventHandler, api, tokenSurprisalColorScale, byteSurprisalColorScale, totalSurprisalFormat } = initializeCommonApp(api_prefix, bodyElement); |
|
|
| |
| const adminManager = AdminManager.getInstance(); |
| api.setAdminToken(adminManager.isInAdminMode() ? adminManager.getAdminToken() : null); |
|
|
| |
| d3.selectAll(".loadersmall").style('display', 'none'); |
|
|
| if (URLHandler.parameters['nodemo']){ |
| current.demo = false; |
| } |
|
|
| const toastController = createToast('#toast'); |
| const showToast = toastController.show; |
|
|
| const side_bar = d3.select(".side_bar"); |
| side_bar.style('width', `${current.sidebar.width}px`); |
|
|
| const toolTip = new ToolTip(d3.select('#major_tooltip'), eventHandler); |
|
|
| const submitBtn = d3.select('#submit_text_btn'); |
| const saveBtn = d3.select('#save_demo_btn'); |
| const saveLocalBtn = d3.select('#save_local_demo_btn'); |
| const semanticSearchBtn = d3.select('#semantic_search_btn'); |
| const clearBtn = d3.select('#clear_text_btn'); |
| const pasteBtn = d3.select('#paste_text_btn'); |
| const loadUrlBtn = d3.select('#load_url_btn'); |
| const analyzeSaveBtn = d3.select('#analyze_save_btn'); |
| const textField = d3.select('#test_text'); |
| const textCountValue = d3.select('#text_count_value'); |
| const textMetrics = d3.select('#text_metrics'); |
| const metricBytes = d3.select('#metric_bytes'); |
| const metricChars = d3.select('#metric_chars'); |
| const metricTokens = d3.select('#metric_tokens'); |
| const metricTotalSurprisal = d3.select('#metric_total_surprisal'); |
| const metricModel = d3.select('#metric_model'); |
|
|
| |
| const defaultNoFileLabel = (() => { |
| const el = document.getElementById('open_local_demo_filename'); |
| return el ? el.textContent?.trim().replace(/\s+/g, ' ') : 'No file selected'; |
| })(); |
|
|
| |
| initI18n(); |
| |
| const isZh = getCurrentLanguage() === 'zh'; |
| document.documentElement.lang = isZh ? 'zh-CN' : 'en'; |
| if (isZh) { |
| const metaDesc = document.querySelector('meta[name="description"]'); |
| if (metaDesc) { |
| const content = metaDesc.getAttribute('content'); |
| if (content) metaDesc.setAttribute('content', tr(content)); |
| } |
| } |
| |
| loadHomeContent('home-intro-content'); |
|
|
| |
| const storedMinimap = localStorage.getItem('minimap_enabled'); |
| let enableMinimap: boolean = storedMinimap !== null |
| ? storedMinimap === '1' |
| : !isMobileDevice(); |
|
|
| if (!textMetrics.empty()) { |
| textMetrics.style('display', null).classed('is-hidden', true); |
| } |
|
|
| |
| const textInputController = new TextInputController({ |
| textField, |
| textCountValue, |
| textMetrics, |
| metricBytes, |
| metricChars, |
| metricTokens, |
| metricTotalSurprisal, |
| metricModel, |
| clearBtn, |
| submitBtn, |
| saveBtn, |
| pasteBtn, |
| totalSurprisalFormat, |
| showAlertDialog |
| }); |
|
|
| const stats_frac = new Histogram(d3.select('#stats_frac'), eventHandler, { |
| width: 400, |
| height: 200 |
| }); |
| const stats_raw_score_normed = new Histogram(d3.select('#stats_raw_score_normed'), eventHandler, { |
| width: 400, |
| height: 200 |
| }); |
| const stats_surprisal_progress = new ScatterPlot(d3.select('#stats_surprisal_progress'), eventHandler, { |
| width: 400, |
| height: 200 |
| }); |
| const stats_match_score_progress = new ScatterPlot(d3.select('#stats_match_score_progress'), eventHandler, { |
| width: 400, |
| height: 200 |
| }); |
|
|
| |
| const appStateManager = new AppStateManager({ |
| submitBtn: submitBtn as d3.Selection<HTMLElement, unknown, HTMLElement, unknown>, |
| saveBtn: saveBtn as d3.Selection<HTMLElement, unknown, HTMLElement, unknown>, |
| saveLocalBtn: saveLocalBtn as d3.Selection<HTMLElement, unknown, HTMLElement, unknown>, |
| textField: textField as d3.Selection<HTMLElement, unknown, HTMLElement, unknown>, |
| textMetrics: textMetrics as d3.Selection<HTMLElement, unknown, HTMLElement, unknown>, |
| semanticSearchBtn: semanticSearchBtn as d3.Selection<HTMLElement, unknown, HTMLElement, unknown>, |
| getSemanticSearchQuery: () => (document.getElementById('semantic_search_input') as HTMLInputElement | null)?.value ?? '', |
| tr |
| }); |
|
|
| |
| const lmf = new GLTR_Text_Box(d3.select("#results"), eventHandler); |
|
|
| |
| lmf.updateOptions({ |
| gltrMode: GLTR_Mode.fract_p, |
| enableMinimap: false |
| }, true); |
|
|
| |
| const highlightController = new HighlightController({ |
| stats_frac, |
| stats_raw_score_normed, |
| stats_match_score_progress, |
| lmf, |
| currentData: null |
| }); |
| const clearHighlights = () => highlightController.clearHighlights(); |
| |
| |
| initHighlightClearListeners(clearHighlights); |
|
|
| |
| const visualizationUpdater = new VisualizationUpdater({ |
| lmf, |
| highlightController, |
| textInputController, |
| stats_frac, |
| stats_raw_score_normed, |
| stats_surprisal_progress, |
| stats_match_score_progress, |
| appStateManager, |
| surprisalColorScale: tokenSurprisalColorScale as d3.ScaleSequential<string> |
| }); |
|
|
| addDigitsMergeRenderListener(() => { |
| visualizationUpdater.applyDigitsMergeSetting(); |
| }); |
|
|
| |
| const themeManager = initThemeManager({ |
| onThemeChange: () => { |
| visualizationUpdater.rerenderOnThemeChange(); |
| } |
| }, '#theme_dropdown'); |
|
|
| |
| const languageManager = initLanguageManager({ |
| onLanguageChange: () => { |
| |
| } |
| }, '#language_dropdown'); |
|
|
| |
| const SEMANTIC_KEYS = { |
| submode: 'info_radar_semantic_submode', |
| chunked: 'info_radar_semantic_chunked', |
| colorSource: 'info_radar_semantic_color_source', |
| threshold: 'info_radar_semantic_match_threshold' |
| } as const; |
| const initSemanticOptions = () => { |
| const validSubmodes = ['count', 'fill_blank', 'hybrid']; |
| const validColorSources = ['raw_score_normed', 'signal_probability', 'pw_score']; |
| const query = URLHandler.parameters['semantic_query'] ?? ''; |
| const submode = localStorage.getItem(SEMANTIC_KEYS.submode) ?? 'hybrid'; |
| const chunked = localStorage.getItem(SEMANTIC_KEYS.chunked) !== '0'; |
| const colorSource = localStorage.getItem(SEMANTIC_KEYS.colorSource) ?? 'pw_score'; |
| const queryEl = document.getElementById('semantic_search_input') as HTMLInputElement | null; |
| if (queryEl) queryEl.value = typeof query === 'string' ? query : ''; |
| const submodeEl = document.getElementById('semantic_submode_select') as HTMLSelectElement | null; |
| if (submodeEl && validSubmodes.includes(submode)) submodeEl.value = submode; |
| const chunkedEl = document.getElementById('semantic_chunked_mode') as HTMLInputElement | null; |
| if (chunkedEl) chunkedEl.checked = chunked; |
| const colorEl = document.getElementById('semantic_color_source_select') as HTMLSelectElement | null; |
| if (colorEl && validColorSources.includes(colorSource)) colorEl.value = colorSource; |
| const thresholdEl = document.getElementById('semantic_threshold_input') as HTMLInputElement | null; |
| if (thresholdEl) thresholdEl.value = String(getSemanticMatchThreshold()); |
| }; |
| const syncSemanticOptionsToStorage = () => { |
| const submodeEl = document.getElementById('semantic_submode_select') as HTMLSelectElement | null; |
| const chunkedEl = document.getElementById('semantic_chunked_mode') as HTMLInputElement | null; |
| const colorEl = document.getElementById('semantic_color_source_select') as HTMLSelectElement | null; |
| const thresholdEl = document.getElementById('semantic_threshold_input') as HTMLInputElement | null; |
| localStorage.setItem(SEMANTIC_KEYS.submode, submodeEl?.value ?? 'hybrid'); |
| if (chunkedEl) localStorage.setItem(SEMANTIC_KEYS.chunked, chunkedEl.checked ? '1' : '0'); |
| if (colorEl) localStorage.setItem(SEMANTIC_KEYS.colorSource, colorEl.value); |
| if (thresholdEl) { |
| const v = parseFloat(thresholdEl.value); |
| if (Number.isFinite(v)) { |
| setSemanticMatchThreshold(v); |
| thresholdEl.value = String(getSemanticMatchThreshold()); |
| } |
| } |
| }; |
| const syncSemanticQueryToUrl = () => { |
| const queryEl = document.getElementById('semantic_search_input') as HTMLInputElement | null; |
| const query = queryEl?.value ?? ''; |
| const params = URLHandler.parameters; |
| if (query) params['semantic_query'] = query; |
| else delete params['semantic_query']; |
| URLHandler.updateUrl(params, false); |
| }; |
|
|
| |
| const settingsMenuManager = new SettingsMenuManager( |
| '#settings_btn', |
| '#settings_menu', |
| '#admin_mode_btn', |
| adminManager, |
| api, |
| () => { |
| |
| const isAdmin = adminManager.isInAdminMode(); |
| analyzeSaveBtn.style('display', isAdmin ? null : 'none'); |
| saveBtn.style('display', isAdmin ? null : 'none'); |
| }, |
| { |
| onMinimapToggle: (enabled: boolean) => { |
| enableMinimap = enabled; |
| lmf.updateOptions({ |
| enableMinimap: enableMinimap |
| }, false); |
| localStorage.setItem('minimap_enabled', enableMinimap ? '1' : '0'); |
| }, |
| onSemanticAnalysisToggle: (_enabled: boolean) => { |
| |
| const queryEl = document.getElementById('semantic_search_input') as HTMLInputElement | null; |
| if (queryEl) queryEl.value = ''; |
| const submodeEl = document.getElementById('semantic_submode_select') as HTMLSelectElement | null; |
| if (submodeEl) submodeEl.value = 'hybrid'; |
| const chunkedEl = document.getElementById('semantic_chunked_mode') as HTMLInputElement | null; |
| if (chunkedEl) chunkedEl.checked = true; |
| const colorEl = document.getElementById('semantic_color_source_select') as HTMLSelectElement | null; |
| if (colorEl) colorEl.value = 'pw_score'; |
| setSemanticMatchThreshold(SEMANTIC_MATCH_THRESHOLD); |
| const thresholdEl = document.getElementById('semantic_threshold_input') as HTMLInputElement | null; |
| if (thresholdEl) thresholdEl.value = String(SEMANTIC_MATCH_THRESHOLD); |
| const params = URLHandler.parameters; |
| delete params['semantic_query']; |
| URLHandler.updateUrl(params, false); |
| syncSemanticOptionsToStorage(); |
| appStateManager.setLastSearchedQuery(null); |
| visualizationUpdater.clearSemanticState(); |
| visualizationUpdater.syncSemanticUiFromConfig(); |
| }, |
| }, |
| themeManager, |
| languageManager |
| ); |
|
|
| |
| const compareLinkEl = document.querySelector<HTMLElement>('.compare-link'); |
| if (compareLinkEl) { |
| compareLinkEl.style.display = adminManager.isInAdminMode() ? null : 'none'; |
| } |
|
|
| |
| settingsMenuManager.setMinimapEnabled(enableMinimap); |
| lmf.updateOptions({ |
| enableMinimap: enableMinimap |
| }, false); |
|
|
| |
| visualizationUpdater.syncSemanticUiFromConfig(); |
|
|
| initSemanticOptions(); |
|
|
| |
| |
| |
|
|
| const startSystem = () => { |
| d3.select('#model_name').text(current.model_name); |
| |
| } |
|
|
| let hasStarted = false; |
| const ensureSystemStarted = () => { |
| if (!hasStarted) { |
| startSystem(); |
| hasStarted = true; |
| } |
| }; |
|
|
| |
| const demoResourceLoader = new DemoResourceLoader(api); |
| const localFileIO = new LocalFileIO(); |
| const localDemoCache = demoResourceLoader.getLocalDemoCache(); |
| |
| |
| const serverStorage = demoResourceLoader.getServerStorage(); |
|
|
| |
| const openLocalFilename = d3.select('#open_local_demo_filename'); |
| const updateFileNameDisplay = (filename: string | null) => { |
| openLocalFilename.text(filename || tr(defaultNoFileLabel)); |
| }; |
|
|
| |
| const demoBusinessLogic = new DemoBusinessLogic({ |
| textInputController, |
| demoManager: null, |
| localDemoCache, |
| updateFromRequest: (data, disableAnimation, options) => |
| visualizationUpdater.updateFromRequest(data, disableAnimation, options), |
| updateAppState: (updates) => appStateManager.updateState(updates), |
| ensureSystemStarted, |
| updateFileNameDisplay |
| }); |
|
|
| |
| const analyzeFlowManager = new AnalyzeFlowManager({ |
| api, |
| textInputController, |
| demoManager: null, |
| appStateManager, |
| visualizationUpdater, |
| demoBusinessLogic, |
| serverStorage, |
| lmf, |
| modelName: current.model_name, |
| enableDemo: current.demo, |
| showToast, |
| updateFileNameDisplay |
| }); |
|
|
| let demoManager: DemoManager | null = null; |
| let hasProcessedUrlDemo = false; |
| const LAST_SAVE_PATH_KEY = 'lastSaveDemoPath'; |
| let cryptoSubtleHintShown = false; |
|
|
| |
| if (!LocalDemoCache.isAvailable()) { |
| console.warn('IndexedDB 不可用,本地缓存功能将受限'); |
| |
| showAlertDialog(tr('Info'), |
| tr('Browser does not support IndexedDB, the following features will not be available:') + '\n\n' + |
| tr('Local file cache (unable to cache local files to browser after opening)') + '\n' + |
| tr('Restore local files after refresh (need to reselect files after refreshing the page)') + '\n\n' + |
| tr('Other features (text analysis, server save, local file download, etc.) are still available.') |
| ); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| const handleLoadFailure = (urlDemoPath: string | undefined, message: string, silent?: boolean): void => { |
| demoBusinessLogic.clearDemoUrlParam(); |
| if (urlDemoPath && DemoResourceLoader.isLocalResource(urlDemoPath)) { |
| updateFileNameDisplay(null); |
| } |
| if (!silent) { |
| showAlertDialog(tr('Error'), tr(message)); |
| } |
| }; |
|
|
| |
| |
| |
| |
| const handleLocalDemoSave = async ( |
| data: AnalyzeResponse, |
| currentFilename?: string, |
| textValue?: string |
| ): Promise<void> => { |
| |
| const defaultName = getDefaultDemoName(data, textValue || '', currentFilename); |
| const filename = ensureJsonExtension(defaultName); |
|
|
| appStateManager.setGlobalLoading(true); |
| appStateManager.updateState({ isSaving: true }); |
|
|
| try { |
| |
| |
| |
| const exportSuccess = await localFileIO.export(data, filename); |
|
|
| if (!exportSuccess) { |
| showAlertDialog(tr('Error'), tr('File download failed')); |
| return; |
| } |
|
|
| |
| appStateManager.updateState({ isSavedToLocal: true }); |
|
|
| |
| } catch (error) { |
| const message = error instanceof Error ? error.message : tr('Save failed'); |
| showAlertDialog(tr('Error'), message); |
| } finally { |
| appStateManager.setGlobalLoading(false); |
| appStateManager.updateState({ isSaving: false }); |
| } |
| }; |
|
|
| |
| const openLocalBtn = d3.select('#open_local_demo_btn'); |
| const openLocalInput = d3.select('#open_local_demo_input'); |
| |
| |
| openLocalBtn.on('click', async () => { |
| appStateManager.setGlobalLoading(true); |
| try { |
| |
| const result = await localFileIO.import(); |
| |
| if (result.success && result.data && result.filename) { |
| try { |
| |
| |
| const saveResult = await localDemoCache.save(result.data, { name: result.filename }); |
| if (!saveResult.success || !saveResult.hash) { |
| throw new Error(tr('Failed to save to cache') + ': ' + (saveResult.message || tr('Hash value missing'))); |
| } |
| |
| |
| const identifier = DemoResourceLoader.createLocalIdentifier(result.filename, saveResult.hash); |
| |
| |
| URLHandler.updateURLParam('demo', identifier, false); |
| |
| |
| const loadResult = await demoResourceLoader.load(identifier); |
| if (loadResult.success && loadResult.data) { |
| |
| const localInfo = DemoResourceLoader.extractLocalInfo(identifier); |
| demoBusinessLogic.renderDemo(loadResult.data, 'local', localInfo.filename, { |
| disableAnimation: true, |
| isNewDemo: true |
| }); |
| |
| } else { |
| throw new Error(loadResult.message || 'Load failed'); |
| } |
| } catch (cacheError) { |
| |
| if (cacheError instanceof CryptoSubtleUnavailableError) { |
| |
| demoBusinessLogic.renderDemo(result.data, 'local', result.filename, { |
| disableAnimation: true, |
| isNewDemo: true |
| }); |
| |
| if (!cryptoSubtleHintShown) { |
| |
| cryptoSubtleHintShown = true; |
| |
| const hintMessage = tr('File opened, but cannot be saved to local cache due to browser security policy restrictions.') + '\n\n' + |
| '✅ ' + tr('Only refresh recovery of opened files is affected, other features work normally.') + '\n\n' + |
| cacheError.message; |
| showAlertDialog(tr('Info'), hintMessage); |
| } |
| } else { |
| |
| throw cacheError; |
| } |
| } |
| } else if (result.message && !result.cancelled) { |
| |
| showAlertDialog(tr('Error'), tr(result.message)); |
| } |
| } catch (error) { |
| const message = error instanceof Error ? error.message : 'Failed to open file'; |
| showAlertDialog(tr('Error'), tr(message)); |
| } finally { |
| appStateManager.setGlobalLoading(false); |
| } |
| }); |
|
|
| if (current.demo) { |
| demoManager = initDemoManager({ |
| api, |
| enableDemo: true, |
| containerSelector: '.demos', |
| loaderSelector: '#demos_loading', |
| refreshSelector: '#refresh_demo_btn', |
| |
| disableFolderOperations: !adminManager.isInAdminMode(), |
| onDemoLoaded: (data, disableAnimation, isNewDemo = false, path?: string) => { |
| |
| demoBusinessLogic.renderDemo(data, 'server', path, { disableAnimation, isNewDemo }); |
| }, |
| onTextPrefill: (text) => { |
| textInputController.setTextValue(text); |
| }, |
| onDemoLoading: (loading) => { |
| |
| |
| |
| appStateManager.setGlobalLoading(loading); |
| }, |
| onRefreshEnd: async () => { |
| ensureSystemStarted(); |
| |
| |
| if (!hasProcessedUrlDemo) { |
| hasProcessedUrlDemo = true; |
| const paramDemo = URLHandler.parameters['demo']; |
| const urlDemoPath = (paramDemo && typeof paramDemo === 'string') ? paramDemo : '/quick-start-1.json'; |
| if (urlDemoPath) { |
| appStateManager.setGlobalLoading(true); |
| try { |
| |
| if (DemoResourceLoader.isLocalResource(urlDemoPath)) { |
| |
| const result = await demoResourceLoader.load(urlDemoPath); |
| if (result.success && result.data) { |
| try { |
| const localInfo = DemoResourceLoader.extractLocalInfo(urlDemoPath); |
| demoBusinessLogic.renderDemo(result.data, 'local', localInfo.filename, { |
| disableAnimation: true, |
| isNewDemo: true |
| }); |
| if (!paramDemo) { |
| URLHandler.updateURLParam('demo', '/quick-start-1.json', false); |
| } |
| } catch (error) { |
| const errorMessage = extractErrorMessage(error, tr('Invalid URL format')); |
| console.error('解析本地资源标识符失败:', error); |
| handleLoadFailure(urlDemoPath, errorMessage); |
| } |
| } else { |
| handleLoadFailure(urlDemoPath, result.message || tr('Load failed')); |
| } |
| } else { |
| |
| const result = await demoResourceLoader.load(urlDemoPath); |
| if (result.success && result.data) { |
| demoBusinessLogic.renderDemo(result.data, 'server', urlDemoPath, { |
| disableAnimation: true, |
| isNewDemo: true |
| }); |
| if (!paramDemo) { |
| URLHandler.updateURLParam('demo', '/quick-start-1.json', false); |
| } |
| |
| if (demoManager) { |
| await demoManager.navigateToDemoAndHighlight(urlDemoPath); |
| } |
| } else { |
| const msg = result.message || tr('Load failed'); |
| handleLoadFailure(urlDemoPath, msg, msg.startsWith('404')); |
| } |
| } |
| } catch (error) { |
| const errorMessage = extractErrorMessage(error, tr('Failed to restore')); |
| console.error('从URL恢复demo失败:', error); |
| handleLoadFailure(urlDemoPath, errorMessage); |
| } finally { |
| appStateManager.setGlobalLoading(false); |
| } |
| } |
| } |
| }, |
| }); |
| |
| |
| demoBusinessLogic.setDemoManager(demoManager); |
| analyzeFlowManager.setDemoManager(demoManager); |
| } else { |
| |
| d3.selectAll('.demo').remove(); |
| ensureSystemStarted(); |
| } |
|
|
|
|
|
|
| |
| |
| |
| const textFieldNode = textField.node() as HTMLTextAreaElement | null; |
| if (textFieldNode) { |
| textFieldNode.addEventListener('input', (event: Event) => { |
| |
| const isMatchingAnalysis = (event as ExtendedInputEvent).isMatchingAnalysis === true; |
| |
| if (!isMatchingAnalysis) { |
| |
| visualizationUpdater.clearDataOnTextChange(); |
| appStateManager.updateState({ |
| hasValidData: false, |
| dataSource: null, |
| isSavedToLocal: false, |
| isSavedToServer: false |
| }); |
| } |
| |
| |
| |
| |
| |
| }); |
| } |
| |
| appStateManager.updateButtonStates(); |
|
|
| |
| |
| |
| const openAnalyzeSaveDialog = async (prefillText: string) => { |
| let folders: string[] = ['/']; |
| try { |
| const result = await api.list_all_folders(); |
| folders = Array.isArray(result?.folders) ? result.folders : ['/']; |
| } catch (error) { |
| const message = error instanceof Error ? error.message : tr('Failed to load folder list'); |
| showAlertDialog(tr('Error'), `${tr('Failed to load folder list')}:${message}`); |
| return; |
| } |
|
|
| const lastPath = localStorage.getItem(LAST_SAVE_PATH_KEY); |
| const { options: folderOptions, defaultPath } = buildFolderOptions(folders, lastPath); |
| const defaultName = getDefaultDemoName(null, prefillText); |
|
|
| const { setConfirmButtonState } = showDialog({ |
| title: tr('Analyze & Upload'), |
| content: createNamePathTextContent( |
| tr('Demo name:'), |
| defaultName, |
| tr('Save directory:'), |
| folderOptions, |
| defaultPath, |
| tr('Text content:'), |
| prefillText |
| ), |
| onConfirm: (value: { input: string; select: string; text: string }): boolean => { |
| const name = (value?.input || '').trim(); |
| const path = normalizeFolderPath(value?.select || '/'); |
| const text = value?.text ?? ''; |
| |
| |
| if (appStateManager.getIsAnalyzing()) { |
| |
| setConfirmButtonState(false, true); |
| |
| |
| const checkInterval = setInterval(() => { |
| if (!appStateManager.getIsAnalyzing()) { |
| |
| clearInterval(checkInterval); |
| |
| |
| setConfirmButtonState(false, false); |
| |
| |
| setTimeout(() => { |
| |
| const overlay = d3.select('.dialog-overlay'); |
| if (!overlay.empty()) { |
| overlay.remove(); |
| } |
| |
| |
| void analyzeFlowManager.runAnalyzeAndUpload({ name, path, text }); |
| }, 100); |
| } |
| }, 200); |
| |
| return false; |
| } |
| |
| |
| setConfirmButtonState(false); |
| void analyzeFlowManager.runAnalyzeAndUpload({ name, path, text }); |
| return true; |
| }, |
| onCancel: () => {}, |
| confirmText: tr('Confirm'), |
| cancelText: tr('Cancel'), |
| |
| |
| width: 'clamp(300px, 90vw, 600px)' |
| }); |
| }; |
|
|
| submitBtn.on('click', () => { |
| const t = textInputController.getTextValue(); |
| if (t.length === 0) { |
| return; |
| } |
| |
| |
| void analyzeFlowManager.runAnalyze(t, true); |
| }); |
|
|
| |
| |
| |
| const openLoadUrlDialog = async () => { |
| |
| let clipboardText = ''; |
| try { |
| clipboardText = await navigator.clipboard.readText(); |
| } catch (error) { |
| |
| clipboardText = ''; |
| } |
| |
| |
| let defaultUrl = ''; |
| if (clipboardText) { |
| if (isPureUrl(clipboardText)) { |
| defaultUrl = clipboardText.trim(); |
| } else { |
| const extractedUrl = extractUrl(clipboardText); |
| if (extractedUrl) { |
| defaultUrl = extractedUrl; |
| } |
| } |
| } |
| |
| |
| const { setConfirmButtonState } = showDialog({ |
| title: tr('Analyze URL content'), |
| content: createUrlInputContent(tr('URL address:'), defaultUrl, 'https://example.com'), |
| onConfirm: async (url: string) => { |
| if (!url) { |
| return true; |
| } |
| |
| setConfirmButtonState(false, true); |
| appStateManager.setGlobalLoading(true); |
| |
| try { |
| const result = await api.fetchUrlText(url); |
| |
| if (result.success && result.text) { |
| textInputController.setTextValue(result.text); |
| |
| |
| (submitBtn.node() as HTMLButtonElement)?.click(); |
| } else { |
| showAlertDialog(tr('Load failed'), tr(result.message || 'Unable to extract text from URL')); |
| } |
| } catch (error) { |
| const errorMessage = extractErrorMessage(error, tr('URL text extraction failed')); |
| showAlertDialog(tr('Load failed'), errorMessage); |
| console.error('URL 文本提取失败:', error); |
| } finally { |
| appStateManager.setGlobalLoading(false); |
| } |
| |
| return true; |
| }, |
| onCancel: () => {}, |
| confirmText: tr('Analyze'), |
| cancelText: tr('Cancel'), |
| loadingConfirmText: tr('Loading...'), |
| width: 'clamp(300px, 90vw, 500px)' |
| }); |
| }; |
|
|
| |
| loadUrlBtn.on('click', async () => { |
| await openLoadUrlDialog(); |
| }); |
|
|
| |
| const semanticSearchInput = document.getElementById('semantic_search_input') as HTMLInputElement | null; |
| const getSubmode = () => |
| (document.getElementById('semantic_submode_select') as HTMLSelectElement | null)?.value || undefined; |
| const showSemanticError = (message?: string) => { |
| d3.select('#semantic_match_degree').style('display', 'none'); |
| showToast(message || tr('Semantic analysis failed'), 'error'); |
| lmf.hideLoading(); |
| visualizationUpdater.rerenderHistograms(); |
| }; |
| const finishSemanticSearch = (query: string, matchDegree: number | null, fromCache: boolean) => { |
| appStateManager.setLastSearchedQuery(query); |
| syncSemanticQueryToUrl(); |
| syncSemanticOptionsToStorage(); |
| if (!fromCache) playAnalysisCompleteSound(); |
| const mdEl = d3.select('#semantic_match_degree'); |
| if (matchDegree !== null) { |
| mdEl.text(tr('Match: {0}%').replace('{0}', (matchDegree * 100).toFixed(1))) |
| .style('display', 'inline-block') |
| .style('color', matchDegree < getSemanticMatchThreshold() ? 'var(--error-color, #e74c3c)' : null); |
| } else { |
| mdEl.style('display', 'none'); |
| } |
| }; |
| const semanticSearchController = new SemanticSearchController({ |
| getQuery: () => semanticSearchInput?.value ?? '', |
| getText: () => (textField.property('value') ?? visualizationUpdater.getCurrentData()?.request?.text ?? '').toString(), |
| getSubmode, |
| isChunkedMode: () => (document.getElementById('semantic_chunked_mode') as HTMLInputElement | null)?.checked ?? true, |
| api, |
| appStateManager, |
| visualizationUpdater, |
| lmf, |
| showToast, |
| showSemanticError, |
| onSearchStart: (query) => saveHistory(query), |
| finishSemanticSearch, |
| tr, |
| extractErrorMessage, |
| }); |
| const runSemanticSearchOrChunked = () => semanticSearchController.run(); |
| const onSemanticBtnClick = () => { |
| if (appStateManager.getState().isSemanticSearching) { |
| semanticSearchController.abort(); |
| } else { |
| runSemanticSearchOrChunked(); |
| } |
| }; |
|
|
| semanticSearchBtn.on('click', onSemanticBtnClick); |
| semanticSearchInput?.addEventListener('keydown', (e) => { |
| if (e.key === 'Enter' && !e.isComposing) onSemanticBtnClick(); |
| }); |
| initQueryHistoryDropdown({ |
| input: semanticSearchInput, |
| dropdownId: 'semantic_search_history_dropdown', |
| onSelect: () => appStateManager.updateButtonStates(), |
| onHistorySelect: runSemanticSearchOrChunked, |
| onRemove: removeSemanticCacheByQuery |
| }); |
| semanticSearchInput?.addEventListener('blur', syncSemanticQueryToUrl); |
| document.getElementById('semantic_submode_select')?.addEventListener('change', syncSemanticOptionsToStorage); |
| document.getElementById('semantic_chunked_mode')?.addEventListener('change', syncSemanticOptionsToStorage); |
| document.getElementById('semantic_threshold_input')?.addEventListener('change', syncSemanticOptionsToStorage); |
| document.getElementById('semantic_color_source_select')?.addEventListener('change', () => { |
| visualizationUpdater.updateSemanticColorSource(); |
| syncSemanticOptionsToStorage(); |
| }); |
|
|
| |
| saveBtn.on('click', async () => { |
| try { |
| const state = appStateManager.getState(); |
| await handleServerDemoSave({ |
| api, |
| currentData: visualizationUpdater.getCurrentData(), |
| rawApiResponse: visualizationUpdater.getRawApiResponse(), |
| textFieldValue: textInputController.getTextValue(), |
| enableDemo: current.demo, |
| demoManager: demoManager || null, |
| serverStorage, |
| currentFileName: state.currentFileName, |
| onSaveStart: () => { |
| appStateManager.updateState({ isSaving: true }); |
| }, |
| onSaveSuccess: (name?: string) => { |
| appStateManager.updateState({ |
| isSaving: false, |
| isSavedToServer: true |
| }); |
| }, |
| onSaveError: () => { |
| appStateManager.updateState({ isSaving: false }); |
| }, |
| setGlobalLoading: (loading: boolean) => appStateManager.setGlobalLoading(loading), |
| showToast |
| }); |
| } catch (error) { |
| |
| } |
| }); |
|
|
| |
| saveLocalBtn.on('click', async () => { |
| const rawApiResponse = visualizationUpdater.getRawApiResponse(); |
| if (!rawApiResponse) { |
| showAlertDialog(tr('Error'), tr('No data to save, please analyze text first')); |
| return; |
| } |
|
|
| |
| const state = appStateManager.getState(); |
|
|
| await handleLocalDemoSave( |
| rawApiResponse, |
| state.currentFileName || undefined, |
| textInputController.getTextValue() |
| ); |
| }); |
|
|
| |
| analyzeSaveBtn.on('click', async () => { |
| let clipboardText = ''; |
| try { |
| clipboardText = await navigator.clipboard.readText(); |
| } catch (error) { |
| |
| clipboardText = ''; |
| } |
| if (!clipboardText) { |
| clipboardText = ''; |
| } |
| await openAnalyzeSaveDialog(clipboardText); |
| }); |
|
|
| |
|
|
| eventHandler.bind(GLTR_Text_Box.events.tokenHovered, (ev: GLTR_HoverEvent) => { |
| if (ev.hovered) { |
| toolTip.updateData(ev.d, ev.event); |
| } else { |
| toolTip.visibility = false; |
| } |
| }); |
|
|
| initDensityAttributionSidebar({ |
| eventHandler, |
| getCurrentAnalyzeResult: () => lmf.getCurrentAnalyzeResult(), |
| apiPrefix: api_prefix, |
| showToast, |
| predictionModelVariant: 'base', |
| sourcePage: 'analysis.html', |
| }); |
|
|
| |
|
|
| |
| eventHandler.bind(Histogram.events.binClicked, (ev: HistogramBinClickEvent) => { |
| highlightController.handleHistogramBinClick(ev); |
| }); |
|
|
| eventHandler.bind(ScatterPlot.events.chunkClicked, (ev: ScatterChunkClickEvent) => { |
| highlightController.handleMatchScoreChunkClick(ev); |
| }); |
|
|
| d3.select('body').on('touchstart', () => { |
| toolTip.hideAndReset(); |
| }) |
|
|
| const mainWindow = { |
| width: () => window.innerWidth - (current.sidebar.visible ? current.sidebar.width : 0), |
| height: () => window.innerHeight - 195 |
| }; |
|
|
|
|
| |
| const layoutController = new LayoutController({ |
| sidebarState: current.sidebar, |
| sideBar: side_bar, |
| sidebarBtn: d3.select('#sidebar_btn'), |
| panelSplitStorageKey: PANEL_SPLIT_STORAGE_KEY_START, |
| }); |
| }; |
|
|
|
|
|
|
|
|
|
|