| |
| |
| |
| 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<void> { |
| 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'); |
| } |
| } |
|
|