import { html } from 'lit'; import { LightElement } from '../lib/base.js'; import { t } from '../lib/i18n.js'; function emptyTForm() { return { provider_id: '', model_id: '', name: '', language: '', priority: 100 }; } export class ModelsTranscribeSection extends LightElement { static properties = { onback: { attribute: false }, _models: { state: true }, _providers: { state: true }, _modal: { state: true }, _form: { state: true }, _saving: { state: true }, _error: { state: true }, _provider: { state: true }, _remoteModels: { state: true }, _loadingModels: { state: true }, }; constructor() { super(); this.onback = null; this._models = []; this._providers = []; this._modal = null; this._form = emptyTForm(); this._saving = false; this._error = null; this._provider = null; this._remoteModels = null; this._loadingModels = false; } connectedCallback() { super.connectedCallback(); this.__onLocaleChanged = () => this.requestUpdate(); window.addEventListener('locale-changed', this.__onLocaleChanged); this._load(); } disconnectedCallback() { window.removeEventListener('locale-changed', this.__onLocaleChanged); super.disconnectedCallback(); } async _load() { try { const [modelsRes, providersRes] = await Promise.all([ fetch('/api/transcribe/models'), fetch('/api/llm/providers'), ]); if (!modelsRes.ok) throw new Error(`models: HTTP ${modelsRes.status}`); if (!providersRes.ok) throw new Error(`providers: HTTP ${providersRes.status}`); this._models = await modelsRes.json(); this._providers = await providersRes.json(); } catch (e) { this._error = e.message; } } // ── Add flow ───────────────────────────────────────────────────────────────── _openAdd() { this._error = null; this._provider = null; this._form = emptyTForm(); this._modal = 'pick-provider'; } async _pickProvider(provider) { this._provider = provider; this._remoteModels = null; this._form = { ...emptyTForm(), provider_id: provider.id }; this._loadingModels = true; this._modal = 'pick-model'; try { const res = await fetch(`/api/transcribe/providers/${provider.id}/models`); this._remoteModels = res.ok ? await res.json() : null; } catch { this._remoteModels = null; } finally { this._loadingModels = false; if (!this._remoteModels || this._remoteModels.length === 0) { this._modal = 'add'; } } } _pickRemoteModel(remote) { this._form = { ...this._form, model_id: remote.id, name: remote.name, }; this._modal = 'add'; } // ── Edit flow ──────────────────────────────────────────────────────────────── async _openEdit(m) { this._error = null; try { const res = await fetch(`/api/transcribe/models/${m.id}`); if (!res.ok) throw new Error(`HTTP ${res.status}`); const r = await res.json(); this._provider = this._providers.find(p => p.id === r.provider_id) ?? null; this._form = { provider_id: r.provider_id, model_id: r.model_id, name: r.name, language: r.language ?? '', priority: r.priority, }; this._modal = { mode: 'edit', id: r.id, name: r.name }; } catch (e) { this._error = e.message; } } // ── Delete ─────────────────────────────────────────────────────────────────── async _delete(m) { if (!confirm(t('models.confirm_delete', { type: t('models.hub.card.transcribe.title'), name: m.name }))) return; try { const res = await fetch(`/api/transcribe/models/${m.id}`, { method: 'DELETE' }); if (!res.ok) throw new Error(await res.text()); await this._load(); } catch (e) { this._error = e.message; } } // ── Submit add ─────────────────────────────────────────────────────────────── async _submitAdd(e) { e.preventDefault(); if (this._saving) return; this._saving = true; this._error = null; const f = this._form; try { const res = await fetch('/api/transcribe/models', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ provider_id: Number(f.provider_id), model_id: f.model_id, name: f.name || f.model_id, language: f.language || null, priority: Number(f.priority) || 100, }), }); if (!res.ok) throw new Error(await res.text()); this._modal = null; await this._load(); } catch (err) { this._error = err.message; } finally { this._saving = false; } } // ── Submit edit ────────────────────────────────────────────────────────────── async _submitEdit(e) { e.preventDefault(); if (this._saving) return; this._saving = true; this._error = null; const f = this._form; const id = this._modal.id; try { const res = await fetch(`/api/transcribe/models/${id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ provider_id: Number(f.provider_id), model_id: f.model_id, name: f.name || f.model_id, language: f.language || null, priority: Number(f.priority) || 100, }), }); if (!res.ok) throw new Error(await res.text()); this._modal = null; await this._load(); } catch (err) { this._error = err.message; } finally { this._saving = false; } } _closeModal() { this._modal = null; this._error = null; } // ── Render ─────────────────────────────────────────────────────────────────── _renderRow(m) { const isPlugin = m.from_plugin; return html` ${isPlugin ? html`${t('models.source_plugin')}` : html`${t('models.source_cloud')}`} ${m.name} ${isPlugin ? '—' : m.provider_name} ${m.model_id} ${m.language ?? html`${t('models.language_auto')}`} ${isPlugin ? html` ` : html` `} `; } _renderPickProvider() { const tProviders = this._providers.filter(p => Array.isArray(p.supported_types) && p.supported_types.includes('transcribe') ); return html`
{ if (e.target === e.currentTarget) this._closeModal(); }}>
${t('models.add_model_provider', { type: t('models.hub.card.transcribe.title') })}
${this._error ? html`
${this._error}
` : ''}
${tProviders.map(p => html` `)}
`; } _renderPickModel() { const p = this._provider; return html`
{ if (e.target === e.currentTarget) this._closeModal(); }}>
${t('models.add_model_type', { type: t('models.hub.card.transcribe.title') })} ${p?.name}
${this._loadingModels ? html`
${t('models.loading')}
` : html`
${(this._remoteModels ?? []).map(m => html` `)}
`}
`; } _renderForm(isEdit = false) { const f = this._form; const p = this._provider; const title = isEdit ? html`${t('models.edit')} ${this._modal.name}` : html`${t('models.add_model_type', { type: t('models.hub.card.transcribe.title') })} ${p?.name}`; return html`
{ if (e.target === e.currentTarget) this._closeModal(); }}>
${title}
${this._error ? html`
${this._error}
` : ''}
isEdit ? this._submitEdit(e) : this._submitAdd(e)}>
this._form = { ...this._form, model_id: e.target.value }} /> ${isEdit ? html`
${t('models.form.model_lock')}
` : ''}
this._form = { ...this._form, name: e.target.value }} />
this._form = { ...this._form, language: e.target.value }} />
this._form = { ...this._form, priority: e.target.value }} />
`; } render() { const tProviders = this._providers.filter(p => Array.isArray(p.supported_types) && p.supported_types.includes('transcribe') ); const canAdd = tProviders.length > 0; return html`
${this.onback ? html` ` : ''}

${t('models.transcribe.title')}

${!canAdd ? html`

${t('models.no_providers_transcribe')}

` : ''} ${this._error && !this._modal ? html`
${this._error}
` : ''} ${this._models.length === 0 ? html`

${t('models.list_empty_transcribe')} ${canAdd ? html` ${t('models.list_empty_add_hint')}` : ''} ${t('models.list_empty_whisper')}

` : html`
${this._models.map(m => this._renderRow(m))}
${t('models.source')} ${t('models.name_col')} ${t('models.provider_col')} ${t('models.model_id_col')} ${t('models.language_col')}
`}
${this._modal === 'pick-provider' ? this._renderPickProvider() : ''} ${this._modal === 'pick-model' ? this._renderPickModel() : ''} ${this._modal === 'add' ? this._renderForm(false) : ''} ${this._modal?.mode === 'edit' ? this._renderForm(true) : ''} `; } }