| | |
| | | let languageOptions = $state<LanguageOption[]>([]) |
| | | let modelOptions = $state<ModelOption[]>([]) |
| | | let modelBusyID = $state('') |
| | | let modelBusyKind = $state<'download' | 'select' | ''>('') |
| | | let modelCancellingID = $state('') |
| | | let modelProgress = $state(0) |
| | | let modelError = $state('') |
| | | |
| | |
| | | |
| | | function modelActionLabel(option: ModelOption): string { |
| | | if (modelBusyID === option.modelID) { |
| | | if (!option.installed && modelProgress > 0) { |
| | | return `${Math.min(100, Math.max(0, modelProgress)).toFixed(0)}%` |
| | | if (modelBusyKind === 'download') { |
| | | return modelCancellingID === option.modelID ? t('settings.modelCancelling') : t('settings.modelCancel') |
| | | } |
| | | return t('settings.modelWorking') |
| | | } |
| | |
| | | return t('settings.modelDownload') |
| | | } |
| | | |
| | | function modelMeta(option: ModelOption): string { |
| | | let meta = option.installed ? t('settings.modelInstalled') : t('settings.modelNotInstalled') |
| | | if (option.downloadSize) { |
| | | meta += ` · ${option.downloadSize}` |
| | | } |
| | | if (modelBusyID === option.modelID && modelBusyKind === 'download' && modelProgress > 0) { |
| | | const pct = Math.min(100, Math.max(0, modelProgress)).toFixed(0) |
| | | meta += ` · ${pct}%` |
| | | } |
| | | return meta |
| | | } |
| | | |
| | | function isCancelError(err: any): boolean { |
| | | const text = String(err?.message || err || '').toLowerCase() |
| | | return text.includes('context canceled') || text.includes('cancelled') || text.includes('canceled') |
| | | } |
| | | |
| | | function canCancelModel(option: ModelOption): boolean { |
| | | return modelBusyID === option.modelID && modelBusyKind === 'download' |
| | | } |
| | | |
| | | async function onModelButton(option: ModelOption) { |
| | | if (canCancelModel(option)) { |
| | | await cancelModelDownload(option) |
| | | return |
| | | } |
| | | await onModelAction(option) |
| | | } |
| | | |
| | | async function cancelModelDownload(option: ModelOption) { |
| | | if (!canCancelModel(option) || modelCancellingID) return |
| | | modelCancellingID = option.modelID |
| | | modelError = '' |
| | | try { |
| | | const cancelled: any = await Call.ByName('voicesnap/services.EngineService.CancelModelDownload', option.modelID) |
| | | if (!cancelled) { |
| | | modelBusyID = '' |
| | | modelBusyKind = '' |
| | | modelCancellingID = '' |
| | | modelProgress = 0 |
| | | await loadModelOptions() |
| | | } |
| | | } catch (err: any) { |
| | | modelError = err?.message || String(err || t('settings.modelActionFailed')) |
| | | modelBusyID = '' |
| | | modelBusyKind = '' |
| | | modelCancellingID = '' |
| | | modelProgress = 0 |
| | | await loadModelOptions() |
| | | } |
| | | } |
| | | |
| | | async function onModelAction(option: ModelOption) { |
| | | if ((option.isCurrent && option.installed) || modelBusyID) return |
| | | modelBusyID = option.modelID |
| | | modelBusyKind = option.installed ? 'select' : 'download' |
| | | modelProgress = 0 |
| | | modelError = '' |
| | | try { |
| | |
| | | } |
| | | await loadModelOptions() |
| | | } catch (err: any) { |
| | | if (!isCancelError(err)) { |
| | | modelError = err?.message || String(err || t('settings.modelActionFailed')) |
| | | } |
| | | await loadModelOptions() |
| | | } finally { |
| | | if (modelBusyID === option.modelID) { |
| | | modelBusyID = '' |
| | | modelBusyKind = '' |
| | | modelCancellingID = '' |
| | | modelProgress = 0 |
| | | } |
| | | } |
| | | } |
| | | |
| | |
| | | </span> |
| | | </div> |
| | | <span class="model-desc">{modelDescription(option)}</span> |
| | | <span class="model-meta"> |
| | | {option.installed ? t('settings.modelInstalled') : t('settings.modelNotInstalled')} |
| | | {#if option.downloadSize} |
| | | · {option.downloadSize} |
| | | {/if} |
| | | </span> |
| | | <span class="model-meta">{modelMeta(option)}</span> |
| | | </div> |
| | | {#if option.isCurrent && option.installed} |
| | | <span class="model-current-status">{t('settings.modelCurrent')}</span> |
| | | {:else} |
| | | <button |
| | | class="model-action primary" |
| | | disabled={!!modelBusyID} |
| | | onclick={() => onModelAction(option)} |
| | | class:cancel={canCancelModel(option)} |
| | | disabled={!!modelBusyID && modelBusyID !== option.modelID} |
| | | onclick={() => onModelButton(option)} |
| | | > |
| | | {modelActionLabel(option)} |
| | | </button> |
| | |
| | | color: white; |
| | | } |
| | | |
| | | .model-action.primary.cancel { |
| | | background: var(--color-red); |
| | | } |
| | | |
| | | .model-action:disabled { |
| | | cursor: not-allowed; |
| | | opacity: 0.72; |