mirror of
https://github.com/mudler/LocalAI.git
synced 2026-04-28 14:20:31 -05:00
8ac7e8c299
Fixes a minor glitch that happens when switching model in from the chat pane where the header was not getting updated. Besides, it allows to create new chat directly when clicking from the management pane to the model. Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
729 lines
38 KiB
HTML
729 lines
38 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
{{template "views/partials/head" .}}
|
|
|
|
<body class="bg-[var(--color-bg-primary)] text-[var(--color-text-primary)]">
|
|
<div class="flex flex-col min-h-screen" x-data="indexDashboard()">
|
|
|
|
{{template "views/partials/navbar" .}}
|
|
|
|
<!-- Notifications -->
|
|
<div class="fixed top-20 right-4 z-50 space-y-2" style="max-width: 400px;">
|
|
<template x-for="notification in notifications" :key="notification.id">
|
|
<div x-show="true"
|
|
x-transition:enter="transition ease-out duration-200"
|
|
x-transition:enter-start="opacity-0"
|
|
x-transition:enter-end="opacity-100"
|
|
x-transition:leave="transition ease-in duration-150"
|
|
x-transition:leave-start="opacity-100"
|
|
x-transition:leave-end="opacity-0"
|
|
:class="notification.type === 'error' ? 'bg-red-500' : 'bg-[var(--color-success)]'"
|
|
class="rounded-lg p-4 text-white flex items-start space-x-3">
|
|
<div class="flex-shrink-0">
|
|
<i :class="notification.type === 'error' ? 'fas fa-exclamation-circle' : 'fas fa-check-circle'" class="text-xl"></i>
|
|
</div>
|
|
<div class="flex-1 min-w-0">
|
|
<p class="text-sm font-medium break-words" x-text="notification.message"></p>
|
|
</div>
|
|
<button @click="dismissNotification(notification.id)" class="flex-shrink-0 text-white hover:opacity-80 transition-opacity">
|
|
<i class="fas fa-times"></i>
|
|
</button>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
|
|
<div class="container mx-auto px-4 py-8 flex-grow">
|
|
<!-- Hero Header -->
|
|
<div class="hero-section">
|
|
<div class="hero-content">
|
|
<h1 class="hero-title">
|
|
Model & Backend Management
|
|
</h1>
|
|
<p class="hero-subtitle">Manage your installed models and backends</p>
|
|
|
|
<!-- Quick Actions -->
|
|
<div class="flex flex-wrap justify-center gap-3">
|
|
<a href="browse/" class="btn-primary text-sm py-1.5 px-3">
|
|
<i class="fas fa-images mr-1.5 text-[10px]"></i>
|
|
<span>Model Gallery</span>
|
|
</a>
|
|
|
|
<a href="/import-model" class="btn-primary text-sm py-1.5 px-3">
|
|
<i class="fas fa-plus mr-1.5 text-[10px]"></i>
|
|
<span>Import Model</span>
|
|
</a>
|
|
|
|
<button id="reload-models-btn" class="btn-primary text-sm py-1.5 px-3">
|
|
<i class="fas fa-sync-alt mr-1.5 text-[10px]"></i>
|
|
<span>Update Models</span>
|
|
</button>
|
|
|
|
<a href="/browse/backends" class="btn-secondary text-sm py-1.5 px-3">
|
|
<i class="fas fa-cogs mr-1.5 text-[10px]"></i>
|
|
<span>Backend Gallery</span>
|
|
</a>
|
|
|
|
{{ if not .DisableRuntimeSettings }}
|
|
<a href="/settings" class="btn-secondary text-sm py-1.5 px-3">
|
|
<i class="fas fa-cog mr-1.5 text-[10px]"></i>
|
|
<span>Settings</span>
|
|
</a>
|
|
{{ end }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Models Section -->
|
|
<div class="models mt-8">
|
|
{{template "views/partials/inprogress" .}}
|
|
|
|
{{ if eq (len .ModelsConfig) 0 }}
|
|
<!-- No Models State -->
|
|
<div class="card p-8">
|
|
<div class="text-center max-w-4xl mx-auto">
|
|
<div class="inline-flex items-center justify-center w-12 h-12 rounded-full bg-yellow-500/10 border border-yellow-500/20 mb-4">
|
|
<i class="text-yellow-400 text-xl fas fa-robot"></i>
|
|
</div>
|
|
<h2 class="h2 mb-2">No models installed yet</h2>
|
|
<p class="text-sm text-[var(--color-text-secondary)] mb-6">Get started by installing a model from the gallery or importing it</p>
|
|
|
|
<div class="flex flex-wrap justify-center gap-2 mb-6">
|
|
<a href="browse" class="btn-primary text-sm py-1.5 px-3">
|
|
<i class="fas fa-images mr-1.5 text-[10px]"></i>
|
|
Browse Model Gallery
|
|
</a>
|
|
<a href="/import-model" class="btn-primary text-sm py-1.5 px-3">
|
|
<i class="fas fa-upload mr-1.5 text-[10px]"></i>
|
|
Import Model
|
|
</a>
|
|
<a href="https://localai.io/basics/getting_started/" target="_blank" class="btn-secondary text-sm py-1.5 px-3">
|
|
<i class="fas fa-book mr-1.5 text-[10px]"></i>
|
|
Documentation
|
|
</a>
|
|
</div>
|
|
|
|
{{ if ne (len .Models) 0 }}
|
|
<div class="mt-8 pt-6 border-t border-[var(--color-primary-border)]/20">
|
|
<h3 class="text-lg font-semibold text-[var(--color-text-primary)] mb-2 flex items-center">
|
|
<i class="fas fa-file-alt mr-2 text-[var(--color-primary)] text-sm"></i>
|
|
Detected Model Files
|
|
</h3>
|
|
<p class="text-xs text-[var(--color-text-secondary)] mb-4">These models were found but don't have configuration files yet</p>
|
|
<div class="flex flex-wrap gap-2 justify-center">
|
|
{{ range .Models }}
|
|
<div class="bg-[var(--color-bg-primary)] border border-[var(--color-primary-border)]/20 rounded px-2 py-1 flex items-center gap-2">
|
|
<i class="fas fa-brain text-xs text-[var(--color-primary)]"></i>
|
|
<span class="text-xs text-[var(--color-text-primary)] font-medium">{{.}}</span>
|
|
</div>
|
|
{{end}}
|
|
</div>
|
|
</div>
|
|
{{end}}
|
|
</div>
|
|
</div>
|
|
{{ else }}
|
|
<!-- Models Table -->
|
|
{{ $modelsN := len .ModelsConfig}}
|
|
{{ $modelsN = add $modelsN (len .Models)}}
|
|
<div class="mb-6">
|
|
<h2 class="h3 mb-1 flex items-center">
|
|
<i class="fas fa-brain mr-2 text-[var(--color-primary)] text-sm"></i>
|
|
Installed Models
|
|
</h2>
|
|
<p class="text-sm text-[var(--color-text-secondary)] mb-4">
|
|
<span class="text-[var(--color-primary)] font-medium">{{$modelsN}}</span> model{{if gt $modelsN 1}}s{{end}} ready to use
|
|
</p>
|
|
</div>
|
|
|
|
<div class="overflow-x-auto mb-8">
|
|
<table class="w-full border-collapse">
|
|
<thead>
|
|
<tr class="border-b border-[var(--color-bg-secondary)]">
|
|
<th class="text-left p-2 text-xs font-semibold text-[var(--color-text-secondary)]">Name</th>
|
|
<th class="text-left p-2 text-xs font-semibold text-[var(--color-text-secondary)]">Status</th>
|
|
<th class="text-left p-2 text-xs font-semibold text-[var(--color-text-secondary)]">Backend</th>
|
|
<th class="text-left p-2 text-xs font-semibold text-[var(--color-text-secondary)]">Use Cases</th>
|
|
<th class="text-right p-2 text-xs font-semibold text-[var(--color-text-secondary)]">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{{$galleryConfig:=.GalleryConfig}}
|
|
{{ $loadedModels := .LoadedModels }}
|
|
|
|
{{ range .ModelsConfig }}
|
|
{{ $backendCfg := . }}
|
|
{{ $cfg:= index $galleryConfig .Name}}
|
|
<tr class="hover:bg-[var(--color-bg-secondary)]/50 border-b border-[var(--color-bg-secondary)] transition-colors">
|
|
<!-- Name Column -->
|
|
<td class="p-2">
|
|
<div class="flex items-center gap-2">
|
|
<div class="relative flex-shrink-0">
|
|
{{ if and $cfg $cfg.Icon }}
|
|
<img src="{{$cfg.Icon}}" class="w-4 h-4 object-contain" alt="{{.Name}} icon">
|
|
{{ else }}
|
|
<i class="fas fa-brain text-xs text-[var(--color-primary)]"></i>
|
|
{{ end }}
|
|
{{ if index $loadedModels .Name }}
|
|
<div class="absolute -top-0.5 -right-0.5 w-2 h-2 bg-[var(--color-success)] rounded-full border border-[var(--color-bg-secondary)]"></div>
|
|
{{ end }}
|
|
</div>
|
|
<span class="text-xs text-[var(--color-text-primary)] font-medium truncate">{{.Name}}</span>
|
|
<a href="/models/edit/{{.Name}}"
|
|
class="text-[var(--color-primary)]/60 hover:text-[var(--color-primary)] hover:bg-[var(--color-primary)]/10 rounded p-0.5 transition-colors ml-1 flex-shrink-0"
|
|
title="Edit {{.Name}}">
|
|
<i class="fas fa-edit text-[10px]"></i>
|
|
</a>
|
|
</div>
|
|
</td>
|
|
|
|
<!-- Status Column -->
|
|
<td class="p-2">
|
|
<div class="flex flex-wrap gap-1">
|
|
{{ if index $loadedModels .Name }}
|
|
<span class="inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-medium bg-[var(--color-success)]/10 text-green-300">
|
|
<i class="fas fa-circle text-[8px] mr-1"></i>Running
|
|
</span>
|
|
{{ end }}
|
|
{{ if and $backendCfg (or (ne $backendCfg.MCP.Servers "") (ne $backendCfg.MCP.Stdio "")) }}
|
|
<span class="inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-medium bg-[var(--color-accent-light)] text-[var(--color-accent)]">
|
|
<i class="fas fa-plug text-[8px] mr-1"></i>MCP
|
|
</span>
|
|
{{ end }}
|
|
</div>
|
|
</td>
|
|
|
|
<!-- Backend Column -->
|
|
<td class="p-2">
|
|
{{ if .Backend }}
|
|
<span class="inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-medium bg-[var(--color-primary)]/10 text-[var(--color-primary)]">
|
|
<i class="fas fa-cog text-[8px] mr-1"></i>{{.Backend}}
|
|
</span>
|
|
{{ else }}
|
|
<span class="inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-medium bg-yellow-500/10 text-yellow-300">
|
|
<i class="fas fa-magic text-[8px] mr-1"></i>Auto
|
|
</span>
|
|
{{ end }}
|
|
</td>
|
|
|
|
<!-- Use Cases Column -->
|
|
<td class="p-2">
|
|
<div class="flex flex-wrap gap-1">
|
|
{{ range .KnownUsecaseStrings }}
|
|
{{ if eq . "FLAG_CHAT" }}
|
|
<a href="chat/{{$backendCfg.Name}}" onclick="sessionStorage.setItem('localai_create_new_chat', 'true');" class="inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-medium bg-[var(--color-primary)]/10 text-[var(--color-primary)] hover:bg-[var(--color-primary)]/20 transition-colors" title="Chat">
|
|
<i class="fas fa-comment-alt text-[8px] mr-1"></i>Chat
|
|
</a>
|
|
{{ end }}
|
|
{{ if eq . "FLAG_IMAGE" }}
|
|
<a href="text2image/{{$backendCfg.Name}}" class="inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-medium bg-[var(--color-success)]/10 text-green-300 hover:bg-[var(--color-success)]/20 transition-colors" title="Image">
|
|
<i class="fas fa-image text-[8px] mr-1"></i>Image
|
|
</a>
|
|
{{ end }}
|
|
{{ if eq . "FLAG_TTS" }}
|
|
<a href="tts/{{$backendCfg.Name}}" class="inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-medium bg-[var(--color-accent-light)] text-[var(--color-accent)] hover:bg-[var(--color-accent-light)] transition-colors" title="TTS">
|
|
<i class="fas fa-microphone text-[8px] mr-1"></i>TTS
|
|
</a>
|
|
{{ end }}
|
|
{{ end }}
|
|
</div>
|
|
</td>
|
|
|
|
<!-- Actions Column -->
|
|
<td class="p-2">
|
|
<div class="flex items-center justify-end gap-1">
|
|
{{ if index $loadedModels .Name }}
|
|
<button class="text-red-400/60 hover:text-red-400 hover:bg-red-500/10 rounded p-1 transition-colors"
|
|
onclick="handleStopModel('{{.Name}}')"
|
|
title="Stop {{.Name}}">
|
|
<i class="fas fa-stop text-xs"></i>
|
|
</button>
|
|
{{ end }}
|
|
<button class="text-red-400/60 hover:text-red-400 hover:bg-red-500/10 rounded p-1 transition-colors"
|
|
onclick="handleDeleteModel('{{.Name}}')"
|
|
title="Delete {{.Name}}">
|
|
<i class="fas fa-trash-alt text-xs"></i>
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
{{ end }}
|
|
|
|
<!-- Models without config -->
|
|
{{ range .Models }}
|
|
<tr class="hover:bg-[var(--color-bg-secondary)]/50 border-b border-[var(--color-bg-secondary)] transition-colors">
|
|
<td class="p-2">
|
|
<div class="flex items-center gap-2">
|
|
<i class="fas fa-brain text-xs text-[var(--color-text-secondary)]"></i>
|
|
<span class="text-xs text-[var(--color-text-primary)] font-medium truncate">{{.}}</span>
|
|
</div>
|
|
</td>
|
|
<td class="p-2">
|
|
<span class="inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-medium bg-orange-500/10 text-orange-300">
|
|
<i class="fas fa-exclamation-triangle text-[8px] mr-1"></i>No Config
|
|
</span>
|
|
</td>
|
|
<td class="p-2">
|
|
<span class="inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-medium bg-yellow-500/10 text-yellow-300">
|
|
<i class="fas fa-magic text-[8px] mr-1"></i>Auto
|
|
</span>
|
|
</td>
|
|
<td class="p-2">
|
|
<span class="text-xs text-[var(--color-text-secondary)]">—</span>
|
|
</td>
|
|
<td class="p-2">
|
|
<span class="text-xs text-[var(--color-text-secondary)]">—</span>
|
|
</td>
|
|
</tr>
|
|
{{end}}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{{ end }}
|
|
</div>
|
|
|
|
<!-- Backends Section -->
|
|
<div class="mt-8">
|
|
<div class="mb-6">
|
|
<div class="flex items-center justify-between mb-1">
|
|
<h2 class="h3 flex items-center">
|
|
<i class="fas fa-cogs mr-2 text-[var(--color-accent)] text-sm"></i>
|
|
Installed Backends
|
|
</h2>
|
|
{{ if gt (len .InstalledBackends) 0 }}
|
|
<button
|
|
@click="reinstallAllBackends()"
|
|
:disabled="reinstallingAll"
|
|
class="btn-primary text-sm py-1.5 px-3"
|
|
title="Reinstall all backends">
|
|
<i class="fas fa-arrow-rotate-right mr-1.5 text-[10px]" :class="reinstallingAll ? 'fa-spin' : ''"></i>
|
|
<span x-text="reinstallingAll ? 'Reinstalling...' : 'Reinstall All'"></span>
|
|
</button>
|
|
{{ end }}
|
|
</div>
|
|
<p class="text-sm text-[var(--color-text-secondary)] mb-4">
|
|
<span class="text-[var(--color-accent)] font-medium">{{len .InstalledBackends}}</span> backend{{if gt (len .InstalledBackends) 1}}s{{end}} ready to use
|
|
</p>
|
|
</div>
|
|
|
|
{{ if eq (len .InstalledBackends) 0 }}
|
|
<!-- No backends state -->
|
|
<div class="card p-8">
|
|
<div class="text-center max-w-4xl mx-auto">
|
|
<div class="inline-flex items-center justify-center w-12 h-12 rounded-full bg-[var(--color-accent-light)] border border-[var(--color-accent-border)] mb-4">
|
|
<i class="text-[var(--color-accent)] text-xl fas fa-cogs"></i>
|
|
</div>
|
|
<h2 class="h2 mb-2">No backends installed yet</h2>
|
|
<p class="text-sm text-[var(--color-text-secondary)] mb-6">Backends power your AI models. Install them from the backend gallery to get started</p>
|
|
|
|
<div class="flex flex-wrap justify-center gap-3">
|
|
<a href="/browse/backends" class="btn-primary">
|
|
<i class="fas fa-cogs mr-2 text-xs"></i>
|
|
Browse Backend Gallery
|
|
</a>
|
|
<a href="https://localai.io/backends/" target="_blank" class="btn-secondary">
|
|
<i class="fas fa-book mr-2 text-xs"></i>
|
|
Documentation
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{{ else }}
|
|
<!-- Backends Table -->
|
|
<div class="overflow-x-auto mb-8">
|
|
<table class="w-full border-collapse">
|
|
<thead>
|
|
<tr class="border-b border-[var(--color-bg-secondary)]">
|
|
<th class="text-left p-2 text-xs font-semibold text-[var(--color-text-secondary)]">Name</th>
|
|
<th class="text-left p-2 text-xs font-semibold text-[var(--color-text-secondary)]">Type</th>
|
|
<th class="text-left p-2 text-xs font-semibold text-[var(--color-text-secondary)]">Metadata</th>
|
|
<th class="text-right p-2 text-xs font-semibold text-[var(--color-text-secondary)]">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{{ range .InstalledBackends }}
|
|
<tr class="hover:bg-[var(--color-bg-secondary)]/50 border-b border-[var(--color-bg-secondary)] transition-colors" data-backend-name="{{.Name}}" data-is-system="{{.IsSystem}}">
|
|
<!-- Name Column -->
|
|
<td class="p-2">
|
|
<div class="flex items-center gap-2">
|
|
<i class="fas fa-cog text-xs text-[var(--color-accent)]"></i>
|
|
<span class="text-xs text-[var(--color-text-primary)] font-medium truncate">{{.Name}}</span>
|
|
</div>
|
|
</td>
|
|
|
|
<!-- Type Column -->
|
|
<td class="p-2">
|
|
<div class="flex flex-wrap gap-1">
|
|
{{ if .IsSystem }}
|
|
<span class="inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-medium bg-blue-500/10 text-blue-300">
|
|
<i class="fas fa-shield-alt text-[8px] mr-1"></i>System
|
|
</span>
|
|
{{ else }}
|
|
<span class="inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-medium bg-[var(--color-success)]/10 text-green-300">
|
|
<i class="fas fa-download text-[8px] mr-1"></i>User
|
|
</span>
|
|
{{ end }}
|
|
{{ if .IsMeta }}
|
|
<span class="inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-medium bg-[var(--color-accent-light)] text-[var(--color-accent)]">
|
|
<i class="fas fa-layer-group text-[8px] mr-1"></i>Meta
|
|
</span>
|
|
{{ end }}
|
|
</div>
|
|
</td>
|
|
|
|
<!-- Metadata Column -->
|
|
<td class="p-2">
|
|
<div class="flex flex-col gap-1">
|
|
{{ if and .Metadata .Metadata.Alias }}
|
|
<span class="text-xs text-[var(--color-text-secondary)]">
|
|
<i class="fas fa-tag text-[8px] mr-1"></i>Alias: <span class="text-[var(--color-text-primary)]">{{.Metadata.Alias}}</span>
|
|
</span>
|
|
{{ end }}
|
|
{{ if and .Metadata .Metadata.MetaBackendFor }}
|
|
<span class="text-xs text-[var(--color-text-secondary)]">
|
|
<i class="fas fa-link text-[8px] mr-1"></i>For: <span class="text-[var(--color-accent)]">{{.Metadata.MetaBackendFor}}</span>
|
|
</span>
|
|
{{ end }}
|
|
{{ if and .Metadata .Metadata.InstalledAt }}
|
|
<span class="text-xs text-[var(--color-text-secondary)]">
|
|
<i class="fas fa-calendar text-[8px] mr-1"></i>{{.Metadata.InstalledAt}}
|
|
</span>
|
|
{{ end }}
|
|
</div>
|
|
</td>
|
|
|
|
<!-- Actions Column -->
|
|
<td class="p-2">
|
|
<div class="flex items-center justify-end gap-1">
|
|
{{ if not .IsSystem }}
|
|
<button
|
|
@click="reinstallBackend('{{.Name}}')"
|
|
:disabled="reinstallingBackends['{{.Name}}']"
|
|
class="text-[var(--color-primary)]/60 hover:text-[var(--color-primary)] hover:bg-[var(--color-primary)]/10 disabled:opacity-50 disabled:cursor-not-allowed rounded p-1 transition-colors"
|
|
title="Reinstall {{.Name}}">
|
|
<i class="fas fa-arrow-rotate-right text-xs" :class="reinstallingBackends['{{.Name}}'] ? 'fa-spin' : ''"></i>
|
|
</button>
|
|
<button
|
|
@click="deleteBackend('{{.Name}}')"
|
|
class="text-red-400/60 hover:text-red-400 hover:bg-red-500/10 rounded p-1 transition-colors"
|
|
title="Delete {{.Name}}">
|
|
<i class="fas fa-trash-alt text-xs"></i>
|
|
</button>
|
|
{{ else }}
|
|
<span class="text-xs text-[var(--color-text-secondary)]">—</span>
|
|
{{ end }}
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
{{end}}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{{ end }}
|
|
</div>
|
|
</div>
|
|
|
|
{{template "views/partials/footer" .}}
|
|
</div>
|
|
|
|
<script>
|
|
// Alpine.js component for index dashboard
|
|
function indexDashboard() {
|
|
return {
|
|
notifications: [],
|
|
reinstallingBackends: {},
|
|
reinstallingAll: false,
|
|
backendJobs: {},
|
|
|
|
init() {
|
|
// Poll for job progress every 600ms
|
|
setInterval(() => this.pollJobs(), 600);
|
|
},
|
|
|
|
addNotification(message, type = 'success') {
|
|
const id = Date.now();
|
|
this.notifications.push({ id, message, type });
|
|
// Auto-dismiss after 5 seconds
|
|
setTimeout(() => this.dismissNotification(id), 5000);
|
|
},
|
|
|
|
dismissNotification(id) {
|
|
this.notifications = this.notifications.filter(n => n.id !== id);
|
|
},
|
|
|
|
async reinstallBackend(backendName) {
|
|
if (this.reinstallingBackends[backendName]) {
|
|
return; // Already reinstalling
|
|
}
|
|
|
|
try {
|
|
this.reinstallingBackends[backendName] = true;
|
|
const response = await fetch(`/api/backends/install/${encodeURIComponent(backendName)}`, {
|
|
method: 'POST'
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (response.ok && data.jobID) {
|
|
this.backendJobs[backendName] = data.jobID;
|
|
this.addNotification(`Reinstalling backend "${backendName}"...`, 'success');
|
|
} else {
|
|
this.reinstallingBackends[backendName] = false;
|
|
this.addNotification(`Failed to start reinstall: ${data.error || 'Unknown error'}`, 'error');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error reinstalling backend:', error);
|
|
this.reinstallingBackends[backendName] = false;
|
|
this.addNotification(`Failed to reinstall backend: ${error.message}`, 'error');
|
|
}
|
|
},
|
|
|
|
async reinstallAllBackends() {
|
|
if (this.reinstallingAll) {
|
|
return; // Already reinstalling
|
|
}
|
|
|
|
if (!confirm('Are you sure you want to reinstall all backends? This may take some time.')) {
|
|
return;
|
|
}
|
|
|
|
this.reinstallingAll = true;
|
|
|
|
// Get all non-system backends from the page using data attributes
|
|
const backendRows = document.querySelectorAll('tr[data-backend-name]');
|
|
const backendsToReinstall = [];
|
|
|
|
backendRows.forEach(row => {
|
|
const backendName = row.getAttribute('data-backend-name');
|
|
const isSystem = row.getAttribute('data-is-system') === 'true';
|
|
if (backendName && !isSystem && !this.reinstallingBackends[backendName]) {
|
|
backendsToReinstall.push(backendName);
|
|
}
|
|
});
|
|
|
|
if (backendsToReinstall.length === 0) {
|
|
this.reinstallingAll = false;
|
|
this.addNotification('No backends available to reinstall', 'error');
|
|
return;
|
|
}
|
|
|
|
this.addNotification(`Starting reinstall of ${backendsToReinstall.length} backend(s)...`, 'success');
|
|
|
|
// Reinstall all backends sequentially to avoid overwhelming the system
|
|
for (const backendName of backendsToReinstall) {
|
|
await this.reinstallBackend(backendName);
|
|
// Small delay between installations
|
|
await new Promise(resolve => setTimeout(resolve, 500));
|
|
}
|
|
|
|
// Don't set reinstallingAll to false here - let pollJobs handle it when all jobs complete
|
|
// This allows the UI to show the batch operation is in progress
|
|
},
|
|
|
|
async pollJobs() {
|
|
for (const [backendName, jobID] of Object.entries(this.backendJobs)) {
|
|
try {
|
|
const response = await fetch(`/api/backends/job/${jobID}`);
|
|
const jobData = await response.json();
|
|
|
|
if (jobData.completed) {
|
|
delete this.backendJobs[backendName];
|
|
this.reinstallingBackends[backendName] = false;
|
|
this.addNotification(`Backend "${backendName}" reinstalled successfully!`, 'success');
|
|
|
|
// Only reload if not in batch mode and no other jobs are running
|
|
if (!this.reinstallingAll && Object.keys(this.backendJobs).length === 0) {
|
|
setTimeout(() => {
|
|
window.location.reload();
|
|
}, 1500);
|
|
}
|
|
}
|
|
|
|
if (jobData.error || (jobData.message && jobData.message.startsWith('error:'))) {
|
|
delete this.backendJobs[backendName];
|
|
this.reinstallingBackends[backendName] = false;
|
|
let errorMessage = 'Unknown error';
|
|
if (typeof jobData.error === 'string') {
|
|
errorMessage = jobData.error;
|
|
} else if (jobData.error && typeof jobData.error === 'object') {
|
|
const errorKeys = Object.keys(jobData.error);
|
|
if (errorKeys.length > 0) {
|
|
errorMessage = jobData.error.message || jobData.error.error || jobData.error.Error || JSON.stringify(jobData.error);
|
|
} else {
|
|
errorMessage = jobData.message || 'Unknown error';
|
|
}
|
|
} else if (jobData.message) {
|
|
errorMessage = jobData.message;
|
|
}
|
|
if (errorMessage.startsWith('error: ')) {
|
|
errorMessage = errorMessage.substring(7);
|
|
}
|
|
this.addNotification(`Error reinstalling backend "${backendName}": ${errorMessage}`, 'error');
|
|
|
|
// If batch mode and all jobs are done (completed or errored), reload
|
|
if (this.reinstallingAll && Object.keys(this.backendJobs).length === 0) {
|
|
this.reinstallingAll = false;
|
|
setTimeout(() => {
|
|
window.location.reload();
|
|
}, 2000);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Error polling job:', error);
|
|
}
|
|
}
|
|
|
|
// If batch mode completed and no jobs left, reload
|
|
if (this.reinstallingAll && Object.keys(this.backendJobs).length === 0) {
|
|
this.reinstallingAll = false;
|
|
setTimeout(() => {
|
|
window.location.reload();
|
|
}, 2000);
|
|
}
|
|
},
|
|
|
|
async deleteBackend(backendName) {
|
|
if (!confirm(`Are you sure you want to delete the backend "${backendName}"?`)) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(`/api/backends/system/delete/${encodeURIComponent(backendName)}`, {
|
|
method: 'POST'
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (response.ok && data.success) {
|
|
this.addNotification(`Backend "${backendName}" deleted successfully!`, 'success');
|
|
// Reload page after short delay
|
|
setTimeout(() => {
|
|
window.location.reload();
|
|
}, 1500);
|
|
} else {
|
|
this.addNotification(`Failed to delete backend: ${data.error || 'Unknown error'}`, 'error');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error deleting backend:', error);
|
|
this.addNotification(`Failed to delete backend: ${error.message}`, 'error');
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
async function handleStopModel(modelName) {
|
|
if (!confirm('Are you sure you wish to stop this model?')) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch('/backend/shutdown', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({ model: modelName })
|
|
});
|
|
|
|
if (response.ok) {
|
|
window.location.reload();
|
|
} else {
|
|
alert('Failed to stop model');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error stopping model:', error);
|
|
alert('Failed to stop model');
|
|
}
|
|
}
|
|
|
|
async function handleDeleteModel(modelName) {
|
|
if (!confirm('Are you sure you wish to delete this model?')) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(`/api/models/delete/${encodeURIComponent(modelName)}`, {
|
|
method: 'POST'
|
|
});
|
|
|
|
if (response.ok) {
|
|
window.location.reload();
|
|
} else {
|
|
alert('Failed to delete model');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error deleting model:', error);
|
|
alert('Failed to delete model');
|
|
}
|
|
}
|
|
|
|
// Handle reload models button
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
const reloadBtn = document.getElementById('reload-models-btn');
|
|
if (reloadBtn) {
|
|
reloadBtn.addEventListener('click', function() {
|
|
const button = this;
|
|
const originalText = button.querySelector('span').textContent;
|
|
const icon = button.querySelector('i');
|
|
|
|
// Show loading state
|
|
button.disabled = true;
|
|
button.querySelector('span').textContent = 'Updating...';
|
|
icon.classList.add('fa-spin');
|
|
|
|
// Make the API call
|
|
fetch('/models/reload', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
}
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
// Show success state briefly
|
|
button.querySelector('span').textContent = 'Updated!';
|
|
icon.classList.remove('fa-spin', 'fa-sync-alt');
|
|
icon.classList.add('fa-check');
|
|
|
|
// Reload the page after a short delay
|
|
setTimeout(() => {
|
|
window.location.reload();
|
|
}, 1000);
|
|
} else {
|
|
// Show error state
|
|
button.querySelector('span').textContent = 'Error!';
|
|
icon.classList.remove('fa-spin');
|
|
console.error('Failed to reload models:', data.error);
|
|
|
|
// Reset button after delay
|
|
setTimeout(() => {
|
|
button.disabled = false;
|
|
button.querySelector('span').textContent = originalText;
|
|
icon.classList.remove('fa-check');
|
|
icon.classList.add('fa-sync-alt');
|
|
}, 3000);
|
|
}
|
|
})
|
|
.catch(error => {
|
|
// Show error state
|
|
button.querySelector('span').textContent = 'Error!';
|
|
icon.classList.remove('fa-spin');
|
|
console.error('Error reloading models:', error);
|
|
|
|
// Reset button after delay
|
|
setTimeout(() => {
|
|
button.disabled = false;
|
|
button.querySelector('span').textContent = originalText;
|
|
icon.classList.remove('fa-check');
|
|
icon.classList.add('fa-sync-alt');
|
|
}, 3000);
|
|
});
|
|
});
|
|
}
|
|
});
|
|
</script>
|
|
|
|
</body>
|
|
</html>
|
|
|