/** * Model Management 弹窗 */ import * as d3 from 'd3'; import { showDialog, showAlertDialog } from '../../shared/ui/dialog'; import { tr } from '../../shared/lang/i18n-lite'; import type { TextAnalysisAPI } from '../../shared/api/GLTR_API'; export async function showModelManageDialog(api: TextAnalysisAPI): Promise { try { const [availableModelsResp, currentModelResp] = await Promise.all([ api.getAvailableModels(), api.getCurrentModel(), ]); if (!availableModelsResp.success || !currentModelResp.success) { showAlertDialog(tr('Error'), 'Failed to load model management information'); return; } const models = availableModelsResp.models; let currentModel = currentModelResp.model; let deviceType = currentModelResp.device_type; let currentUseInt8 = currentModelResp.use_int8; let currentUseBfloat16 = currentModelResp.use_bfloat16; let isLoading = currentModelResp.loading; let pollId: number | null = null; let setConfirmBtnState: (enabled: boolean, queuing?: boolean) => void = () => {}; showDialog({ title: 'Model Management', loadingConfirmText: 'Applying...', content: (dialog, setConfirmButtonState) => { setConfirmBtnState = setConfirmButtonState ?? (() => {}); const container = dialog.append('div').attr('class', 'dialog-form-container'); const deviceInfo = container .append('div') .attr('class', 'device-info') .style('margin-bottom', '12px') .style('padding', '8px') .style('background-color', 'var(--panel-bg)') .style('border-radius', '4px') .style('font-size', '12px'); const titleRow = deviceInfo .append('div') .style('display', 'flex') .style('justify-content', 'space-between') .style('align-items', 'center') .style('margin-bottom', '6px'); const modelTitle = titleRow.append('div').style('font-weight', 'bold').style('color', 'var(--primary-color, #2196F3)'); const refreshBtn = titleRow.append('button').attr('class', 'refresh-btn').attr('title', 'Refresh').text('↻'); const hideDisplay = () => { deviceInfo.style('opacity', '0'); }; const updateDisplay = () => { modelTitle.text(`Current Model: ${currentModel}${isLoading ? ' (Loading...)' : ''}`); deviceInfo.select('.device-type').text(`Device Type: ${deviceType.toUpperCase()}`); const currentQuantization = currentUseInt8 ? 'INT8' : currentUseBfloat16 ? 'bfloat16' : deviceType === 'cpu' ? 'float32' : 'float16'; deviceInfo.select('.quantization').text(`Current Quantization: ${currentQuantization}`); deviceInfo.style('opacity', '1'); }; deviceInfo.append('div').attr('class', 'device-type'); deviceInfo.append('div').attr('class', 'quantization'); updateDisplay(); const fetchAndUpdate = async () => { hideDisplay(); try { const resp = await api.getCurrentModel(); if (resp.success) { currentModel = resp.model; deviceType = resp.device_type; currentUseInt8 = resp.use_int8; currentUseBfloat16 = resp.use_bfloat16; isLoading = resp.loading; updateDisplay(); } } catch { // 未收到或失败不恢复,保持隐藏 } }; refreshBtn.on('click', async () => { refreshBtn.property('disabled', true).text('…'); await fetchAndUpdate(); refreshBtn.property('disabled', false).text('↻'); }); const overlay = deviceInfo.node()?.closest('.dialog-overlay'); const pollMs = 2000; pollId = window.setInterval(async () => { if (!overlay?.isConnected) { if (pollId != null) window.clearInterval(pollId); pollId = null; return; } await fetchAndUpdate(); }, pollMs); container.append('label').attr('class', 'dialog-label').style('margin-top', '12px').text('Select model:'); const modelList = container .append('div') .attr('class', 'model-list') .style('max-height', '200px') .style('overflow-y', 'auto') .style('margin-top', '8px'); let selectedModel = currentModel; models.forEach((model) => { const modelItem = modelList .append('div') .attr('class', 'model-item') .style('padding', '8px 12px') .style('margin', '4px 0') .style('border', '1px solid var(--border-color, #ddd)') .style('border-radius', '4px') .style('cursor', 'pointer') .style('transition', 'background-color 0.2s') .classed('current-model', model === currentModel); if (model === currentModel) { modelItem.style('background-color', 'var(--bg-hover, #f0f0f0)').style('font-weight', 'bold'); } modelItem.append('span').text(model); modelItem.on('click', function () { selectedModel = model; modelList.selectAll('.model-item').style('background-color', null).style('font-weight', null); d3.select(this).style('background-color', 'var(--bg-hover, #f0f0f0)').style('font-weight', 'bold'); }); modelItem .on('mouseenter', function () { if (model !== selectedModel) { d3.select(this).style('background-color', 'var(--bg-hover-light, #f8f8f8)'); } }) .on('mouseleave', function () { if (model !== selectedModel) { d3.select(this).style('background-color', null); } }); }); container.append('label').attr('class', 'dialog-label').style('margin-top', '16px').text('Quantization Options:'); const quantizationOptions = container .append('div') .attr('class', 'quantization-options') .style('margin-top', '8px') .style('padding', '8px') .style('border', '1px solid var(--border-color, #ddd)') .style('border-radius', '4px'); const int8Option = quantizationOptions.append('div').style('margin-bottom', '8px'); const int8Checkbox = int8Option .append('input') .attr('type', 'checkbox') .attr('id', 'use_int8_checkbox') .property('checked', currentUseInt8) .property('disabled', deviceType === 'mps'); const int8LabelText = deviceType === 'mps' ? 'Use INT8 Quantization (not supported on MPS)' : 'Use INT8 Quantization'; int8Option .append('label') .attr('for', 'use_int8_checkbox') .style('margin-left', '6px') .style('cursor', deviceType === 'mps' ? 'not-allowed' : 'pointer') .style('color', deviceType === 'mps' ? 'var(--text-disabled, #999)' : null) .text(int8LabelText); const bfloat16Option = quantizationOptions.append('div'); const bfloat16Checkbox = bfloat16Option .append('input') .attr('type', 'checkbox') .attr('id', 'use_bfloat16_checkbox') .property('checked', currentUseBfloat16) .property('disabled', deviceType !== 'cpu'); const bfloat16LabelText = deviceType !== 'cpu' ? 'Use bfloat16 (CPU only)' : 'Use bfloat16'; bfloat16Option .append('label') .attr('for', 'use_bfloat16_checkbox') .style('margin-left', '6px') .style('cursor', deviceType !== 'cpu' ? 'not-allowed' : 'pointer') .style('color', deviceType !== 'cpu' ? 'var(--text-disabled, #999)' : null) .text(bfloat16LabelText); int8Checkbox.on('change', function () { if ((this as HTMLInputElement).checked) { bfloat16Checkbox.property('checked', false); } }); bfloat16Checkbox.on('change', function () { if ((this as HTMLInputElement).checked) { int8Checkbox.property('checked', false); } }); return { getValue: () => ({ model: selectedModel, use_int8: (int8Checkbox.node() as HTMLInputElement)?.checked || false, use_bfloat16: (bfloat16Checkbox.node() as HTMLInputElement)?.checked || false, }), validate: () => { if (isLoading) return false; const useInt8 = (int8Checkbox.node() as HTMLInputElement)?.checked || false; const useBfloat16 = (bfloat16Checkbox.node() as HTMLInputElement)?.checked || false; return ( selectedModel !== currentModel || useInt8 !== currentUseInt8 || useBfloat16 !== currentUseBfloat16 ); }, focus: () => {}, }; }, onConfirm: async (params: { model: string; use_int8: boolean; use_bfloat16: boolean }) => { setConfirmBtnState(false, true); try { const result = await api.switchModel(params.model, params.use_int8, params.use_bfloat16); setConfirmBtnState(true, false); if (result.success) { showAlertDialog( tr('Success'), result.message || 'Model settings applied. The selected model will be used for the next analysis.' ); } else { showAlertDialog(tr('Error'), result.message || 'Failed to apply model settings'); } } catch (error: any) { setConfirmBtnState(true, false); showAlertDialog(tr('Error'), 'Failed to apply model settings: ' + error.message); } return false; }, onCancel: () => { if (pollId != null) { window.clearInterval(pollId); pollId = null; } }, confirmText: 'Apply', cancelText: tr('Exit'), width: 'clamp(400px, 90vw, 500px)', }); } catch (error) { console.error('Failed to load models:', error); showAlertDialog(tr('Error'), 'Failed to load model management information'); } }