| import * as d3 from 'd3'; |
| import './utils/d3-polyfill'; |
| import '../css/start.scss'; |
| import '../css/chat.scss'; |
|
|
| import { initThemeManager } from './ui/theme'; |
| import { initLanguageManager } from './ui/language'; |
| import { initI18n, tr } from './lang/i18n-lite'; |
| import { AdminManager } from './utils/adminManager'; |
| import { SettingsMenuManager } from './utils/settingsMenuManager'; |
| import { initCachedHistoryQueryDropdown, type CachedHistorySelectContext } from './utils/cachedHistoryUi'; |
| import { initChatPanelLayout } from './chat/chatPanelLayout'; |
| import { PANEL_SPLIT_STORAGE_KEY_CHAT } from './utils/panelSplitStorage'; |
| import { TextInputController } from './controllers/textInputController'; |
| import { initializeCommonApp } from './appInitializer'; |
| import { showAlertDialog } from './ui/dialog'; |
| import URLHandler from './utils/URLHandler'; |
| import { ToolTip } from './vis/ToolTip'; |
| import { GLTR_HoverEvent, GLTR_Mode, GLTR_Text_Box } from './vis/GLTR_Text_Box'; |
| import { |
| postCompletions, |
| postCompletionsPrompt, |
| postCompletionsStop, |
| type OpenAICompletionsResponse |
| } from './api/completionsClient'; |
| import { translateApiErrorMessage } from './utils/errorUtils'; |
| import { |
| buildCompletionDisplayResult, |
| CHAT_DEFAULT_COMPLETION_MODEL |
| } from './chat/buildCompletionDisplayResult'; |
| import { completionFinishReasonLabel } from './utils/generationEndReasonLabel'; |
| import { addDigitsMergeRenderListener } from './utils/digitsMergeManager'; |
| import { |
| CHAT_RAW_INPUT_HISTORY_KEY, |
| CHAT_SYSTEM_INPUT_HISTORY_KEY, |
| CHAT_USER_INPUT_HISTORY_KEY, |
| initQueryHistoryDropdown, |
| saveHistory |
| } from './utils/queryHistory'; |
| import { |
| buildCachedContentUrlParam, |
| getCachedEntryByContentKey, |
| listCachedHistoryRows, |
| removeCachedEntryByContentKey, |
| touchCachedEntryByContentKey, |
| type CompletionCachedEntry, |
| type CompletionResultCacheKey, |
| } from './utils/completionResultCache'; |
| import { |
| DEFAULT_CONTENT_URL_PARAM, |
| readContentUrlParam, |
| replaceContentUrlParam, |
| runContentUrlHydrate, |
| } from './utils/contentUrl'; |
| import { CHAT_SURPRISAL_COLOR_MAP_MAX } from './utils/SurprisalColorConfig'; |
| import { updateChatCompletionMetrics } from './utils/textMetricsUpdater'; |
| import { |
| readSkipChatTemplateFromStorage, |
| writeSkipChatTemplateToStorage, |
| } from './utils/chatPromptTemplateMode'; |
| import { createToast } from './ui/toast'; |
| import { initDensityAttributionSidebar } from './attribution/densityAttributionSidebar'; |
| import { syncDraftCommittedButtonPair } from './utils/syncDraftCommittedButtonPair'; |
|
|
| |
| d3.selectAll('.loadersmall').style('display', 'none'); |
|
|
| initI18n(); |
|
|
| const showToast = createToast('#toast').show; |
|
|
| const apiPrefix = URLHandler.parameters['api'] || ''; |
| const bodyElement = d3.select('body').node() as Element; |
| const { eventHandler, totalSurprisalFormat, api } = initializeCommonApp(apiPrefix, bodyElement); |
|
|
| const adminManager = AdminManager.getInstance(); |
| api.setAdminToken(adminManager.isInAdminMode() ? adminManager.getAdminToken() : null); |
|
|
| const textField = d3.select('#test_text'); |
| const textCountValue = d3.select('#text_count_value'); |
| const chatSystemTextField = d3.select('#chat_system_text'); |
| const chatSystemTextCountValue = d3.select('#chat_system_text_count_value'); |
| const chatUserTextField = d3.select('#chat_user_text'); |
| const chatUserTextCountValue = d3.select('#chat_user_text_count_value'); |
| const metricUsage = d3.select('#metric_usage'); |
| const metricModel = d3.select('#metric_model'); |
| const chatCompleteReasonEl = d3.select('#chat_complete_reason'); |
| const clearBtn = d3.select('#clear_text_btn'); |
| const chatSystemClearBtn = d3.select('#chat_system_clear_text_btn'); |
| const chatUserClearBtn = d3.select('#chat_user_clear_text_btn'); |
| const submitBtn = d3.select('#submit_text_btn'); |
| const forceRetryBtn = d3.select('#force_retry_btn'); |
| const pasteBtn = d3.select('#paste_text_btn'); |
| const chatSystemPasteBtn = d3.select('#chat_system_paste_text_btn'); |
| const chatUserPasteBtn = d3.select('#chat_user_paste_text_btn'); |
| const rawInputHistoryBtn = document.getElementById('chat_raw_input_history_btn'); |
| const chatSystemHistoryBtn = document.getElementById('chat_system_prompt_history_btn'); |
| const chatUserHistoryBtn = document.getElementById('chat_user_prompt_history_btn'); |
| const maxNewTokensInput = document.getElementById( |
| 'chat_max_new_tokens' |
| ) as HTMLInputElement | null; |
| const loaderSmall = d3.select('.loadersmall'); |
|
|
| const rawInputPanel = document.getElementById('raw_input_panel'); |
| const chatInputPanel = document.getElementById('chat_input_panel'); |
|
|
| const skipChatTemplateInput = document.getElementById( |
| 'chat_skip_chat_template' |
| ) as HTMLInputElement | null; |
| const chatUseSystemPromptInput = document.getElementById( |
| 'chat_use_system_prompt' |
| ) as HTMLInputElement | null; |
| const chatSystemPromptPanel = document.getElementById('chat_system_prompt_panel'); |
|
|
| function isSkipChatTemplate(): boolean { |
| return skipChatTemplateInput?.checked ?? false; |
| } |
|
|
| function isChatUseSystemPrompt(): boolean { |
| return chatUseSystemPromptInput?.checked ?? true; |
| } |
|
|
| function syncChatSystemPromptSuppressedUi(): void { |
| const on = isChatUseSystemPrompt(); |
| chatSystemPromptPanel?.classList.toggle('chat-system-prompt-suppressed', !on); |
| const ta = chatSystemTextField.node() as HTMLTextAreaElement | null; |
| if (ta) { |
| ta.disabled = !on; |
| } |
| const dis = !on; |
| chatSystemClearBtn.property('disabled', dis); |
| chatSystemPasteBtn.property('disabled', dis); |
| if (chatSystemHistoryBtn instanceof HTMLButtonElement) { |
| chatSystemHistoryBtn.disabled = dis; |
| } |
| } |
|
|
| function syncPromptPanelVisibility(): void { |
| const skip = isSkipChatTemplate(); |
| if (rawInputPanel) rawInputPanel.hidden = !skip; |
| if (chatInputPanel) chatInputPanel.hidden = skip; |
| } |
|
|
| const chatRightStack = d3.select('.chat-right-stack'); |
| const chatPromptUsedEl = d3.select('#chat_prompt_used'); |
| const chatStreamingPreviewEl = d3.select('#chat_streaming_preview'); |
|
|
| async function copyChatFullText(): Promise<void> { |
| const pu = document.getElementById('chat_prompt_used'); |
| const prompt = |
| pu && !pu.hasAttribute('hidden') ? (pu.textContent ?? '') : ''; |
| const layer = document.querySelector('#results .text-layer'); |
| const generated = layer?.textContent ?? ''; |
| const text = prompt + generated; |
| if (!text) { |
| showToast('Nothing to copy', 'error'); |
| return; |
| } |
| try { |
| await navigator.clipboard.writeText(text); |
| showToast('Copied to clipboard', 'success'); |
| } catch { |
| showToast('Failed to copy to clipboard', 'error'); |
| } |
| } |
|
|
| new TextInputController({ |
| textField, |
| textCountValue, |
| clearBtn, |
| submitBtn, |
| saveBtn: forceRetryBtn, |
| pasteBtn, |
| totalSurprisalFormat, |
| showAlertDialog |
| }); |
|
|
| new TextInputController({ |
| textField: chatSystemTextField, |
| textCountValue: chatSystemTextCountValue, |
| clearBtn: chatSystemClearBtn, |
| submitBtn, |
| saveBtn: forceRetryBtn, |
| pasteBtn: chatSystemPasteBtn, |
| totalSurprisalFormat, |
| showAlertDialog |
| }); |
|
|
| new TextInputController({ |
| textField: chatUserTextField, |
| textCountValue: chatUserTextCountValue, |
| clearBtn: chatUserClearBtn, |
| submitBtn, |
| saveBtn: forceRetryBtn, |
| pasteBtn: chatUserPasteBtn, |
| totalSurprisalFormat, |
| showAlertDialog |
| }); |
|
|
| const toolTip = new ToolTip(d3.select('#major_tooltip'), eventHandler, { |
| surprisalRowLabel: tr('log perplexity:') |
| }); |
|
|
| const lmf = new GLTR_Text_Box(d3.select('#results'), eventHandler); |
| lmf.updateOptions( |
| { |
| gltrMode: GLTR_Mode.fract_p, |
| enableRenderAnimation: false, |
| enableMinimap: false, |
| overlayTokenRenderStyle: 'classic', |
| overlayIgnoreGlobalInfoDensityDisable: true, |
| surprisalColorMax: CHAT_SURPRISAL_COLOR_MAP_MAX |
| }, |
| true |
| ); |
|
|
| eventHandler.bind(GLTR_Text_Box.events.tokenHovered, (ev: GLTR_HoverEvent) => { |
| if (ev.hovered) { |
| toolTip.updateData(ev.d, ev.event); |
| } else { |
| toolTip.visibility = false; |
| } |
| }); |
|
|
| d3.select('body').on('touchstart', () => { |
| toolTip.hideAndReset(); |
| }); |
|
|
| const modelParam = URLHandler.parameters['model']; |
| const completionModel = |
| typeof modelParam === 'string' && modelParam.length > 0 |
| ? modelParam |
| : CHAT_DEFAULT_COMPLETION_MODEL; |
|
|
| let askAbort: AbortController | null = null; |
| let askInFlight = false; |
| let currentPromptUsed = ''; |
|
|
| |
| const STREAMING_PREVIEW_MIN_INTERVAL_MS = 10; |
|
|
| |
| let streamingPreviewLastFlush = 0; |
|
|
| |
| let lastCompletionForRerender: { |
| res: OpenAICompletionsResponse; |
| promptUsed: string; |
| } | null = null; |
|
|
| |
| type ChatCommittedFingerprint = |
| | { skipTemplate: true; raw: string; maxTokens: string } |
| | { |
| skipTemplate: false; |
| user: string; |
| system: string; |
| useSystem: boolean; |
| maxTokens: string; |
| }; |
|
|
| let lastCommittedFingerprint: ChatCommittedFingerprint | null = null; |
|
|
| function getCurrentFingerprint(): ChatCommittedFingerprint { |
| const maxTokens = maxNewTokensInput?.value ?? ''; |
| if (isSkipChatTemplate()) { |
| return { |
| skipTemplate: true, |
| raw: (textField.node() as HTMLTextAreaElement | null)?.value ?? '', |
| maxTokens, |
| }; |
| } |
| return { |
| skipTemplate: false, |
| user: (chatUserTextField.node() as HTMLTextAreaElement | null)?.value ?? '', |
| system: (chatSystemTextField.node() as HTMLTextAreaElement | null)?.value ?? '', |
| useSystem: isChatUseSystemPrompt(), |
| maxTokens, |
| }; |
| } |
|
|
| function fingerprintsEqual(a: ChatCommittedFingerprint, b: ChatCommittedFingerprint): boolean { |
| if (a.skipTemplate && b.skipTemplate) { |
| return a.raw === b.raw && a.maxTokens === b.maxTokens; |
| } |
| if (!a.skipTemplate && !b.skipTemplate) { |
| const ta = a as Extract<ChatCommittedFingerprint, { skipTemplate: false }>; |
| const tb = b as Extract<ChatCommittedFingerprint, { skipTemplate: false }>; |
| return ( |
| ta.user === tb.user && |
| ta.system === tb.system && |
| ta.useSystem === tb.useSystem && |
| ta.maxTokens === tb.maxTokens |
| ); |
| } |
| return false; |
| } |
|
|
| function syncAskButtonState(): void { |
| const fp = getCurrentFingerprint(); |
| const idleInputsReady = 'raw' in fp ? fp.raw.length > 0 : fp.user.length > 0; |
| const hasUncommittedDraft = |
| lastCommittedFingerprint === null || |
| !fingerprintsEqual(lastCommittedFingerprint, fp); |
| syncDraftCommittedButtonPair({ |
| primaryBtn: submitBtn, |
| forceRetryBtn, |
| inFlight: askInFlight, |
| primaryInFlightMode: 'stop', |
| primaryInFlightLabel: tr('Stop'), |
| primaryIdleLabel: tr('Ask'), |
| idleInputsReady, |
| hasUncommittedDraft, |
| }); |
| } |
|
|
| |
| function applyCompletionResponseToUi(res: OpenAICompletionsResponse, promptUsed: string): void { |
| currentPromptUsed = promptUsed; |
| lastCompletionForRerender = { res, promptUsed }; |
| streamingPreviewLastFlush = 0; |
| chatStreamingPreviewEl.text('').attr('hidden', 'true'); |
| chatPromptUsedEl.text(promptUsed).attr('hidden', null); |
| const finalText = res.choices?.[0]?.text; |
| if (typeof finalText !== 'string') { |
| throw new Error('续写响应缺少 choices[0].text'); |
| } |
| const display = buildCompletionDisplayResult( |
| finalText, |
| res.model, |
| res.info_radar?.bpe_strings ?? null |
| ); |
| lmf.update(display); |
| updateChatCompletionMetrics(metricUsage, metricModel, res.model ?? null, res.usage ?? null); |
| chatCompleteReasonEl.text(completionFinishReasonLabel(res.choices?.[0]?.finish_reason)); |
| replaceContentUrlParam(buildCachedContentUrlParam(promptUsed), DEFAULT_CONTENT_URL_PARAM, 'chat'); |
| lastCommittedFingerprint = getCurrentFingerprint(); |
| syncAskButtonState(); |
| } |
|
|
| addDigitsMergeRenderListener(() => { |
| if (!lastCompletionForRerender) return; |
| const { res, promptUsed } = lastCompletionForRerender; |
| applyCompletionResponseToUi(res, promptUsed); |
| }); |
|
|
| const themeManager = initThemeManager( |
| { |
| onThemeChange: () => { |
| if (lastCompletionForRerender) { |
| const { res, promptUsed } = lastCompletionForRerender; |
| applyCompletionResponseToUi(res, promptUsed); |
| } else { |
| lmf.reRenderCurrent(); |
| } |
| }, |
| }, |
| '#theme_dropdown' |
| ); |
|
|
| const languageManager = initLanguageManager({}, '#language_dropdown'); |
|
|
| void new SettingsMenuManager( |
| '#settings_btn', |
| '#settings_menu', |
| '#admin_mode_btn', |
| adminManager, |
| api, |
| undefined, |
| undefined, |
| themeManager, |
| languageManager, |
| 'common' |
| ); |
|
|
| const flushStreamingPreview = (text: string, streamEnd: boolean): void => { |
| if ( |
| !streamEnd && |
| Date.now() - streamingPreviewLastFlush < STREAMING_PREVIEW_MIN_INTERVAL_MS |
| ) { |
| return; |
| } |
| chatStreamingPreviewEl.text(text).attr('hidden', null); |
| streamingPreviewLastFlush = Date.now(); |
| }; |
|
|
| const getActivePromptValue = (): string => { |
| if (isSkipChatTemplate()) { |
| return (textField.node() as HTMLTextAreaElement | null)?.value ?? ''; |
| } |
| return (chatUserTextField.node() as HTMLTextAreaElement | null)?.value ?? ''; |
| }; |
|
|
| |
| function parseOptionalMaxNewTokens(raw: string): number | undefined { |
| const t = raw.trim(); |
| if (t === '') return undefined; |
| if (!/^\d+$/.test(t)) { |
| throw new Error(tr('Max new tokens must be a positive integer or empty')); |
| } |
| const n = parseInt(t, 10); |
| if (n <= 0) { |
| throw new Error(tr('Max new tokens must be a positive integer or empty')); |
| } |
| return n; |
| } |
|
|
| const setAskLoading = (loading: boolean): void => { |
| askInFlight = loading; |
| loaderSmall.style('display', loading ? null : 'none'); |
| chatRightStack.classed('chat-ask-in-flight', loading); |
| if (loading) { |
| lastCompletionForRerender = null; |
| streamingPreviewLastFlush = 0; |
| chatPromptUsedEl.text('').attr('hidden', 'true'); |
| chatStreamingPreviewEl.text('').attr('hidden', 'true'); |
| chatCompleteReasonEl.text(''); |
| |
| lmf.update(buildCompletionDisplayResult('', completionModel, null)); |
| } |
| syncAskButtonState(); |
| }; |
|
|
| const runAsk = async (options?: { forceRefresh?: boolean }): Promise<void> => { |
| const prompt = getActivePromptValue(); |
| if (askInFlight || prompt.length === 0) return; |
| const forceRefresh = options?.forceRefresh === true; |
|
|
| let maxTokensOpt: number | undefined; |
| try { |
| maxTokensOpt = parseOptionalMaxNewTokens(maxNewTokensInput?.value ?? ''); |
| } catch (e: unknown) { |
| const msg = e instanceof Error ? e.message : String(e); |
| showAlertDialog(tr('LLM Raw Chat'), translateApiErrorMessage(msg)); |
| return; |
| } |
|
|
| askAbort?.abort(); |
| askAbort = new AbortController(); |
| setAskLoading(true); |
|
|
| try { |
| let streamedText = ''; |
| const skipTemplate = skipChatTemplateInput?.checked ?? false; |
|
|
| let modelPrompt: string; |
| if (skipTemplate) { |
| modelPrompt = prompt; |
| chatPromptUsedEl.text(prompt).attr('hidden', null); |
| } else { |
| const useSystem = isChatUseSystemPrompt(); |
| const systemRaw = |
| (chatSystemTextField.node() as HTMLTextAreaElement | null)?.value ?? ''; |
| const promptReq: { model: string; prompt: string; system?: string } = { |
| model: completionModel, |
| prompt |
| }; |
| if (useSystem) { |
| promptReq.system = systemRaw; |
| } |
| const assembled = await postCompletionsPrompt(promptReq, { |
| signal: askAbort.signal |
| }); |
| modelPrompt = assembled.prompt_used; |
| chatPromptUsedEl.text(modelPrompt).attr('hidden', null); |
| } |
|
|
| if (skipTemplate) { |
| saveHistory(prompt, CHAT_RAW_INPUT_HISTORY_KEY); |
| } else { |
| saveHistory(prompt, CHAT_USER_INPUT_HISTORY_KEY); |
| if (isChatUseSystemPrompt()) { |
| const systemForHistory = |
| (chatSystemTextField.node() as HTMLTextAreaElement | null)?.value ?? ''; |
| if (systemForHistory.length > 0) { |
| saveHistory(systemForHistory, CHAT_SYSTEM_INPUT_HISTORY_KEY); |
| } |
| } |
| } |
|
|
| const cacheKey: CompletionResultCacheKey = { prompt: modelPrompt }; |
|
|
| const res = await postCompletions( |
| { |
| model: completionModel, |
| prompt: modelPrompt, |
| ...(maxTokensOpt !== undefined ? { max_tokens: maxTokensOpt } : {}) |
| }, |
| { |
| signal: askAbort.signal, |
| cacheKey, |
| forceRefresh, |
| onDelta: (chunk, streamEnd) => { |
| streamedText += chunk; |
| flushStreamingPreview(streamedText, streamEnd); |
| } |
| } |
| ); |
| const finalText = res.choices?.[0]?.text; |
| if (typeof finalText !== 'string') { |
| throw new Error('Completion response missing choices[0].text'); |
| } |
| |
| |
| if (finalText !== streamedText && !finalText.startsWith(streamedText)) { |
| throw new Error( |
| 'Streaming deltas do not match final text (retry or report): ' + |
| `delta_len=${streamedText.length}, final_len=${finalText.length}` |
| ); |
| } |
| applyCompletionResponseToUi(res, modelPrompt); |
| } catch (err: unknown) { |
| if ( |
| err && |
| typeof err === 'object' && |
| 'name' in err && |
| (err as { name: string }).name === 'AbortError' |
| ) { |
| return; |
| } |
| const msg = err instanceof Error ? err.message : String(err); |
| showAlertDialog(tr('LLM Raw Chat'), translateApiErrorMessage(msg)); |
| } finally { |
| streamingPreviewLastFlush = 0; |
| setAskLoading(false); |
| } |
| }; |
|
|
| if (skipChatTemplateInput) { |
| skipChatTemplateInput.checked = readSkipChatTemplateFromStorage(); |
| skipChatTemplateInput.addEventListener('change', () => { |
| writeSkipChatTemplateToStorage(skipChatTemplateInput.checked); |
| syncPromptPanelVisibility(); |
| syncChatSystemPromptSuppressedUi(); |
| syncAskButtonState(); |
| }); |
| } |
| syncPromptPanelVisibility(); |
| syncChatSystemPromptSuppressedUi(); |
| chatUseSystemPromptInput?.addEventListener('change', () => { |
| syncChatSystemPromptSuppressedUi(); |
| syncAskButtonState(); |
| }); |
| syncAskButtonState(); |
|
|
| const promptTextarea = textField.node() as HTMLTextAreaElement | null; |
| const chatSystemPromptTextarea = chatSystemTextField.node() as HTMLTextAreaElement | null; |
| const chatUserPromptTextarea = chatUserTextField.node() as HTMLTextAreaElement | null; |
| if (promptTextarea) { |
| promptTextarea.addEventListener('input', () => { |
| syncAskButtonState(); |
| }); |
| } |
| if (chatUserPromptTextarea) { |
| chatUserPromptTextarea.addEventListener('input', () => { |
| syncAskButtonState(); |
| }); |
| } |
| if (chatSystemPromptTextarea) { |
| chatSystemPromptTextarea.addEventListener('input', () => { |
| syncAskButtonState(); |
| }); |
| } |
| maxNewTokensInput?.addEventListener('input', () => { |
| syncAskButtonState(); |
| }); |
|
|
| async function restoreChatFromCachedPrompt( |
| contentKey: string, |
| options: { |
| shouldTouch: boolean; |
| ctx?: CachedHistorySelectContext; |
| syncPromptToTextField: boolean; |
| cached?: CompletionCachedEntry; |
| } |
| ): Promise<void> { |
| const entry = options.cached ?? (await getCachedEntryByContentKey(contentKey)); |
| if (!entry) { |
| showToast(tr('Cached completion not found'), 'error'); |
| return; |
| } |
| const promptUsed = entry.promptUsed; |
| const res = entry.response; |
| try { |
| if (options.syncPromptToTextField) { |
| textField.property('value', promptUsed); |
| promptTextarea?.dispatchEvent(new Event('input', { bubbles: true })); |
| syncAskButtonState(); |
| } |
| applyCompletionResponseToUi(res, promptUsed); |
| if (!options.syncPromptToTextField || !isSkipChatTemplate()) { |
| lastCommittedFingerprint = null; |
| } |
| syncAskButtonState(); |
| if (options.shouldTouch && options.ctx) { |
| await touchCachedEntryByContentKey(contentKey); |
| await options.ctx.refreshList(); |
| } |
| } catch (e: unknown) { |
| const msg = e instanceof Error ? e.message : String(e); |
| showToast(translateApiErrorMessage(msg), 'error'); |
| } |
| } |
|
|
| submitBtn.on('click', () => { |
| if (askInFlight) { |
| postCompletionsStop(); |
| |
| return; |
| } |
| void runAsk(); |
| }); |
|
|
| forceRetryBtn.on('click', () => { |
| void runAsk({ forceRefresh: true }); |
| }); |
|
|
| initQueryHistoryDropdown({ |
| input: promptTextarea, |
| dropdownId: 'chat_raw_input_history_dropdown', |
| storageKey: CHAT_RAW_INPUT_HISTORY_KEY, |
| openDropdownOnFocusInput: false, |
| filterHistoryByInput: false, |
| onSelect: syncAskButtonState, |
| historyButton: rawInputHistoryBtn, |
| applyHistoryOnHover: true |
| }); |
|
|
| void initCachedHistoryQueryDropdown({ |
| dropdownId: 'chat_cached_history_dropdown', |
| historyButton: document.getElementById('chat_cached_history_btn'), |
| clickOutsideRoot: document.getElementById('chat_cached_history_dropdown'), |
| listMru: listCachedHistoryRows, |
| onSelectEntry: async (contentKey, shouldTouch, ctx) => { |
| await restoreChatFromCachedPrompt(contentKey, { |
| shouldTouch: Boolean(shouldTouch), |
| ctx, |
| syncPromptToTextField: true, |
| }); |
| }, |
| onRemove: removeCachedEntryByContentKey, |
| onPromote: (contentKey) => touchCachedEntryByContentKey(contentKey), |
| }); |
|
|
| void runContentUrlHydrate({ |
| readRaw: readContentUrlParam, |
| fetchEntry: getCachedEntryByContentKey, |
| apply: async (entry, rawContentKey) => { |
| await restoreChatFromCachedPrompt(rawContentKey, { |
| shouldTouch: false, |
| syncPromptToTextField: false, |
| cached: entry, |
| }); |
| }, |
| onMissing: async () => { |
| showToast(tr('Cached completion not found (link may be expired)'), 'error'); |
| replaceContentUrlParam(null, DEFAULT_CONTENT_URL_PARAM, 'chat'); |
| }, |
| onApplyError: (e: unknown) => { |
| const msg = e instanceof Error ? e.message : String(e); |
| showToast(translateApiErrorMessage(msg), 'error'); |
| replaceContentUrlParam(null, DEFAULT_CONTENT_URL_PARAM, 'chat'); |
| }, |
| }); |
|
|
| initQueryHistoryDropdown({ |
| input: chatSystemPromptTextarea, |
| dropdownId: 'chat_system_prompt_history_dropdown', |
| storageKey: CHAT_SYSTEM_INPUT_HISTORY_KEY, |
| openDropdownOnFocusInput: false, |
| filterHistoryByInput: false, |
| onSelect: syncAskButtonState, |
| historyButton: chatSystemHistoryBtn, |
| applyHistoryOnHover: true |
| }); |
|
|
| initQueryHistoryDropdown({ |
| input: chatUserPromptTextarea, |
| dropdownId: 'chat_user_prompt_history_dropdown', |
| storageKey: CHAT_USER_INPUT_HISTORY_KEY, |
| openDropdownOnFocusInput: false, |
| filterHistoryByInput: false, |
| onSelect: syncAskButtonState, |
| historyButton: chatUserHistoryBtn, |
| applyHistoryOnHover: true |
| }); |
|
|
| initChatPanelLayout({ storageKey: PANEL_SPLIT_STORAGE_KEY_CHAT }); |
|
|
| const chatCopyFulltextBtn = document.getElementById('chat_copy_fulltext_btn'); |
| if (chatCopyFulltextBtn) { |
| chatCopyFulltextBtn.addEventListener('click', () => { |
| void copyChatFullText(); |
| }); |
| } |
|
|
| initDensityAttributionSidebar({ |
| eventHandler, |
| getCurrentAnalyzeResult: () => lmf.getCurrentAnalyzeResult(), |
| apiPrefix, |
| showToast, |
| getContextPrefix: () => currentPromptUsed, |
| predictionModelVariant: 'instruct', |
| sourcePage: 'chat.html', |
| }); |
|
|