()
| 339 | |
| 340 | // ─── Main component ─────────────────────────────────────────────────────────── |
| 341 | export function LocalModelManager() { |
| 342 | const root = document.createElement('div'); |
| 343 | root.className = 'flex flex-col gap-5'; |
| 344 | |
| 345 | if (!isLocalAIAvailable()) { |
| 346 | root.innerHTML = ` |
| 347 | <div class="flex flex-col items-center gap-3 py-8 text-center"> |
| 348 | <p class="text-sm font-bold text-white">${t('localModels.title')}</p> |
| 349 | <p class="text-xs text-muted max-w-xs">${t('localModels.webOnly')}</p> |
| 350 | </div> |
| 351 | `; |
| 352 | return root; |
| 353 | } |
| 354 | |
| 355 | // ── Section: engine status |
| 356 | const engineSection = document.createElement('div'); |
| 357 | engineSection.className = 'flex flex-col gap-2'; |
| 358 | engineSection.innerHTML = `<h3 class="text-xs font-bold text-secondary uppercase tracking-wider">${t('localModels.inferenceEngine')}</h3>`; |
| 359 | |
| 360 | let binaryReady = false; |
| 361 | const binaryBar = BinaryStatusBar((ready) => { binaryReady = ready; }); |
| 362 | engineSection.appendChild(binaryBar); |
| 363 | |
| 364 | const wan2gpBar = Wan2gpConfigBar(() => renderModels()); |
| 365 | engineSection.appendChild(wan2gpBar); |
| 366 | root.appendChild(engineSection); |
| 367 | |
| 368 | // ── Section: models |
| 369 | const modelsSection = document.createElement('div'); |
| 370 | modelsSection.className = 'flex flex-col gap-3'; |
| 371 | modelsSection.innerHTML = ` |
| 372 | <div class="flex items-center justify-between gap-3"> |
| 373 | <h3 class="text-xs font-bold text-secondary uppercase tracking-wider shrink-0">${t('localModels.title')}</h3> |
| 374 | <span id="local-model-storage" class="min-w-0 truncate text-right text-[10px] text-muted">${t('localModels.checkingStorage')}</span> |
| 375 | </div> |
| 376 | <div id="local-model-list" class="flex flex-col gap-3"></div> |
| 377 | `; |
| 378 | root.appendChild(modelsSection); |
| 379 | |
| 380 | const listEl = modelsSection.querySelector('#local-model-list'); |
| 381 | const storageEl = modelsSection.querySelector('#local-model-storage'); |
| 382 | |
| 383 | const refreshStorageInfo = async () => { |
| 384 | try { |
| 385 | const status = await localAI.getBinaryStatus(); |
| 386 | const storagePath = status.modelsDir || status.dataDir; |
| 387 | storageEl.textContent = storagePath ? `${t('localModels.storedIn')} ${storagePath}` : t('localModels.storedDefault'); |
| 388 | if (storagePath && status.envVar) { |
| 389 | storageEl.title = `Set ${status.envVar} before launch to change this location`; |
| 390 | } |
| 391 | } catch (_) { |
| 392 | storageEl.textContent = t('localModels.storedDefault'); |
| 393 | } |
| 394 | }; |
| 395 | |
| 396 | const renderModels = async () => { |
| 397 | listEl.innerHTML = `<div class="text-xs text-muted text-center py-4">${t('localModels.loading')}</div>`; |
| 398 | try { |
no test coverage detected