mirror of
https://github.com/mudler/LocalAI.git
synced 2026-02-12 21:19:23 -06:00
feat(ui): General improvements (#6072)
* wip * Simplify stop Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * Improve UI Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * Show installed backends at the index Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * Imporve UI Signed-off-by: Ettore Di Giacinto <mudler@localai.io> --------- Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
This commit is contained in:
committed by
GitHub
parent
80f15851c5
commit
bef4c10629
@@ -14,7 +14,7 @@ import (
|
||||
func SystemInformations(ml *model.ModelLoader, appConfig *config.ApplicationConfig) func(*fiber.Ctx) error {
|
||||
return func(c *fiber.Ctx) error {
|
||||
availableBackends := []string{}
|
||||
loadedModels := ml.ListModels()
|
||||
loadedModels := ml.ListLoadedModels()
|
||||
for b := range appConfig.ExternalGRPCBackends {
|
||||
availableBackends = append(availableBackends, b)
|
||||
}
|
||||
|
||||
@@ -16,6 +16,15 @@ func WelcomeEndpoint(appConfig *config.ApplicationConfig,
|
||||
modelConfigs := cl.GetAllModelsConfigs()
|
||||
galleryConfigs := map[string]*gallery.ModelConfig{}
|
||||
|
||||
backends, _ := gallery.AvailableBackends(appConfig.BackendGalleries, appConfig.SystemState)
|
||||
|
||||
installedBackends := gallery.GalleryElements[*gallery.GalleryBackend]{}
|
||||
for _, b := range backends {
|
||||
if b.Installed {
|
||||
installedBackends = append(installedBackends, b)
|
||||
}
|
||||
}
|
||||
|
||||
for _, m := range modelConfigs {
|
||||
cfg, err := gallery.GetLocalModelConfiguration(ml.ModelPath, m.Name)
|
||||
if err != nil {
|
||||
@@ -24,6 +33,12 @@ func WelcomeEndpoint(appConfig *config.ApplicationConfig,
|
||||
galleryConfigs[m.Name] = cfg
|
||||
}
|
||||
|
||||
loadedModels := ml.ListLoadedModels()
|
||||
loadedModelsMap := map[string]bool{}
|
||||
for _, m := range loadedModels {
|
||||
loadedModelsMap[m.ID] = true
|
||||
}
|
||||
|
||||
modelsWithoutConfig, _ := services.ListModels(cl, ml, config.NoFilterFn, services.LOOSE_ONLY)
|
||||
|
||||
// Get model statuses to display in the UI the operation in progress
|
||||
@@ -39,6 +54,8 @@ func WelcomeEndpoint(appConfig *config.ApplicationConfig,
|
||||
"ApplicationConfig": appConfig,
|
||||
"ProcessingModels": processingModels,
|
||||
"TaskTypes": taskTypes,
|
||||
"LoadedModels": loadedModelsMap,
|
||||
"InstalledBackends": installedBackends,
|
||||
}
|
||||
|
||||
if string(c.Context().Request.Header.ContentType()) == "application/json" || len(c.Accepts("html")) == 0 {
|
||||
|
||||
@@ -48,6 +48,24 @@ function submitSystemPrompt(event) {
|
||||
document.getElementById("systemPrompt").blur();
|
||||
}
|
||||
|
||||
function handleShutdownResponse(event, modelName) {
|
||||
// Check if the request was successful
|
||||
if (event.detail.successful) {
|
||||
// Show a success message (optional)
|
||||
console.log(`Model ${modelName} stopped successfully`);
|
||||
|
||||
// Refresh the page to update the UI
|
||||
window.location.reload();
|
||||
} else {
|
||||
// Show an error message (optional)
|
||||
console.error(`Failed to stop model ${modelName}`);
|
||||
|
||||
// You could also show a user-friendly error message here
|
||||
// For now, we'll still refresh to show the current state
|
||||
window.location.reload();
|
||||
}
|
||||
}
|
||||
|
||||
var images = [];
|
||||
var audios = [];
|
||||
var fileContents = [];
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<html lang="en">
|
||||
{{template "views/partials/head" .}}
|
||||
|
||||
<body class="bg-gradient-to-br from-gray-900 to-gray-950 text-gray-200">
|
||||
<body class="bg-gradient-to-br from-gray-900 via-gray-950 to-black text-gray-200">
|
||||
<div class="flex flex-col min-h-screen">
|
||||
|
||||
{{template "views/partials/navbar" .}}
|
||||
@@ -10,94 +10,128 @@
|
||||
<div class="container mx-auto px-4 py-8 flex-grow">
|
||||
|
||||
<!-- Hero Header -->
|
||||
<div class="bg-gradient-to-r from-indigo-900/30 to-purple-900/30 rounded-2xl shadow-xl p-6 mb-8">
|
||||
<div class="max-w-4xl mx-auto text-center">
|
||||
<h1 class="text-3xl md:text-4xl font-bold text-white mb-3">
|
||||
<span class="bg-clip-text text-transparent bg-gradient-to-r from-indigo-400 to-purple-400">
|
||||
<div class="relative bg-gradient-to-r from-emerald-900/40 via-teal-900/30 to-cyan-900/40 rounded-3xl shadow-2xl p-8 mb-12 overflow-hidden">
|
||||
<!-- Background Pattern -->
|
||||
<div class="absolute inset-0 opacity-10">
|
||||
<div class="absolute inset-0 bg-gradient-to-r from-emerald-500/20 to-cyan-500/20"></div>
|
||||
<div class="absolute top-0 left-0 w-full h-full" style="background-image: radial-gradient(circle at 1px 1px, rgba(255,255,255,0.15) 1px, transparent 0); background-size: 20px 20px;"></div>
|
||||
</div>
|
||||
|
||||
<div class="relative max-w-5xl mx-auto text-center">
|
||||
<h1 class="text-4xl md:text-5xl font-bold text-white mb-4">
|
||||
<span class="bg-clip-text text-transparent bg-gradient-to-r from-emerald-400 via-teal-400 to-cyan-400">
|
||||
Backend Management
|
||||
</span>
|
||||
</h1>
|
||||
<p class="text-lg text-gray-300 mb-2">
|
||||
<span class="font-semibold text-indigo-300">{{.AvailableBackends}}</span> backends available
|
||||
<a href="https://localai.io/backends/" target="_blank" class="ml-2 text-blue-400 hover:text-blue-300 transition">
|
||||
<i class="fas fa-circle-info"></i>
|
||||
</a>
|
||||
<p class="text-lg md:text-xl text-gray-300 mb-6 font-light">
|
||||
Discover and install AI backends to power your models
|
||||
</p>
|
||||
<div class="flex flex-wrap justify-center items-center gap-6 text-sm md:text-base">
|
||||
<div class="flex items-center bg-white/10 rounded-full px-4 py-2">
|
||||
<div class="w-2 h-2 bg-emerald-400 rounded-full mr-2 animate-pulse"></div>
|
||||
<span class="font-semibold text-emerald-300">{{.AvailableBackends}}</span>
|
||||
<span class="text-gray-300 ml-1">backends available</span>
|
||||
</div>
|
||||
<a href="https://localai.io/backends/" target="_blank"
|
||||
class="flex items-center bg-cyan-600/80 hover:bg-cyan-600 text-white px-4 py-2 rounded-full transition-all duration-300 hover:scale-105">
|
||||
<i class="fas fa-info-circle mr-2"></i>
|
||||
<span>Documentation</span>
|
||||
<i class="fas fa-external-link-alt ml-2 text-xs"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{template "views/partials/inprogress" .}}
|
||||
|
||||
<!-- Search and Filter Section -->
|
||||
<div class="bg-gray-800/70 rounded-xl p-6 mb-8 shadow-lg border border-gray-700/50">
|
||||
<!-- Search Input -->
|
||||
<div class="relative mb-6">
|
||||
<div class="absolute inset-y-0 start-0 flex items-center ps-3 pointer-events-none">
|
||||
<i class="fas fa-search text-gray-400"></i>
|
||||
</div>
|
||||
<input class="form-control block w-full pl-10 px-4 py-3 text-base font-normal text-gray-300 bg-gray-900/80 bg-clip-padding border border-gray-700/70 rounded-lg transition ease-in-out focus:text-gray-200 focus:bg-gray-900 focus:border-blue-500 focus:ring-1 focus:ring-blue-500/50 focus:outline-none"
|
||||
type="search"
|
||||
name="search"
|
||||
placeholder="Search backends by name, description or type..."
|
||||
hx-post="browse/search/backends"
|
||||
hx-trigger="input changed delay:500ms, search"
|
||||
hx-target="#search-results"
|
||||
oninput="hidePagination()"
|
||||
onchange="hidePagination()"
|
||||
onsearch="hidePagination()"
|
||||
hx-indicator=".htmx-indicator">
|
||||
<span class="htmx-indicator absolute right-3 top-3">
|
||||
<svg class="animate-spin h-5 w-5 text-blue-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
<div class="relative bg-gradient-to-br from-gray-800/80 to-gray-900/80 rounded-2xl p-8 mb-8 shadow-xl border border-gray-700/50 backdrop-blur-sm">
|
||||
<div class="absolute inset-0 rounded-2xl bg-gradient-to-br from-emerald-500/5 to-cyan-500/5"></div>
|
||||
|
||||
<!-- Filter by Type -->
|
||||
<div class="mb-4">
|
||||
<h3 class="text-gray-200 font-medium mb-3">Filter by type:</h3>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button hx-post="browse/search/backends"
|
||||
class="inline-flex items-center rounded-full px-4 py-2 text-sm font-medium bg-indigo-900/60 text-indigo-200 border border-indigo-700/50 hover:bg-indigo-800 transition duration-200 ease-in-out"
|
||||
hx-target="#search-results"
|
||||
hx-vals='{"search": "llm"}'
|
||||
onclick="hidePagination()"
|
||||
hx-indicator=".htmx-indicator">
|
||||
<i class="fas fa-brain mr-2"></i>LLM
|
||||
</button>
|
||||
<button hx-post="browse/search/backends"
|
||||
class="inline-flex items-center rounded-full px-4 py-2 text-sm font-medium bg-purple-900/60 text-purple-200 border border-purple-700/50 hover:bg-purple-800 transition duration-200 ease-in-out"
|
||||
hx-target="#search-results"
|
||||
hx-vals='{"search": "diffusion"}'
|
||||
onclick="hidePagination()"
|
||||
hx-indicator=".htmx-indicator">
|
||||
<i class="fas fa-image mr-2"></i>Diffusion
|
||||
</button>
|
||||
<button hx-post="browse/search/backends"
|
||||
class="inline-flex items-center rounded-full px-4 py-2 text-sm font-medium bg-blue-900/60 text-blue-200 border border-blue-700/50 hover:bg-blue-800 transition duration-200 ease-in-out"
|
||||
hx-target="#search-results"
|
||||
hx-vals='{"search": "tts"}'
|
||||
onclick="hidePagination()"
|
||||
hx-indicator=".htmx-indicator">
|
||||
<i class="fas fa-microphone mr-2"></i>TTS
|
||||
</button>
|
||||
<button hx-post="browse/search/backends"
|
||||
class="inline-flex items-center rounded-full px-4 py-2 text-sm font-medium bg-green-900/60 text-green-200 border border-green-700/50 hover:bg-green-800 transition duration-200 ease-in-out"
|
||||
hx-target="#search-results"
|
||||
hx-vals='{"search": "whisper"}'
|
||||
onclick="hidePagination()"
|
||||
hx-indicator=".htmx-indicator">
|
||||
<i class="fas fa-headphones mr-2"></i>Whisper
|
||||
</button>
|
||||
<button hx-post="browse/search/backends"
|
||||
class="inline-flex items-center rounded-full px-4 py-2 text-sm font-medium bg-red-900/60 text-red-200 border border-red-700/50 hover:bg-red-800 transition duration-200 ease-in-out"
|
||||
hx-target="#search-results"
|
||||
hx-vals='{"search": "object-detection"}'
|
||||
onclick="hidePagination()"
|
||||
hx-indicator=".htmx-indicator">
|
||||
<i class="fas fa-eye mr-2"></i>Object detection
|
||||
</button>
|
||||
<div class="relative">
|
||||
<!-- Search Input -->
|
||||
<div class="mb-8">
|
||||
<h3 class="text-xl font-semibold text-white mb-4 flex items-center">
|
||||
<i class="fas fa-search mr-3 text-emerald-400"></i>
|
||||
Find Backend Components
|
||||
</h3>
|
||||
<div class="relative">
|
||||
<div class="absolute inset-y-0 start-0 flex items-center ps-4 pointer-events-none">
|
||||
<i class="fas fa-search text-gray-400"></i>
|
||||
</div>
|
||||
<input class="w-full pl-12 pr-16 py-4 text-base font-normal text-gray-300 bg-gray-900/90 border border-gray-700/70 rounded-xl transition-all duration-300 focus:text-gray-200 focus:bg-gray-900 focus:border-emerald-500 focus:ring-2 focus:ring-emerald-500/50 focus:outline-none"
|
||||
type="search"
|
||||
name="search"
|
||||
placeholder="Search backends by name, description or type..."
|
||||
hx-post="browse/search/backends"
|
||||
hx-trigger="input changed delay:500ms, search"
|
||||
hx-target="#search-results"
|
||||
oninput="hidePagination()"
|
||||
onchange="hidePagination()"
|
||||
onsearch="hidePagination()"
|
||||
hx-indicator=".htmx-indicator">
|
||||
<span class="htmx-indicator absolute right-4 top-4">
|
||||
<svg class="animate-spin h-6 w-6 text-emerald-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filter by Type -->
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-white mb-4 flex items-center">
|
||||
<i class="fas fa-filter mr-3 text-teal-400"></i>
|
||||
Filter by Backend Type
|
||||
</h3>
|
||||
<div class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-3">
|
||||
<button hx-post="browse/search/backends"
|
||||
class="group flex items-center justify-center rounded-xl px-4 py-3 text-sm font-semibold bg-gradient-to-r from-indigo-600/80 to-indigo-700/80 hover:from-indigo-600 hover:to-indigo-700 text-indigo-100 border border-indigo-500/30 hover:border-indigo-400/50 transition-all duration-300 transform hover:scale-105 hover:shadow-lg hover:shadow-indigo-500/25"
|
||||
hx-target="#search-results"
|
||||
hx-vals='{"search": "llm"}'
|
||||
onclick="hidePagination()"
|
||||
hx-indicator=".htmx-indicator">
|
||||
<i class="fas fa-brain mr-2 group-hover:animate-pulse"></i>
|
||||
<span>LLM</span>
|
||||
</button>
|
||||
<button hx-post="browse/search/backends"
|
||||
class="group flex items-center justify-center rounded-xl px-4 py-3 text-sm font-semibold bg-gradient-to-r from-purple-600/80 to-purple-700/80 hover:from-purple-600 hover:to-purple-700 text-purple-100 border border-purple-500/30 hover:border-purple-400/50 transition-all duration-300 transform hover:scale-105 hover:shadow-lg hover:shadow-purple-500/25"
|
||||
hx-target="#search-results"
|
||||
hx-vals='{"search": "diffusion"}'
|
||||
onclick="hidePagination()"
|
||||
hx-indicator=".htmx-indicator">
|
||||
<i class="fas fa-image mr-2 group-hover:animate-pulse"></i>
|
||||
<span>Diffusion</span>
|
||||
</button>
|
||||
<button hx-post="browse/search/backends"
|
||||
class="group flex items-center justify-center rounded-xl px-4 py-3 text-sm font-semibold bg-gradient-to-r from-blue-600/80 to-blue-700/80 hover:from-blue-600 hover:to-blue-700 text-blue-100 border border-blue-500/30 hover:border-blue-400/50 transition-all duration-300 transform hover:scale-105 hover:shadow-lg hover:shadow-blue-500/25"
|
||||
hx-target="#search-results"
|
||||
hx-vals='{"search": "tts"}'
|
||||
onclick="hidePagination()"
|
||||
hx-indicator=".htmx-indicator">
|
||||
<i class="fas fa-microphone mr-2 group-hover:animate-pulse"></i>
|
||||
<span>TTS</span>
|
||||
</button>
|
||||
<button hx-post="browse/search/backends"
|
||||
class="group flex items-center justify-center rounded-xl px-4 py-3 text-sm font-semibold bg-gradient-to-r from-green-600/80 to-green-700/80 hover:from-green-600 hover:to-green-700 text-green-100 border border-green-500/30 hover:border-green-400/50 transition-all duration-300 transform hover:scale-105 hover:shadow-lg hover:shadow-green-500/25"
|
||||
hx-target="#search-results"
|
||||
hx-vals='{"search": "whisper"}'
|
||||
onclick="hidePagination()"
|
||||
hx-indicator=".htmx-indicator">
|
||||
<i class="fas fa-headphones mr-2 group-hover:animate-pulse"></i>
|
||||
<span>Whisper</span>
|
||||
</button>
|
||||
<button hx-post="browse/search/backends"
|
||||
class="group flex items-center justify-center rounded-xl px-4 py-3 text-sm font-semibold bg-gradient-to-r from-red-600/80 to-red-700/80 hover:from-red-600 hover:to-red-700 text-red-100 border border-red-500/30 hover:border-red-400/50 transition-all duration-300 transform hover:scale-105 hover:shadow-lg hover:shadow-red-500/25"
|
||||
hx-target="#search-results"
|
||||
hx-vals='{"search": "object-detection"}'
|
||||
onclick="hidePagination()"
|
||||
hx-indicator=".htmx-indicator">
|
||||
<i class="fas fa-eye mr-2 group-hover:animate-pulse"></i>
|
||||
<span>Vision</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -109,21 +143,24 @@
|
||||
|
||||
<!-- Pagination -->
|
||||
{{ if gt .AvailableBackends $numBackendsPerPage }}
|
||||
<div id="paginate" class="flex justify-center mt-8">
|
||||
<div class="flex items-center gap-4">
|
||||
<div id="paginate" class="flex justify-center mt-12">
|
||||
<div class="flex items-center gap-4 bg-gray-800/60 rounded-2xl p-4 backdrop-blur-sm border border-gray-700/50">
|
||||
{{ if .PrevPage }}
|
||||
<button onclick="window.location.href='browse/backends?page={{.PrevPage}}'"
|
||||
class="flex items-center justify-center h-10 w-10 bg-gray-800/80 text-gray-300 hover:bg-indigo-900/70 hover:text-white rounded-lg shadow transition duration-300 ease-in-out">
|
||||
<i class="fas fa-chevron-left"></i>
|
||||
class="group flex items-center justify-center h-12 w-12 bg-gray-700/80 hover:bg-emerald-600 text-gray-300 hover:text-white rounded-xl shadow-lg transition-all duration-300 ease-in-out transform hover:scale-110">
|
||||
<i class="fas fa-chevron-left group-hover:animate-pulse"></i>
|
||||
</button>
|
||||
{{ end }}
|
||||
<div class="text-gray-400 text-sm">
|
||||
Page <span class="text-white font-medium">{{.CurrentPage}}</span> of <span class="text-white font-medium">{{.TotalPages}}</span>
|
||||
<div class="text-gray-300 text-sm font-medium px-4">
|
||||
<span class="text-gray-400">Page</span>
|
||||
<span class="text-white font-bold text-lg mx-2">{{.CurrentPage}}</span>
|
||||
<span class="text-gray-400">of</span>
|
||||
<span class="text-white font-bold text-lg mx-2">{{.TotalPages}}</span>
|
||||
</div>
|
||||
{{ if .NextPage }}
|
||||
<button onclick="window.location.href='browse/backends?page={{.NextPage}}'"
|
||||
class="flex items-center justify-center h-10 w-10 bg-gray-800/80 text-gray-300 hover:bg-indigo-900/70 hover:text-white rounded-lg shadow transition duration-300 ease-in-out">
|
||||
<i class="fas fa-chevron-right"></i>
|
||||
class="group flex items-center justify-center h-12 w-12 bg-gray-700/80 hover:bg-emerald-600 text-gray-300 hover:text-white rounded-xl shadow-lg transition-all duration-300 ease-in-out transform hover:scale-110">
|
||||
<i class="fas fa-chevron-right group-hover:animate-pulse"></i>
|
||||
</button>
|
||||
{{ end }}
|
||||
</div>
|
||||
@@ -134,6 +171,135 @@
|
||||
{{template "views/partials/footer" .}}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* Enhanced scrollbar styling */
|
||||
.scrollbar-thin::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.scrollbar-thin::-webkit-scrollbar-track {
|
||||
background: rgba(31, 41, 55, 0.5);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.scrollbar-thin::-webkit-scrollbar-thumb {
|
||||
background: rgba(107, 114, 128, 0.5);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.scrollbar-thin::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(107, 114, 128, 0.8);
|
||||
}
|
||||
|
||||
/* Add some custom CSS for backend cards to match our theme */
|
||||
#search-results .dark\:bg-gray-800 {
|
||||
background: linear-gradient(135deg, rgba(31, 41, 55, 0.9) 0%, rgba(17, 24, 39, 0.9) 100%) !important;
|
||||
border: 1px solid rgba(75, 85, 99, 0.5) !important;
|
||||
border-radius: 1rem !important;
|
||||
transition: all 0.5s ease !important;
|
||||
backdrop-filter: blur(8px) !important;
|
||||
}
|
||||
|
||||
#search-results .dark\:bg-gray-800:hover {
|
||||
transform: translateY(-8px) !important;
|
||||
box-shadow: 0 25px 50px -12px rgba(16, 185, 129, 0.1) !important;
|
||||
border-color: rgba(16, 185, 129, 0.3) !important;
|
||||
}
|
||||
|
||||
/* Style the install buttons */
|
||||
#search-results .bg-blue-600 {
|
||||
background: linear-gradient(135deg, #10b981 0%, #059669 100%) !important;
|
||||
border-radius: 0.75rem !important;
|
||||
padding: 0.75rem 1.5rem !important;
|
||||
font-weight: 600 !important;
|
||||
transition: all 0.3s ease !important;
|
||||
box-shadow: 0 4px 15px rgba(16, 185, 129, 0.25) !important;
|
||||
}
|
||||
|
||||
#search-results .bg-blue-600:hover {
|
||||
background: linear-gradient(135deg, #059669 0%, #047857 100%) !important;
|
||||
transform: scale(1.05) !important;
|
||||
box-shadow: 0 8px 25px rgba(16, 185, 129, 0.4) !important;
|
||||
}
|
||||
|
||||
/* Style the reinstall buttons specifically */
|
||||
#search-results .bg-primary {
|
||||
background: linear-gradient(135deg, #06b6d4 0%, #0891b2 100%) !important;
|
||||
border-radius: 0.75rem !important;
|
||||
padding: 0.75rem 1.5rem !important;
|
||||
font-weight: 600 !important;
|
||||
transition: all 0.3s ease !important;
|
||||
box-shadow: 0 4px 15px rgba(6, 182, 212, 0.25) !important;
|
||||
}
|
||||
|
||||
#search-results .bg-primary:hover {
|
||||
background: linear-gradient(135deg, #0891b2 0%, #0e7490 100%) !important;
|
||||
transform: scale(1.05) !important;
|
||||
box-shadow: 0 8px 25px rgba(6, 182, 212, 0.4) !important;
|
||||
}
|
||||
|
||||
/* Style the delete buttons */
|
||||
#search-results .bg-red-800 {
|
||||
background: linear-gradient(135deg, #dc2626 0%, #b91c1c 100%) !important;
|
||||
border-radius: 0.75rem !important;
|
||||
padding: 0.75rem 1.5rem !important;
|
||||
font-weight: 600 !important;
|
||||
transition: all 0.3s ease !important;
|
||||
box-shadow: 0 4px 15px rgba(220, 38, 38, 0.25) !important;
|
||||
}
|
||||
|
||||
#search-results .bg-red-800:hover {
|
||||
background: linear-gradient(135deg, #b91c1c 0%, #991b1b 100%) !important;
|
||||
transform: scale(1.05) !important;
|
||||
box-shadow: 0 8px 25px rgba(220, 38, 38, 0.4) !important;
|
||||
}
|
||||
|
||||
/* Style the info buttons */
|
||||
#search-results .bg-gray-700 {
|
||||
background: linear-gradient(135deg, #374151 0%, #1f2937 100%) !important;
|
||||
border-radius: 0.75rem !important;
|
||||
padding: 0.75rem 1.5rem !important;
|
||||
font-weight: 600 !important;
|
||||
transition: all 0.3s ease !important;
|
||||
box-shadow: 0 4px 15px rgba(55, 65, 81, 0.25) !important;
|
||||
}
|
||||
|
||||
#search-results .bg-gray-700:hover {
|
||||
background: linear-gradient(135deg, #1f2937 0%, #111827 100%) !important;
|
||||
transform: scale(1.05) !important;
|
||||
box-shadow: 0 8px 25px rgba(55, 65, 81, 0.4) !important;
|
||||
}
|
||||
|
||||
/* Style the backend images */
|
||||
#search-results img.rounded-t-lg {
|
||||
border-radius: 1rem !important;
|
||||
transition: transform 0.3s ease !important;
|
||||
}
|
||||
|
||||
#search-results .dark\:bg-gray-800:hover img.rounded-t-lg {
|
||||
transform: scale(1.05) !important;
|
||||
}
|
||||
|
||||
/* Style the progress bars */
|
||||
#search-results .progress {
|
||||
background: linear-gradient(135deg, rgba(16, 185, 129, 0.2) 0%, rgba(6, 182, 212, 0.2) 100%) !important;
|
||||
border-radius: 0.5rem !important;
|
||||
border: 1px solid rgba(16, 185, 129, 0.3) !important;
|
||||
}
|
||||
|
||||
/* Style action buttons */
|
||||
#search-results button[class*="primary"] {
|
||||
background: linear-gradient(135deg, #06b6d4 0%, #0891b2 100%) !important;
|
||||
border-radius: 0.5rem !important;
|
||||
transition: all 0.2s ease !important;
|
||||
}
|
||||
|
||||
#search-results button[class*="primary"]:hover {
|
||||
transform: scale(1.05) !important;
|
||||
box-shadow: 0 4px 15px rgba(6, 182, 212, 0.3) !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
function hidePagination() {
|
||||
const paginateDiv = document.getElementById('paginate');
|
||||
@@ -149,7 +315,20 @@
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Add loading state animation
|
||||
document.body.addEventListener('htmx:beforeRequest', function(event) {
|
||||
const searchInput = document.querySelector('input[name="search"]');
|
||||
if (searchInput && event.detail.elt === searchInput) {
|
||||
searchInput.classList.add('animate-pulse');
|
||||
}
|
||||
});
|
||||
|
||||
document.body.addEventListener('htmx:afterRequest', function(event) {
|
||||
const searchInput = document.querySelector('input[name="search"]');
|
||||
if (searchInput) {
|
||||
searchInput.classList.remove('animate-pulse');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
@@ -2,39 +2,51 @@
|
||||
<html lang="en">
|
||||
{{template "views/partials/head" .}}
|
||||
|
||||
<body class="bg-gradient-to-br from-gray-900 to-gray-950 text-gray-200">
|
||||
<body class="bg-gradient-to-br from-gray-900 via-gray-950 to-black text-gray-200">
|
||||
<div class="flex flex-col min-h-screen">
|
||||
|
||||
{{template "views/partials/navbar" .}}
|
||||
|
||||
<div class="container mx-auto px-4 py-8 flex-grow">
|
||||
<!-- Hero Section -->
|
||||
<div class="bg-gradient-to-r from-blue-900/30 to-indigo-900/30 rounded-2xl shadow-xl p-8 mb-10">
|
||||
<div class="max-w-4xl mx-auto text-center">
|
||||
<h1 class="text-4xl md:text-5xl font-bold text-white mb-4">
|
||||
<span class="bg-clip-text text-transparent bg-gradient-to-r from-blue-400 to-indigo-400">
|
||||
Welcome to <i>your</i> LocalAI instance!
|
||||
<div class="relative bg-gradient-to-r from-blue-900/40 via-indigo-900/30 to-purple-900/40 rounded-3xl shadow-2xl p-8 mb-12 overflow-hidden">
|
||||
<!-- Background Pattern -->
|
||||
<div class="absolute inset-0 opacity-10">
|
||||
<div class="absolute inset-0 bg-gradient-to-r from-blue-500/20 to-purple-500/20"></div>
|
||||
<div class="absolute top-0 left-0 w-full h-full" style="background-image: radial-gradient(circle at 1px 1px, rgba(255,255,255,0.15) 1px, transparent 0); background-size: 20px 20px;"></div>
|
||||
</div>
|
||||
|
||||
<div class="relative max-w-5xl mx-auto text-center">
|
||||
<h1 class="text-5xl md:text-6xl font-bold text-white mb-6">
|
||||
<span class="bg-clip-text text-transparent bg-gradient-to-r from-blue-400 via-indigo-400 to-purple-400">
|
||||
Welcome to <em class="not-italic font-black">your</em> LocalAI
|
||||
</span>
|
||||
</h1>
|
||||
<p class="text-xl text-gray-300 mb-6">The FOSS alternative to OpenAI, Claude, and more</p>
|
||||
<p class="text-xl md:text-2xl text-gray-300 mb-8 font-light">The powerful FOSS alternative to OpenAI, Claude, and more</p>
|
||||
|
||||
<div class="flex flex-wrap justify-center gap-4">
|
||||
<a href="https://localai.io" target="_blank"
|
||||
class="group flex items-center bg-blue-600 hover:bg-blue-700 text-white py-2 px-6 rounded-lg transition duration-300 ease-in-out transform hover:scale-105 hover:shadow-lg">
|
||||
<i class="fas fa-book-reader mr-2"></i>
|
||||
class="group relative inline-flex items-center bg-gradient-to-r from-blue-600 to-blue-700 hover:from-blue-700 hover:to-blue-800 text-white py-3 px-8 rounded-xl font-semibold transition-all duration-300 ease-in-out transform hover:scale-105 hover:shadow-xl hover:shadow-blue-500/25">
|
||||
<i class="fas fa-book-reader mr-3 text-lg"></i>
|
||||
<span>Documentation</span>
|
||||
<i class="fas fa-arrow-right opacity-0 group-hover:opacity-100 group-hover:translate-x-2 ml-2 transition-all duration-300"></i>
|
||||
<i class="fas fa-external-link-alt ml-3 text-sm opacity-70 group-hover:opacity-100 transition-opacity"></i>
|
||||
<div class="absolute inset-0 rounded-xl bg-white/10 opacity-0 group-hover:opacity-100 transition-opacity"></div>
|
||||
</a>
|
||||
|
||||
<a href="browse"
|
||||
class="group flex items-center bg-indigo-600 hover:bg-indigo-700 text-white py-2 px-6 rounded-lg transition duration-300 ease-in-out transform hover:scale-105 hover:shadow-lg">
|
||||
<i class="fas fa-images mr-2"></i>
|
||||
<span>Gallery</span>
|
||||
<i class="fas fa-arrow-right opacity-0 group-hover:opacity-100 group-hover:translate-x-2 ml-2 transition-all duration-300"></i>
|
||||
class="group relative inline-flex items-center bg-gradient-to-r from-indigo-600 to-purple-600 hover:from-indigo-700 hover:to-purple-700 text-white py-3 px-8 rounded-xl font-semibold transition-all duration-300 ease-in-out transform hover:scale-105 hover:shadow-xl hover:shadow-indigo-500/25">
|
||||
<i class="fas fa-images mr-3 text-lg"></i>
|
||||
<span>Model Gallery</span>
|
||||
<i class="fas fa-arrow-right ml-3 opacity-0 group-hover:opacity-100 group-hover:translate-x-1 transition-all duration-300"></i>
|
||||
<div class="absolute inset-0 rounded-xl bg-white/10 opacity-0 group-hover:opacity-100 transition-opacity"></div>
|
||||
</a>
|
||||
|
||||
<a href="/import-model"
|
||||
class="group flex items-center bg-green-600 hover:bg-green-700 text-white py-2 px-6 rounded-lg transition duration-300 ease-in-out transform hover:scale-105 hover:shadow-lg">
|
||||
<i class="fas fa-plus mr-2"></i>
|
||||
<span>Import Custom Model</span>
|
||||
<i class="fas fa-arrow-right opacity-0 group-hover:opacity-100 group-hover:translate-x-2 ml-2 transition-all duration-300"></i>
|
||||
class="group relative inline-flex items-center bg-gradient-to-r from-green-600 to-emerald-600 hover:from-green-700 hover:to-emerald-700 text-white py-3 px-8 rounded-xl font-semibold transition-all duration-300 ease-in-out transform hover:scale-105 hover:shadow-xl hover:shadow-green-500/25">
|
||||
<i class="fas fa-plus mr-3 text-lg"></i>
|
||||
<span>Import Model</span>
|
||||
<i class="fas fa-upload ml-3 opacity-70 group-hover:opacity-100 transition-opacity"></i>
|
||||
<div class="absolute inset-0 rounded-xl bg-white/10 opacity-0 group-hover:opacity-100 transition-opacity"></div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -45,22 +57,41 @@
|
||||
{{template "views/partials/inprogress" .}}
|
||||
|
||||
{{ if eq (len .ModelsConfig) 0 }}
|
||||
<div class="bg-gray-800/50 border border-gray-700/50 rounded-xl p-8 shadow-md backdrop-blur-sm">
|
||||
<div class="text-center max-w-3xl mx-auto">
|
||||
<div class="inline-flex items-center justify-center w-16 h-16 rounded-full bg-yellow-500/20 mb-4">
|
||||
<i class="text-yellow-400 text-2xl fa-solid fa-triangle-exclamation"></i>
|
||||
<!-- No Models State -->
|
||||
<div class="relative bg-gradient-to-br from-gray-800/60 to-gray-900/60 border border-gray-700/50 rounded-2xl p-12 shadow-xl backdrop-blur-sm">
|
||||
<div class="absolute inset-0 rounded-2xl bg-gradient-to-br from-yellow-500/5 to-orange-500/5"></div>
|
||||
<div class="relative text-center max-w-4xl mx-auto">
|
||||
<div class="inline-flex items-center justify-center w-20 h-20 rounded-full bg-gradient-to-br from-yellow-500/20 to-orange-500/20 mb-6">
|
||||
<i class="text-yellow-400 text-3xl fas fa-robot"></i>
|
||||
</div>
|
||||
<h2 class="text-3xl md:text-4xl font-bold text-gray-100 mb-6">No models installed yet</h2>
|
||||
<p class="text-xl text-gray-300 mb-8 leading-relaxed">Get started by installing models from the gallery or check our documentation for guidance</p>
|
||||
|
||||
<div class="flex flex-wrap justify-center gap-4 mb-8">
|
||||
<a href="browse" class="inline-flex items-center bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-700 hover:to-indigo-700 text-white py-3 px-6 rounded-xl font-semibold transition-all duration-300 transform hover:scale-105">
|
||||
<i class="fas fa-images mr-2"></i>
|
||||
Browse Gallery
|
||||
</a>
|
||||
<a href="https://localai.io/basics/getting_started/" class="inline-flex items-center bg-gradient-to-r from-gray-700 to-gray-800 hover:from-gray-600 hover:to-gray-700 text-white py-3 px-6 rounded-xl font-semibold transition-all duration-300 transform hover:scale-105">
|
||||
<i class="fas fa-book mr-2"></i>
|
||||
Documentation
|
||||
</a>
|
||||
</div>
|
||||
<h2 class="text-2xl md:text-3xl font-semibold text-gray-100 mb-4">No models installed from the LocalAI gallery</h2>
|
||||
<p class="text-lg text-gray-300 mb-6">Install models from the <a class="text-blue-400 hover:text-blue-300 underline underline-offset-2" href="browse">🖼️ Gallery</a> or check the <a href="https://localai.io/basics/getting_started/" class="text-blue-400 hover:text-blue-300 underline underline-offset-2"> <i class="fa-solid fa-book"></i> Getting started documentation</a></p>
|
||||
|
||||
{{ if ne (len .Models) 0 }}
|
||||
<div class="mt-8 pt-8 border-t border-gray-700/50">
|
||||
<h3 class="text-xl font-semibold text-gray-100 mb-4">Models installed without a configuration file:</h3>
|
||||
<div class="mt-12 pt-8 border-t border-gray-700/50">
|
||||
<h3 class="text-2xl font-bold text-gray-100 mb-6">Detected Model Files</h3>
|
||||
<p class="text-gray-400 mb-6">These models were found but don't have configuration files yet</p>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{{ range .Models }}
|
||||
<div class="bg-gray-800/80 border border-gray-700 rounded-lg p-4 flex items-center">
|
||||
<i class="fas fa-brain text-lg text-gray-400 mr-3"></i>
|
||||
<p class="font-medium text-gray-200">{{if .Name}}{{.Name}}{{else}}{{.}}{{end}}</p>
|
||||
<div class="bg-gradient-to-br from-gray-800/90 to-gray-900/90 border border-gray-700 rounded-xl p-4 flex items-center hover:border-gray-600 transition-colors">
|
||||
<div class="w-10 h-10 rounded-lg bg-gray-700/50 flex items-center justify-center mr-3">
|
||||
<i class="fas fa-brain text-gray-400"></i>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<p class="font-semibold text-gray-200 truncate">{{if .Name}}{{.Name}}{{else}}{{.}}{{end}}</p>
|
||||
<p class="text-xs text-gray-500">No configuration</p>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
@@ -69,140 +100,226 @@
|
||||
</div>
|
||||
</div>
|
||||
{{ else }}
|
||||
<!-- Models Grid -->
|
||||
{{ $modelsN := len .ModelsConfig}}
|
||||
{{ $modelsN = add $modelsN (len .Models)}}
|
||||
<div class="mb-6 flex flex-col md:flex-row md:items-center md:justify-between">
|
||||
<h2 class="text-2xl md:text-3xl font-bold text-white mb-4 md:mb-0">
|
||||
<span class="text-blue-400">{{$modelsN}}</span> Installed Model<span class="{{if gt $modelsN 1}}s{{end}}">
|
||||
</h2>
|
||||
<!--
|
||||
<div class="flex gap-4">
|
||||
<button class="text-sm bg-gray-800 hover:bg-gray-700 text-gray-300 py-2 px-4 rounded-lg transition flex items-center gap-2">
|
||||
<i class="fas fa-filter"></i> Filter
|
||||
</button>
|
||||
<button class="text-sm bg-gray-800 hover:bg-gray-700 text-gray-300 py-2 px-4 rounded-lg transition flex items-center gap-2">
|
||||
<i class="fas fa-sort"></i> Sort
|
||||
</button>
|
||||
<div class="mb-8 flex flex-col md:flex-row md:items-center md:justify-between">
|
||||
<div class="mb-4 md:mb-0">
|
||||
<h2 class="text-3xl md:text-4xl font-bold text-white mb-2">
|
||||
Installed Models
|
||||
</h2>
|
||||
<p class="text-gray-400">
|
||||
<span class="text-blue-400 font-semibold">{{$modelsN}}</span> model{{if gt $modelsN 1}}s{{end}} ready to use
|
||||
</p>
|
||||
</div>
|
||||
-->
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
|
||||
{{$galleryConfig:=.GalleryConfig}}
|
||||
{{ $loadedModels := .LoadedModels }}
|
||||
{{$noicon:="https://upload.wikimedia.org/wikipedia/commons/6/65/No-Image-Placeholder.svg"}}
|
||||
|
||||
{{ range .ModelsConfig }}
|
||||
{{ $backendCfg := . }}
|
||||
{{ $cfg:= index $galleryConfig .Name}}
|
||||
<div class="bg-gray-800/90 border border-gray-700/50 rounded-xl overflow-hidden transition-all duration-300 hover:shadow-lg hover:shadow-blue-900/20 hover:-translate-y-1 hover:border-blue-700/50">
|
||||
<div class="flex p-5">
|
||||
<div class="w-20 h-20 rounded-lg overflow-hidden flex-shrink-0 bg-gray-700/50 flex items-center justify-center">
|
||||
<img {{ if and $cfg $cfg.Icon }}
|
||||
src="{{$cfg.Icon}}"
|
||||
{{ else }}
|
||||
src="{{$noicon}}"
|
||||
<div class="group relative bg-gradient-to-br from-gray-800/90 to-gray-900/90 border border-gray-700/50 rounded-2xl overflow-hidden transition-all duration-500 hover:shadow-2xl hover:shadow-blue-500/10 hover:-translate-y-2 hover:border-blue-500/30">
|
||||
<!-- Card Header -->
|
||||
<div class="relative p-6 border-b border-gray-700/50">
|
||||
<div class="flex items-start space-x-4">
|
||||
<div class="relative w-16 h-16 rounded-xl overflow-hidden flex-shrink-0 bg-gradient-to-br from-gray-700/50 to-gray-800/50 flex items-center justify-center group-hover:scale-110 transition-transform duration-300">
|
||||
<img {{ if and $cfg $cfg.Icon }}
|
||||
src="{{$cfg.Icon}}"
|
||||
{{ else }}
|
||||
src="{{$noicon}}"
|
||||
{{ end }}
|
||||
class="w-full h-full object-contain"
|
||||
alt="{{.Name}} icon"
|
||||
>
|
||||
{{ if index $loadedModels .Name }}
|
||||
<div class="absolute -top-1 -right-1 w-4 h-4 bg-green-500 rounded-full border-2 border-gray-800 animate-pulse"></div>
|
||||
{{ end }}
|
||||
class="w-full h-full object-contain"
|
||||
alt="{{.Name}} icon"
|
||||
>
|
||||
</div>
|
||||
<div class="ml-4 flex-1 min-w-0">
|
||||
<div class="flex items-center">
|
||||
<h3 class="font-bold text-lg text-white truncate">{{.Name}}</h3>
|
||||
<a href="browse?term={{.Name}}" class="ml-2 text-gray-400 hover:text-blue-400 transition" title="Search for similar models">
|
||||
<i class="fas fa-search text-xs"></i>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="mt-2 flex flex-wrap gap-2">
|
||||
{{ if .Backend }}
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-md text-xs font-medium bg-blue-900/50 text-blue-300 border border-blue-700/50">
|
||||
{{.Backend}}
|
||||
</span>
|
||||
{{ else }}
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-md text-xs font-medium bg-yellow-900/50 text-yellow-300 border border-yellow-700/50">
|
||||
auto
|
||||
</span>
|
||||
{{ end }}
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="font-bold text-xl text-white truncate group-hover:text-blue-300 transition-colors">{{.Name}}</h3>
|
||||
<a href="browse?term={{.Name}}" class="text-gray-400 hover:text-blue-400 transition-colors p-1 rounded-lg hover:bg-blue-500/10" title="Search for similar models">
|
||||
<i class="fas fa-search text-sm"></i>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="mt-2 flex flex-wrap gap-2">
|
||||
{{ if .Backend }}
|
||||
<span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold bg-blue-500/20 text-blue-300 border border-blue-500/30">
|
||||
<i class="fas fa-cog mr-1"></i>{{.Backend}}
|
||||
</span>
|
||||
{{ else }}
|
||||
<span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold bg-yellow-500/20 text-yellow-300 border border-yellow-500/30">
|
||||
<i class="fas fa-magic mr-1"></i>Auto
|
||||
</span>
|
||||
{{ end }}
|
||||
|
||||
{{ if index $loadedModels .Name }}
|
||||
<span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold bg-green-500/20 text-green-300 border border-green-500/30">
|
||||
<i class="fas fa-play mr-1"></i>Running
|
||||
</span>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="px-5 pb-5 pt-2">
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<!-- Usage Buttons -->
|
||||
<div class="p-6">
|
||||
<div class="flex flex-wrap gap-2 mb-4">
|
||||
{{ range .KnownUsecaseStrings }}
|
||||
{{ if eq . "FLAG_CHAT" }}
|
||||
<a href="chat/{{$backendCfg.Name}}" class="inline-flex items-center rounded-full px-4 py-2 text-sm font-medium bg-blue-900/60 text-blue-200 border border-blue-700/50 hover:bg-blue-800 transition duration-200 ease-in-out">
|
||||
<i class="fas fa-comment-alt text-xs mr-1.5"></i>Chat
|
||||
<a href="chat/{{$backendCfg.Name}}" class="flex-1 min-w-0 group/chat inline-flex items-center justify-center rounded-xl px-4 py-3 text-sm font-semibold bg-gradient-to-r from-blue-600 to-blue-700 hover:from-blue-700 hover:to-blue-800 text-white transition-all duration-300 transform hover:scale-105 hover:shadow-lg hover:shadow-blue-500/25">
|
||||
<i class="fas fa-comment-alt mr-2 group-hover/chat:animate-bounce"></i>
|
||||
Chat
|
||||
</a>
|
||||
{{ end }}
|
||||
{{ if eq . "FLAG_IMAGE" }}
|
||||
<a href="text2image/{{$backendCfg.Name}}" class="inline-flex items-center text-sm bg-green-600/80 hover:bg-green-700 text-white py-1.5 px-3 rounded-lg shadow transition duration-300 ease-in-out">
|
||||
<i class="fas fa-image text-xs mr-1.5"></i>Image
|
||||
<a href="text2image/{{$backendCfg.Name}}" class="flex-1 min-w-0 group/image inline-flex items-center justify-center rounded-xl px-4 py-3 text-sm font-semibold bg-gradient-to-r from-green-600 to-emerald-600 hover:from-green-700 hover:to-emerald-700 text-white transition-all duration-300 transform hover:scale-105 hover:shadow-lg hover:shadow-green-500/25">
|
||||
<i class="fas fa-image mr-2 group-hover/image:animate-pulse"></i>
|
||||
Image
|
||||
</a>
|
||||
{{ end }}
|
||||
{{ if eq . "FLAG_TTS" }}
|
||||
<a href="tts/{{$backendCfg.Name}}" class="inline-flex items-center text-sm bg-purple-600/80 hover:bg-purple-700 text-white py-1.5 px-3 rounded-lg shadow transition duration-300 ease-in-out">
|
||||
<i class="fas fa-microphone text-xs mr-1.5"></i>TTS
|
||||
<a href="tts/{{$backendCfg.Name}}" class="flex-1 min-w-0 group/tts inline-flex items-center justify-center rounded-xl px-4 py-3 text-sm font-semibold bg-gradient-to-r from-purple-600 to-indigo-600 hover:from-purple-700 hover:to-indigo-700 text-white transition-all duration-300 transform hover:scale-105 hover:shadow-lg hover:shadow-purple-500/25">
|
||||
<i class="fas fa-microphone mr-2 group-hover/tts:animate-pulse"></i>
|
||||
TTS
|
||||
</a>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex justify-end gap-2">
|
||||
<a href="/models/edit/{{.Name}}"
|
||||
class="inline-flex items-center text-xs font-medium text-indigo-400 hover:text-indigo-300 hover:bg-indigo-900/20 rounded-md px-2 py-1 transition-colors duration-200">
|
||||
<i class="fas fa-edit mr-1.5"></i>Edit
|
||||
</a>
|
||||
<button
|
||||
class="inline-flex items-center text-xs font-medium text-red-400 hover:text-red-300 hover:bg-red-900/20 rounded-md px-2 py-1 transition-colors duration-200"
|
||||
data-twe-ripple-init=""
|
||||
hx-confirm="Are you sure you wish to delete this model?"
|
||||
hx-post="browse/delete/model/{{.Name}}"
|
||||
hx-swap="outerHTML">
|
||||
<i class="fas fa-trash-alt mr-1.5"></i>Delete
|
||||
</button>
|
||||
<!-- Action Buttons -->
|
||||
<div class="flex justify-between items-center pt-4 border-t border-gray-700/30">
|
||||
<div class="flex gap-2">
|
||||
{{ if index $loadedModels .Name }}
|
||||
<button class="group/stop inline-flex items-center text-sm font-semibold text-red-400 hover:text-red-300 hover:bg-red-500/10 rounded-lg px-3 py-2 transition-all duration-200"
|
||||
data-twe-ripple-init=""
|
||||
hx-confirm="Are you sure you wish to stop this model?"
|
||||
hx-post="/backend/shutdown"
|
||||
hx-vals='{"model": "{{.Name}}"}'
|
||||
hx-target="this"
|
||||
hx-swap="none"
|
||||
hx-on::after-request="handleShutdownResponse(event, '{{.Name}}')">
|
||||
<i class="fas fa-stop mr-2 group-hover/stop:animate-pulse"></i>Stop
|
||||
</button>
|
||||
{{ end }}
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<a href="/models/edit/{{.Name}}"
|
||||
class="group/edit inline-flex items-center text-sm font-semibold text-indigo-400 hover:text-indigo-300 hover:bg-indigo-500/10 rounded-lg px-3 py-2 transition-all duration-200">
|
||||
<i class="fas fa-edit mr-2 group-hover/edit:animate-pulse"></i>Edit
|
||||
</a>
|
||||
<button
|
||||
class="group/delete inline-flex items-center text-sm font-semibold text-red-400 hover:text-red-300 hover:bg-red-500/10 rounded-lg px-3 py-2 transition-all duration-200"
|
||||
data-twe-ripple-init=""
|
||||
hx-confirm="Are you sure you wish to delete this model?"
|
||||
hx-post="browse/delete/model/{{.Name}}"
|
||||
hx-swap="outerHTML">
|
||||
<i class="fas fa-trash-alt mr-2 group-hover/delete:animate-bounce"></i>Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
<!-- Models without config -->
|
||||
{{ range .Models }}
|
||||
<div class="bg-gray-800/90 border border-gray-700/50 rounded-xl overflow-hidden transition-all duration-300 hover:shadow-lg hover:shadow-blue-900/20 hover:-translate-y-1 hover:border-blue-700/50">
|
||||
<div class="flex p-5">
|
||||
<div class="w-20 h-20 rounded-lg overflow-hidden flex-shrink-0 bg-gray-700/50 flex items-center justify-center">
|
||||
<img src="{{$noicon}}" class="w-full h-full object-contain" alt="Model icon">
|
||||
</div>
|
||||
<div class="ml-4 flex-1 min-w-0">
|
||||
<div class="flex items-center">
|
||||
<h3 class="font-bold text-lg text-white truncate"><i class="fas fa-brain mr-2 text-gray-400"></i>{{.}}</h3>
|
||||
<div class="group relative bg-gradient-to-br from-gray-800/60 to-gray-900/60 border border-gray-700/50 rounded-2xl overflow-hidden transition-all duration-500 hover:shadow-xl hover:shadow-yellow-500/5 hover:-translate-y-1 hover:border-yellow-500/30">
|
||||
<div class="p-6">
|
||||
<div class="flex items-start space-x-4">
|
||||
<div class="w-16 h-16 rounded-xl overflow-hidden flex-shrink-0 bg-gradient-to-br from-gray-700/50 to-gray-800/50 flex items-center justify-center">
|
||||
<i class="fas fa-brain text-2xl text-gray-400"></i>
|
||||
</div>
|
||||
|
||||
<div class="mt-2 flex flex-wrap gap-2">
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-md text-xs font-medium bg-yellow-900/50 text-yellow-300 border border-yellow-700/50">
|
||||
auto
|
||||
</span>
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-md text-xs font-medium bg-red-900/50 text-red-300 border border-red-700/50">
|
||||
No Configuration
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex justify-end">
|
||||
<span class="inline-flex items-center text-xs font-medium text-gray-400 px-2 py-1">
|
||||
<i class="fas fa-info-circle mr-1.5"></i>Cannot edit (no config)
|
||||
</span>
|
||||
<div class="flex-1 min-w-0">
|
||||
<h3 class="font-bold text-xl text-white truncate mb-2">{{.}}</h3>
|
||||
|
||||
<div class="flex flex-wrap gap-2 mb-4">
|
||||
<span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold bg-yellow-500/20 text-yellow-300 border border-yellow-500/30">
|
||||
<i class="fas fa-magic mr-1"></i>Auto Backend
|
||||
</span>
|
||||
<span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold bg-orange-500/20 text-orange-300 border border-orange-500/30">
|
||||
<i class="fas fa-exclamation-triangle mr-1"></i>No Config
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-center pt-4">
|
||||
<span class="inline-flex items-center text-sm font-medium text-gray-400 px-4 py-2 bg-gray-700/30 rounded-lg">
|
||||
<i class="fas fa-info-circle mr-2"></i>
|
||||
Configuration required for full functionality
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
|
||||
<div class="mt-8 flex flex-col md:flex-row md:items-center md:justify-between">
|
||||
<div class="mb-4 md:mb-0">
|
||||
<h2 class="text-3xl md:text-4xl font-bold text-white mb-2">
|
||||
Installed Backends
|
||||
</h2>
|
||||
<p class="text-gray-400">
|
||||
<span class="text-blue-400 font-semibold">{{len .InstalledBackends}}</span> backend{{if gt (len .InstalledBackends) 1}}s{{end}} ready to use
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
|
||||
|
||||
|
||||
{{ if ne (len .InstalledBackends) 0 }}
|
||||
<!-- No backends, suggest to install one -->
|
||||
{{ else }}
|
||||
<div class="relative bg-gradient-to-br from-gray-800/60 to-gray-900/60 border border-gray-700/50 rounded-2xl p-12 shadow-xl backdrop-blur-sm">
|
||||
<div class="relative text-center max-w-4xl mx-auto">
|
||||
<div class="inline-flex items-center justify-center w-20 h-20 rounded-full bg-gradient-to-br from-yellow-500/20 to-orange-500/20 mb-6">
|
||||
<i class="text-yellow-400 text-3xl fas fa-robot"></i>
|
||||
</div>
|
||||
<h2 class="text-3xl md:text-4xl font-bold text-gray-100 mb-6">No backends installed yet</h2>
|
||||
<p class="text-xl text-gray-300 mb-8 leading-relaxed">Get started by installing backends from the gallery or check our documentation for guidance</p>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
<!-- Backends -->
|
||||
{{ range .InstalledBackends }}
|
||||
<div class="group relative bg-gradient-to-br from-gray-800/60 to-gray-900/60 border border-gray-700/50 rounded-2xl overflow-hidden transition-all duration-500 hover:shadow-xl hover:shadow-yellow-500/5 hover:-translate-y-1 hover:border-yellow-500/30">
|
||||
<div class="p-6">
|
||||
<div class="flex items-start space-x-4">
|
||||
<div class="w-16 h-16 rounded-xl overflow-hidden flex-shrink-0 bg-gradient-to-br from-gray-700/50 to-gray-800/50 flex items-center justify-center">
|
||||
<i class="fas fa-cog text-2xl text-gray-400"></i>
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<h3 class="font-bold text-xl text-white truncate mb-2">{{.Name}}</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{template "views/partials/footer" .}}
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function handleShutdownResponse(event, modelName) {
|
||||
const button = event.target;
|
||||
const response = event.detail.xhr;
|
||||
window.location.reload();
|
||||
}
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -2,30 +2,42 @@
|
||||
<html lang="en">
|
||||
{{template "views/partials/head" .}}
|
||||
|
||||
<body class="bg-gradient-to-br from-gray-900 to-gray-950 text-gray-200">
|
||||
<body class="bg-gradient-to-br from-gray-900 via-gray-950 to-black text-gray-200">
|
||||
<div class="flex flex-col min-h-screen">
|
||||
|
||||
{{template "views/partials/navbar" .}}
|
||||
|
||||
<div class="container mx-auto px-4 py-8 flex-grow">
|
||||
<!-- Header -->
|
||||
<div class="mb-8">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-white">
|
||||
{{if .ModelName}}Edit Model: {{.ModelName}}{{else}}Import New Model{{end}}
|
||||
</h1>
|
||||
<p class="text-gray-400 mt-2">Configure your model settings using the form or YAML editor</p>
|
||||
</div>
|
||||
<div class="flex gap-3">
|
||||
<button id="validateBtn" class="bg-blue-600 hover:bg-blue-700 text-white py-2 px-4 rounded-lg transition flex items-center gap-2">
|
||||
<i class="fas fa-check"></i>
|
||||
Validate
|
||||
</button>
|
||||
<button id="saveBtn" class="bg-green-600 hover:bg-green-700 text-white py-2 px-4 rounded-lg transition flex items-center gap-2">
|
||||
<i class="fas fa-save"></i>
|
||||
{{if .ModelName}}Update{{else}}Create{{end}}
|
||||
</button>
|
||||
<!-- Hero Header -->
|
||||
<div class="relative bg-gradient-to-r from-violet-900/40 via-purple-900/30 to-fuchsia-900/40 rounded-3xl shadow-2xl p-8 mb-8 overflow-hidden">
|
||||
<!-- Background Pattern -->
|
||||
<div class="absolute inset-0 opacity-10">
|
||||
<div class="absolute inset-0 bg-gradient-to-r from-violet-500/20 to-fuchsia-500/20"></div>
|
||||
<div class="absolute top-0 left-0 w-full h-full" style="background-image: radial-gradient(circle at 1px 1px, rgba(255,255,255,0.15) 1px, transparent 0); background-size: 20px 20px;"></div>
|
||||
</div>
|
||||
|
||||
<div class="relative max-w-5xl mx-auto">
|
||||
<div class="flex flex-col md:flex-row md:items-center md:justify-between">
|
||||
<div class="mb-4 md:mb-0">
|
||||
<h1 class="text-3xl md:text-4xl font-bold text-white mb-2">
|
||||
<span class="bg-clip-text text-transparent bg-gradient-to-r from-violet-400 via-purple-400 to-fuchsia-400">
|
||||
{{if .ModelName}}Edit Model: {{.ModelName}}{{else}}Import New Model{{end}}
|
||||
</span>
|
||||
</h1>
|
||||
<p class="text-lg text-gray-300 font-light">Configure your model settings using the form or YAML editor</p>
|
||||
</div>
|
||||
<div class="flex gap-3">
|
||||
<button id="validateBtn" class="group relative inline-flex items-center bg-gradient-to-r from-blue-600 to-blue-700 hover:from-blue-700 hover:to-blue-800 text-white py-3 px-6 rounded-xl font-semibold transition-all duration-300 ease-in-out transform hover:scale-105 hover:shadow-xl hover:shadow-blue-500/25">
|
||||
<i class="fas fa-check mr-2 group-hover:animate-pulse"></i>
|
||||
<span>Validate</span>
|
||||
<div class="absolute inset-0 rounded-xl bg-white/10 opacity-0 group-hover:opacity-100 transition-opacity"></div>
|
||||
</button>
|
||||
<button id="saveBtn" class="group relative inline-flex items-center bg-gradient-to-r from-green-600 to-emerald-600 hover:from-green-700 hover:to-emerald-700 text-white py-3 px-6 rounded-xl font-semibold transition-all duration-300 ease-in-out transform hover:scale-105 hover:shadow-xl hover:shadow-green-500/25">
|
||||
<i class="fas fa-save mr-2 group-hover:animate-pulse"></i>
|
||||
<span>{{if .ModelName}}Update{{else}}Create{{end}}</span>
|
||||
<div class="absolute inset-0 rounded-xl bg-white/10 opacity-0 group-hover:opacity-100 transition-opacity"></div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -37,45 +49,53 @@
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 h-[calc(100vh-250px)]">
|
||||
|
||||
<!-- Form Panel (Left) -->
|
||||
<div class="bg-gray-800/90 border border-gray-700/50 rounded-xl overflow-hidden">
|
||||
<div class="sticky top-0 bg-gray-800 border-b border-gray-700/50 p-4 flex items-center justify-between z-10">
|
||||
<h2 class="text-xl font-semibold text-white flex items-center gap-2">
|
||||
<i class="fas fa-edit"></i>
|
||||
<div class="relative bg-gradient-to-br from-gray-800/90 to-gray-900/90 border border-gray-700/50 rounded-2xl overflow-hidden shadow-xl backdrop-blur-sm">
|
||||
<div class="absolute inset-0 rounded-2xl bg-gradient-to-br from-violet-500/5 to-purple-500/5"></div>
|
||||
|
||||
<div class="relative sticky top-0 bg-gray-800/95 border-b border-gray-700/50 p-6 flex items-center justify-between z-10 backdrop-blur-sm">
|
||||
<h2 class="text-xl font-semibold text-white flex items-center gap-3">
|
||||
<div class="w-8 h-8 rounded-lg bg-violet-500/20 flex items-center justify-center">
|
||||
<i class="fas fa-edit text-violet-400"></i>
|
||||
</div>
|
||||
Configuration Form
|
||||
</h2>
|
||||
<div class="flex items-center gap-2">
|
||||
<button id="resetFormBtn" class="text-gray-400 hover:text-gray-200 text-sm">
|
||||
<i class="fas fa-undo"></i> Reset
|
||||
<div class="flex items-center gap-3">
|
||||
<button id="resetFormBtn" class="group text-gray-400 hover:text-gray-200 text-sm px-3 py-1.5 rounded-lg hover:bg-gray-700/50 transition-all duration-200">
|
||||
<i class="fas fa-undo mr-1.5 group-hover:animate-spin"></i> Reset
|
||||
</button>
|
||||
<button id="expandAllBtn" class="text-gray-400 hover:text-gray-200 text-sm">
|
||||
<i class="fas fa-expand-alt"></i> Expand All
|
||||
<button id="expandAllBtn" class="group text-gray-400 hover:text-gray-200 text-sm px-3 py-1.5 rounded-lg hover:bg-gray-700/50 transition-all duration-200">
|
||||
<i class="fas fa-expand-alt mr-1.5 group-hover:animate-pulse"></i> Expand All
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-6 overflow-y-auto" style="height: calc(100% - 80px);">
|
||||
<div class="relative p-6 overflow-y-auto" style="height: calc(100% - 88px);">
|
||||
<form id="configForm" class="space-y-6">
|
||||
<!-- Form will be dynamically generated here -->
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- YAML Editor Panel (Right) -->
|
||||
<div class="bg-gray-800/90 border border-gray-700/50 rounded-xl overflow-hidden">
|
||||
<div class="sticky top-0 bg-gray-800 border-b border-gray-700/50 p-4 flex items-center justify-between z-10">
|
||||
<h2 class="text-xl font-semibold text-white flex items-center gap-2">
|
||||
<i class="fas fa-code"></i>
|
||||
<div class="relative bg-gradient-to-br from-gray-800/90 to-gray-900/90 border border-gray-700/50 rounded-2xl overflow-hidden shadow-xl backdrop-blur-sm">
|
||||
<div class="absolute inset-0 rounded-2xl bg-gradient-to-br from-fuchsia-500/5 to-purple-500/5"></div>
|
||||
|
||||
<div class="relative sticky top-0 bg-gray-800/95 border-b border-gray-700/50 p-6 flex items-center justify-between z-10 backdrop-blur-sm">
|
||||
<h2 class="text-xl font-semibold text-white flex items-center gap-3">
|
||||
<div class="w-8 h-8 rounded-lg bg-fuchsia-500/20 flex items-center justify-center">
|
||||
<i class="fas fa-code text-fuchsia-400"></i>
|
||||
</div>
|
||||
YAML Editor
|
||||
</h2>
|
||||
<div class="flex items-center gap-2">
|
||||
<button id="formatYamlBtn" class="text-gray-400 hover:text-gray-200 text-sm">
|
||||
<i class="fas fa-indent"></i> Format
|
||||
</button>
|
||||
<button id="copyYamlBtn" class="text-gray-400 hover:text-gray-200 text-sm">
|
||||
<i class="fas fa-copy"></i> Copy
|
||||
<div class="flex items-center gap-3">
|
||||
<button id="formatYamlBtn" class="group text-gray-400 hover:text-gray-200 text-sm px-3 py-1.5 rounded-lg hover:bg-gray-700/50 transition-all duration-200">
|
||||
<i class="fas fa-indent mr-1.5 group-hover:animate-pulse"></i> Format
|
||||
</button>
|
||||
<button id="copyYamlBtn" class="group text-gray-400 hover:text-gray-200 text-sm px-3 py-1.5 rounded-lg hover:bg-gray-700/50 transition-all duration-200">
|
||||
<i class="fas fa-copy mr-1.5 group-hover:animate-bounce"></i> Copy
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative" style="height: calc(100% - 80px);">
|
||||
<div class="relative" style="height: calc(100% - 88px);">
|
||||
<div id="yamlCodeMirror" class="h-full"></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -95,90 +115,238 @@
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/6.65.7/addon/display/autorefresh.min.js"></script>
|
||||
|
||||
<style>
|
||||
/* Enhanced CodeMirror styling */
|
||||
.CodeMirror {
|
||||
background: #111827 !important;
|
||||
background: linear-gradient(135deg, #111827 0%, #1f2937 100%) !important;
|
||||
color: #e5e7eb !important;
|
||||
border: none !important;
|
||||
height: 100% !important;
|
||||
font-family: 'JetBrains Mono', 'Fira Code', 'Monaco', 'Consolas', monospace !important;
|
||||
font-size: 14px !important;
|
||||
border-radius: 0 !important;
|
||||
line-height: 1.5 !important;
|
||||
}
|
||||
|
||||
.CodeMirror-cursor {
|
||||
border-left: 1px solid #e5e7eb !important;
|
||||
border-left: 2px solid #a78bfa !important;
|
||||
animation: blink 1s infinite;
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
0%, 50% { opacity: 1; }
|
||||
51%, 100% { opacity: 0; }
|
||||
}
|
||||
|
||||
.CodeMirror-gutters {
|
||||
background: #1f2937 !important;
|
||||
border-right: 1px solid #374151 !important;
|
||||
color: #6b7280 !important;
|
||||
background: linear-gradient(135deg, #1f2937 0%, #374151 100%) !important;
|
||||
border-right: 1px solid rgba(75, 85, 99, 0.5) !important;
|
||||
color: #9ca3af !important;
|
||||
padding-right: 8px !important;
|
||||
}
|
||||
|
||||
.CodeMirror-linenumber {
|
||||
color: #6b7280 !important;
|
||||
padding: 0 8px 0 0 !important;
|
||||
}
|
||||
.CodeMirror-activeline-background {
|
||||
background: rgba(75, 85, 99, 0.3) !important;
|
||||
}
|
||||
.CodeMirror-selected {
|
||||
background: rgba(59, 130, 246, 0.25) !important;
|
||||
}
|
||||
.CodeMirror-selectedtext {
|
||||
background: rgba(59, 130, 246, 0.25) !important;
|
||||
}
|
||||
.CodeMirror-focused .CodeMirror-selected {
|
||||
background: rgba(59, 130, 246, 0.3) !important;
|
||||
}
|
||||
.CodeMirror-line::selection, .CodeMirror-line > span::selection, .CodeMirror-line > span > span::selection {
|
||||
background: rgba(59, 130, 246, 0.3) !important;
|
||||
}
|
||||
.CodeMirror-line::-moz-selection, .CodeMirror-line > span::-moz-selection, .CodeMirror-line > span > span::-moz-selection {
|
||||
background: rgba(59, 130, 246, 0.3) !important;
|
||||
padding: 0 8px 0 4px !important;
|
||||
font-size: 12px !important;
|
||||
}
|
||||
|
||||
/* YAML Syntax Highlighting - Tailored for LocalAI's gray theme */
|
||||
.cm-keyword { color: #60a5fa !important; font-weight: 500 !important; } /* Blue for YAML keys */
|
||||
.cm-string { color: #34d399 !important; } /* Green for strings */
|
||||
.cm-number { color: #fbbf24 !important; } /* Amber for numbers */
|
||||
.cm-comment { color: #9ca3af !important; font-style: italic !important; } /* Gray for comments */
|
||||
.cm-property { color: #a78bfa !important; } /* Purple for properties */
|
||||
.cm-operator { color: #f87171 !important; } /* Red for operators */
|
||||
.cm-variable { color: #22d3ee !important; } /* Cyan for variables */
|
||||
.cm-tag { color: #60a5fa !important; font-weight: 500 !important; } /* Blue for tags */
|
||||
.cm-attribute { color: #fbbf24 !important; } /* Amber for attributes */
|
||||
.cm-def { color: #a78bfa !important; font-weight: 500 !important; } /* Purple for definitions */
|
||||
.CodeMirror-activeline-background {
|
||||
background: rgba(139, 92, 246, 0.1) !important;
|
||||
}
|
||||
|
||||
.CodeMirror-selected {
|
||||
background: rgba(139, 92, 246, 0.25) !important;
|
||||
}
|
||||
|
||||
.CodeMirror-selectedtext {
|
||||
background: rgba(139, 92, 246, 0.25) !important;
|
||||
}
|
||||
|
||||
.CodeMirror-focused .CodeMirror-selected {
|
||||
background: rgba(139, 92, 246, 0.3) !important;
|
||||
}
|
||||
|
||||
.CodeMirror-line::selection, .CodeMirror-line > span::selection, .CodeMirror-line > span > span::selection {
|
||||
background: rgba(139, 92, 246, 0.3) !important;
|
||||
}
|
||||
|
||||
.CodeMirror-line::-moz-selection, .CodeMirror-line > span::-moz-selection, .CodeMirror-line > span > span::-moz-selection {
|
||||
background: rgba(139, 92, 246, 0.3) !important;
|
||||
}
|
||||
|
||||
/* Enhanced YAML Syntax Highlighting */
|
||||
.cm-keyword { color: #8b5cf6 !important; font-weight: 600 !important; } /* Purple for YAML keys */
|
||||
.cm-string { color: #10b981 !important; } /* Emerald for strings */
|
||||
.cm-number { color: #f59e0b !important; } /* Amber for numbers */
|
||||
.cm-comment { color: #6b7280 !important; font-style: italic !important; } /* Gray for comments */
|
||||
.cm-property { color: #ec4899 !important; } /* Pink for properties */
|
||||
.cm-operator { color: #ef4444 !important; } /* Red for operators */
|
||||
.cm-variable { color: #06b6d4 !important; } /* Cyan for variables */
|
||||
.cm-tag { color: #8b5cf6 !important; font-weight: 600 !important; } /* Purple for tags */
|
||||
.cm-attribute { color: #f59e0b !important; } /* Amber for attributes */
|
||||
.cm-def { color: #ec4899 !important; font-weight: 600 !important; } /* Pink for definitions */
|
||||
.cm-bracket { color: #d1d5db !important; } /* Light gray for brackets */
|
||||
.cm-punctuation { color: #d1d5db !important; } /* Light gray for punctuation */
|
||||
.cm-quote { color: #34d399 !important; } /* Green for quotes */
|
||||
.cm-meta { color: #9ca3af !important; } /* Gray for meta */
|
||||
.cm-quote { color: #10b981 !important; } /* Emerald for quotes */
|
||||
.cm-meta { color: #6b7280 !important; } /* Gray for meta */
|
||||
.cm-builtin { color: #f472b6 !important; } /* Pink for builtins */
|
||||
.cm-atom { color: #fbbf24 !important; } /* Amber for atoms like true/false/null */
|
||||
.cm-atom { color: #f59e0b !important; } /* Amber for atoms like true/false/null */
|
||||
|
||||
/* Scrollbar styling to match theme */
|
||||
/* Enhanced scrollbar styling */
|
||||
.CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler {
|
||||
background: #1f2937 !important;
|
||||
}
|
||||
|
||||
.CodeMirror-vscrollbar, .CodeMirror-hscrollbar {
|
||||
background: #1f2937 !important;
|
||||
}
|
||||
|
||||
.CodeMirror-vscrollbar::-webkit-scrollbar, .CodeMirror-hscrollbar::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
.CodeMirror-vscrollbar::-webkit-scrollbar-track, .CodeMirror-hscrollbar::-webkit-scrollbar-track {
|
||||
background: #1f2937;
|
||||
}
|
||||
.CodeMirror-vscrollbar::-webkit-scrollbar-thumb, .CodeMirror-hscrollbar::-webkit-scrollbar-thumb {
|
||||
background: #4b5563;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.CodeMirror-vscrollbar::-webkit-scrollbar-thumb, .CodeMirror-hscrollbar::-webkit-scrollbar-thumb {
|
||||
background: linear-gradient(135deg, #6b7280 0%, #9ca3af 100%);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.CodeMirror-vscrollbar::-webkit-scrollbar-thumb:hover, .CodeMirror-hscrollbar::-webkit-scrollbar-thumb:hover {
|
||||
background: #6b7280;
|
||||
background: linear-gradient(135deg, #9ca3af 0%, #d1d5db 100%);
|
||||
}
|
||||
|
||||
/* Focus ring styling */
|
||||
.CodeMirror-focused {
|
||||
outline: 2px solid rgba(59, 130, 246, 0.5) !important;
|
||||
outline: 2px solid rgba(139, 92, 246, 0.5) !important;
|
||||
outline-offset: -2px !important;
|
||||
border-radius: 0.5rem !important;
|
||||
}
|
||||
|
||||
/* Enhanced form styling */
|
||||
.form-section {
|
||||
background: linear-gradient(135deg, rgba(55, 65, 81, 0.3) 0%, rgba(75, 85, 99, 0.3) 100%);
|
||||
border: 1px solid rgba(107, 114, 128, 0.3);
|
||||
border-radius: 1rem;
|
||||
backdrop-filter: blur(8px);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.form-section:hover {
|
||||
border-color: rgba(139, 92, 246, 0.3);
|
||||
box-shadow: 0 4px 15px rgba(139, 92, 246, 0.1);
|
||||
}
|
||||
|
||||
.form-section.expanded {
|
||||
border-color: rgba(139, 92, 246, 0.4);
|
||||
box-shadow: 0 8px 25px rgba(139, 92, 246, 0.15);
|
||||
}
|
||||
|
||||
.section-header {
|
||||
background: linear-gradient(135deg, rgba(75, 85, 99, 0.4) 0%, rgba(107, 114, 128, 0.4) 100%);
|
||||
border-bottom: 1px solid rgba(107, 114, 128, 0.3);
|
||||
padding: 1rem 1.5rem;
|
||||
border-radius: 1rem 1rem 0 0;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.section-header:hover {
|
||||
background: linear-gradient(135deg, rgba(107, 114, 128, 0.4) 0%, rgba(139, 92, 246, 0.2) 100%);
|
||||
}
|
||||
|
||||
.form-input {
|
||||
background: linear-gradient(135deg, rgba(17, 24, 39, 0.8) 0%, rgba(31, 41, 55, 0.8) 100%) !important;
|
||||
border: 1px solid rgba(107, 114, 128, 0.4) !important;
|
||||
border-radius: 0.75rem !important;
|
||||
padding: 0.75rem 1rem !important;
|
||||
color: #e5e7eb !important;
|
||||
transition: all 0.3s ease !important;
|
||||
backdrop-filter: blur(4px) !important;
|
||||
}
|
||||
|
||||
.form-input:focus {
|
||||
border-color: rgba(139, 92, 246, 0.6) !important;
|
||||
box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.1) !important;
|
||||
background: linear-gradient(135deg, rgba(31, 41, 55, 0.9) 0%, rgba(55, 65, 81, 0.9) 100%) !important;
|
||||
}
|
||||
|
||||
.form-input:hover {
|
||||
border-color: rgba(139, 92, 246, 0.4) !important;
|
||||
}
|
||||
|
||||
/* Enhanced button styling */
|
||||
.action-button {
|
||||
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
|
||||
border-radius: 0.75rem;
|
||||
padding: 0.5rem 1rem;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s ease;
|
||||
border: 1px solid rgba(139, 92, 246, 0.3);
|
||||
}
|
||||
|
||||
.action-button:hover {
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 8px 25px rgba(139, 92, 246, 0.3);
|
||||
background: linear-gradient(135deg, #8b5cf6 0%, #a855f7 100%);
|
||||
}
|
||||
|
||||
.danger-button {
|
||||
background: linear-gradient(135deg, #dc2626 0%, #ef4444 100%);
|
||||
border: 1px solid rgba(239, 68, 68, 0.3);
|
||||
}
|
||||
|
||||
.danger-button:hover {
|
||||
background: linear-gradient(135deg, #ef4444 0%, #f87171 100%);
|
||||
box-shadow: 0 8px 25px rgba(239, 68, 68, 0.3);
|
||||
}
|
||||
|
||||
/* Alert styling */
|
||||
.alert {
|
||||
border-radius: 1rem;
|
||||
padding: 1rem 1.5rem;
|
||||
backdrop-filter: blur(8px);
|
||||
border: 1px solid;
|
||||
animation: slideInFromTop 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes slideInFromTop {
|
||||
from {
|
||||
transform: translateY(-20px);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.alert-success {
|
||||
background: linear-gradient(135deg, rgba(16, 185, 129, 0.1) 0%, rgba(5, 150, 105, 0.1) 100%);
|
||||
border-color: rgba(16, 185, 129, 0.3);
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
.alert-error {
|
||||
background: linear-gradient(135deg, rgba(239, 68, 68, 0.1) 0%, rgba(220, 38, 38, 0.1) 100%);
|
||||
border-color: rgba(239, 68, 68, 0.3);
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.alert-warning {
|
||||
background: linear-gradient(135deg, rgba(245, 158, 11, 0.1) 0%, rgba(217, 119, 6, 0.1) 100%);
|
||||
border-color: rgba(245, 158, 11, 0.3);
|
||||
color: #f59e0b;
|
||||
}
|
||||
|
||||
.alert-info {
|
||||
background: linear-gradient(135deg, rgba(59, 130, 246, 0.1) 0%, rgba(37, 99, 235, 0.1) 100%);
|
||||
border-color: rgba(59, 130, 246, 0.3);
|
||||
color: #3b82f6;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -398,20 +566,22 @@ class ModelEditor {
|
||||
|
||||
createFormSection(section) {
|
||||
const sectionDiv = document.createElement('div');
|
||||
sectionDiv.className = 'bg-gray-700/30 rounded-lg border border-gray-600/50';
|
||||
sectionDiv.className = 'form-section';
|
||||
|
||||
const isCollapsible = section.collapsible;
|
||||
const sectionId = section.title.toLowerCase().replace(/\s+/g, '-');
|
||||
|
||||
sectionDiv.innerHTML = `
|
||||
<div class="p-4 border-b border-gray-600/50 ${isCollapsible ? 'cursor-pointer' : ''}" ${isCollapsible ? `onclick="this.nextElementSibling.classList.toggle('hidden'); this.querySelector('.collapse-icon').classList.toggle('rotate-180')"` : ''}>
|
||||
<h3 class="text-lg font-semibold text-white flex items-center gap-2">
|
||||
<i class="${section.icon}"></i>
|
||||
<div class="section-header ${isCollapsible ? 'cursor-pointer' : ''}" ${isCollapsible ? `onclick="this.nextElementSibling.classList.toggle('hidden'); this.querySelector('.collapse-icon').classList.toggle('rotate-180'); this.closest('.form-section').classList.toggle('expanded')"` : ''}>
|
||||
<h3 class="text-lg font-semibold text-white flex items-center gap-3">
|
||||
<div class="w-8 h-8 rounded-lg bg-violet-500/20 flex items-center justify-center">
|
||||
<i class="${section.icon} text-violet-400"></i>
|
||||
</div>
|
||||
${section.title}
|
||||
${isCollapsible ? '<i class="fas fa-chevron-down text-sm ml-auto collapse-icon transition-transform"></i>' : ''}
|
||||
${isCollapsible ? '<i class="fas fa-chevron-down text-sm ml-auto collapse-icon transition-transform duration-300"></i>' : ''}
|
||||
</h3>
|
||||
</div>
|
||||
<div class="p-4 space-y-4 ${isCollapsible ? 'hidden' : ''}" id="${sectionId}-content">
|
||||
<div class="p-6 space-y-6 ${isCollapsible ? 'hidden' : ''}" id="${sectionId}-content">
|
||||
${section.fields.map(field => this.createFormField(field)).join('')}
|
||||
</div>
|
||||
`;
|
||||
@@ -428,33 +598,33 @@ class ModelEditor {
|
||||
switch (field.type) {
|
||||
case 'text':
|
||||
const readonlyAttr = field.readonly ? 'readonly' : '';
|
||||
const readonlyClass = field.readonly ? 'bg-gray-700 cursor-not-allowed' : 'bg-gray-800';
|
||||
inputHtml = `<input type="text" id="${fieldId}" name="${field.key}" value="${value}" class="w-full p-3 ${readonlyClass} border border-gray-600 rounded-lg text-gray-200 focus:ring-2 focus:ring-blue-500 focus:border-transparent" ${field.required ? 'required' : ''} ${readonlyAttr}>`;
|
||||
const readonlyClass = field.readonly ? 'bg-gray-700 cursor-not-allowed opacity-60' : '';
|
||||
inputHtml = `<input type="text" id="${fieldId}" name="${field.key}" value="${value}" class="form-input w-full ${readonlyClass}" ${field.required ? 'required' : ''} ${readonlyAttr}>`;
|
||||
break;
|
||||
|
||||
case 'textarea':
|
||||
inputHtml = `<textarea id="${fieldId}" name="${field.key}" rows="3" class="w-full p-3 bg-gray-800 border border-gray-600 rounded-lg text-gray-200 focus:ring-2 focus:ring-blue-500 focus:border-transparent resize-vertical">${value}</textarea>`;
|
||||
inputHtml = `<textarea id="${fieldId}" name="${field.key}" rows="3" class="form-input w-full resize-vertical">${value}</textarea>`;
|
||||
break;
|
||||
|
||||
case 'number':
|
||||
const step = field.step || '1';
|
||||
const min = field.min !== undefined ? `min="${field.min}"` : '';
|
||||
const max = field.max !== undefined ? `max="${field.max}"` : '';
|
||||
inputHtml = `<input type="number" id="${fieldId}" name="${field.key}" value="${value}" step="${step}" ${min} ${max} class="w-full p-3 bg-gray-800 border border-gray-600 rounded-lg text-gray-200 focus:ring-2 focus:ring-blue-500 focus:border-transparent">`;
|
||||
inputHtml = `<input type="number" id="${fieldId}" name="${field.key}" value="${value}" step="${step}" ${min} ${max} class="form-input w-full">`;
|
||||
break;
|
||||
|
||||
case 'checkbox':
|
||||
const checked = value === true || value === 'true' ? 'checked' : '';
|
||||
inputHtml = `
|
||||
<div class="flex items-center">
|
||||
<input type="checkbox" id="${fieldId}" name="${field.key}" ${checked} class="w-4 h-4 text-blue-600 bg-gray-800 border-gray-600 rounded focus:ring-blue-500 focus:ring-2">
|
||||
<label for="${fieldId}" class="ml-2 text-sm text-gray-300">Enable</label>
|
||||
<input type="checkbox" id="${fieldId}" name="${field.key}" ${checked} class="w-5 h-5 text-violet-600 bg-gray-800 border-gray-600 rounded focus:ring-violet-500 focus:ring-2">
|
||||
<label for="${fieldId}" class="ml-3 text-sm text-gray-300 font-medium">Enable</label>
|
||||
</div>`;
|
||||
break;
|
||||
|
||||
case 'select':
|
||||
const options = field.options.map(opt => `<option value="${opt}" ${value === opt ? 'selected' : ''}>${opt || '(Select backend)'}</option>`).join('');
|
||||
inputHtml = `<select id="${fieldId}" name="${field.key}" class="w-full p-3 bg-gray-800 border border-gray-600 rounded-lg text-gray-200 focus:ring-2 focus:ring-blue-500 focus:border-transparent" ${field.required ? 'required' : ''}>${options}</select>`;
|
||||
inputHtml = `<select id="${fieldId}" name="${field.key}" class="form-input w-full" ${field.required ? 'required' : ''}>${options}</select>`;
|
||||
break;
|
||||
|
||||
case 'multiselect':
|
||||
@@ -462,30 +632,30 @@ class ModelEditor {
|
||||
const checkboxes = field.options.map(opt => {
|
||||
const isChecked = currentValues.includes(opt);
|
||||
return `
|
||||
<div class="flex items-center">
|
||||
<input type="checkbox" id="${fieldId}_${opt}" name="${field.key}" value="${opt}" ${isChecked ? 'checked' : ''} class="w-4 h-4 text-blue-600 bg-gray-800 border-gray-600 rounded focus:ring-blue-500 focus:ring-2">
|
||||
<label for="${fieldId}_${opt}" class="ml-2 text-sm text-gray-300">${opt}</label>
|
||||
<div class="flex items-center p-2 rounded-lg hover:bg-violet-500/10 transition-colors">
|
||||
<input type="checkbox" id="${fieldId}_${opt}" name="${field.key}" value="${opt}" ${isChecked ? 'checked' : ''} class="w-4 h-4 text-violet-600 bg-gray-800 border-gray-600 rounded focus:ring-violet-500 focus:ring-2">
|
||||
<label for="${fieldId}_${opt}" class="ml-3 text-sm text-gray-300 font-medium">${opt}</label>
|
||||
</div>`;
|
||||
}).join('');
|
||||
inputHtml = `<div class="space-y-2 max-h-32 overflow-y-auto">${checkboxes}</div>`;
|
||||
inputHtml = `<div class="space-y-2 max-h-40 overflow-y-auto scrollbar-thin scrollbar-thumb-gray-600 scrollbar-track-gray-800 p-2 border border-gray-600/50 rounded-lg bg-gray-800/30">${checkboxes}</div>`;
|
||||
break;
|
||||
|
||||
case 'array':
|
||||
const arrayValues = Array.isArray(value) ? value : [];
|
||||
inputHtml = `
|
||||
<div class="space-y-2">
|
||||
<div class="space-y-3">
|
||||
<div id="${fieldId}_container" class="space-y-2">
|
||||
${arrayValues.map((item, index) => `
|
||||
<div class="flex gap-2">
|
||||
<input type="text" value="${item}" class="flex-1 p-2 bg-gray-800 border border-gray-600 rounded text-gray-200" onchange="modelEditor.updateArrayField('${field.key}', ${index}, this.value)">
|
||||
<button type="button" onclick="modelEditor.removeArrayItem('${field.key}', ${index})" class="px-3 py-2 bg-red-600 hover:bg-red-700 text-white rounded">
|
||||
<input type="text" value="${item}" class="form-input flex-1" onchange="modelEditor.updateArrayField('${field.key}', ${index}, this.value)">
|
||||
<button type="button" onclick="modelEditor.removeArrayItem('${field.key}', ${index})" class="danger-button px-3 py-2 text-white">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
<button type="button" onclick="modelEditor.addArrayItem('${field.key}')" class="px-3 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded text-sm">
|
||||
<i class="fas fa-plus"></i> Add Item
|
||||
<button type="button" onclick="modelEditor.addArrayItem('${field.key}')" class="action-button px-4 py-2 text-white text-sm">
|
||||
<i class="fas fa-plus mr-2"></i> Add Item
|
||||
</button>
|
||||
</div>`;
|
||||
break;
|
||||
@@ -494,33 +664,33 @@ class ModelEditor {
|
||||
const kvPairs = typeof value === 'object' && value !== null ? value : {};
|
||||
const kvEntries = Object.entries(kvPairs);
|
||||
inputHtml = `
|
||||
<div class="space-y-2">
|
||||
<div class="space-y-3">
|
||||
<div id="${fieldId}_container" class="space-y-2">
|
||||
${kvEntries.map(([key, val], index) => `
|
||||
<div class="flex gap-2">
|
||||
<input type="text" value="${key}" placeholder="Key" class="flex-1 p-2 bg-gray-800 border border-gray-600 rounded text-gray-200" onchange="modelEditor.updateKeyValueField('${field.key}', ${index}, this.value, this.nextElementSibling.value, 'key')">
|
||||
<input type="text" value="${val}" placeholder="Value" class="flex-1 p-2 bg-gray-800 border border-gray-600 rounded text-gray-200" onchange="modelEditor.updateKeyValueField('${field.key}', ${index}, this.previousElementSibling.value, this.value, 'value')">
|
||||
<button type="button" onclick="modelEditor.removeKeyValueItem('${field.key}', ${index})" class="px-3 py-2 bg-red-600 hover:bg-red-700 text-white rounded">
|
||||
<input type="text" value="${key}" placeholder="Key" class="form-input flex-1" onchange="modelEditor.updateKeyValueField('${field.key}', ${index}, this.value, this.nextElementSibling.value, 'key')">
|
||||
<input type="text" value="${val}" placeholder="Value" class="form-input flex-1" onchange="modelEditor.updateKeyValueField('${field.key}', ${index}, this.previousElementSibling.value, this.value, 'value')">
|
||||
<button type="button" onclick="modelEditor.removeKeyValueItem('${field.key}', ${index})" class="danger-button px-3 py-2 text-white">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
<button type="button" onclick="modelEditor.addKeyValueItem('${field.key}')" class="px-3 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded text-sm">
|
||||
<i class="fas fa-plus"></i> Add Pair
|
||||
<button type="button" onclick="modelEditor.addKeyValueItem('${field.key}')" class="action-button px-4 py-2 text-white text-sm">
|
||||
<i class="fas fa-plus mr-2"></i> Add Pair
|
||||
</button>
|
||||
</div>`;
|
||||
break;
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="space-y-2">
|
||||
<label for="${fieldId}" class="block text-sm font-medium text-gray-300">
|
||||
<div class="space-y-3">
|
||||
<label for="${fieldId}" class="block text-sm font-semibold text-gray-300">
|
||||
${field.label}
|
||||
${field.required ? '<span class="text-red-400">*</span>' : ''}
|
||||
${field.required ? '<span class="text-red-400 ml-1">*</span>' : ''}
|
||||
</label>
|
||||
${inputHtml}
|
||||
${field.description ? `<p class="text-xs text-gray-500">${field.description}</p>` : ''}
|
||||
${field.description ? `<p class="text-xs text-gray-500 leading-relaxed">${field.description}</p>` : ''}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -536,6 +706,8 @@ class ModelEditor {
|
||||
indentWithTabs: false,
|
||||
lineWrapping: false,
|
||||
styleActiveLine: true,
|
||||
matchBrackets: true,
|
||||
autoCloseBrackets: true,
|
||||
value: {{if .ConfigYAML}}`{{.ConfigYAML}}`{{else}}'# Configuration will appear here...'{{end}}
|
||||
});
|
||||
|
||||
@@ -744,8 +916,8 @@ class ModelEditor {
|
||||
|
||||
container.innerHTML = values.map((item, index) => `
|
||||
<div class="flex gap-2">
|
||||
<input type="text" value="${item}" class="flex-1 p-2 bg-gray-800 border border-gray-600 rounded text-gray-200" onchange="modelEditor.updateArrayField('${fieldKey}', ${index}, this.value)">
|
||||
<button type="button" onclick="modelEditor.removeArrayItem('${fieldKey}', ${index})" class="px-3 py-2 bg-red-600 hover:bg-red-700 text-white rounded">
|
||||
<input type="text" value="${item}" class="form-input flex-1" onchange="modelEditor.updateArrayField('${fieldKey}', ${index}, this.value)">
|
||||
<button type="button" onclick="modelEditor.removeArrayItem('${fieldKey}', ${index})" class="danger-button px-3 py-2 text-white">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
@@ -795,9 +967,9 @@ class ModelEditor {
|
||||
const entries = Object.entries(values);
|
||||
container.innerHTML = entries.map(([key, val], index) => `
|
||||
<div class="flex gap-2">
|
||||
<input type="text" value="${key}" placeholder="Key" class="flex-1 p-2 bg-gray-800 border border-gray-600 rounded text-gray-200" onchange="modelEditor.updateKeyValueField('${fieldKey}', ${index}, this.value, this.nextElementSibling.value, 'key')">
|
||||
<input type="text" value="${val}" placeholder="Value" class="flex-1 p-2 bg-gray-800 border border-gray-600 rounded text-gray-200" onchange="modelEditor.updateKeyValueField('${fieldKey}', ${index}, this.previousElementSibling.value, this.value, 'value')">
|
||||
<button type="button" onclick="modelEditor.removeKeyValueItem('${fieldKey}', ${index})" class="px-3 py-2 bg-red-600 hover:bg-red-700 text-white rounded">
|
||||
<input type="text" value="${key}" placeholder="Key" class="form-input flex-1" onchange="modelEditor.updateKeyValueField('${fieldKey}', ${index}, this.value, this.nextElementSibling.value, 'key')">
|
||||
<input type="text" value="${val}" placeholder="Value" class="form-input flex-1" onchange="modelEditor.updateKeyValueField('${fieldKey}', ${index}, this.previousElementSibling.value, this.value, 'value')">
|
||||
<button type="button" onclick="modelEditor.removeKeyValueItem('${fieldKey}', ${index})" class="danger-button px-3 py-2 text-white">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
@@ -933,6 +1105,7 @@ class ModelEditor {
|
||||
if (content.classList.contains('hidden')) {
|
||||
content.classList.remove('hidden');
|
||||
icon.classList.add('rotate-180');
|
||||
icon.closest('.form-section').classList.add('expanded');
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -973,10 +1146,10 @@ class ModelEditor {
|
||||
showAlert(type, message) {
|
||||
const container = document.getElementById('alertContainer');
|
||||
const alertClasses = {
|
||||
success: 'bg-green-600/20 border-green-600/50 text-green-200',
|
||||
error: 'bg-red-600/20 border-red-600/50 text-red-200',
|
||||
warning: 'bg-yellow-600/20 border-yellow-600/50 text-yellow-200',
|
||||
info: 'bg-blue-600/20 border-blue-600/50 text-blue-200'
|
||||
success: 'alert alert-success',
|
||||
error: 'alert alert-error',
|
||||
warning: 'alert alert-warning',
|
||||
info: 'alert alert-info'
|
||||
};
|
||||
|
||||
const alertIcons = {
|
||||
@@ -987,11 +1160,11 @@ class ModelEditor {
|
||||
};
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="p-4 rounded-lg border ${alertClasses[type]}">
|
||||
<div class="${alertClasses[type]}">
|
||||
<div class="flex items-center">
|
||||
<i class="${alertIcons[type]} mr-2"></i>
|
||||
<span>${message}</span>
|
||||
<button onclick="this.parentElement.parentElement.remove()" class="ml-auto text-gray-400 hover:text-gray-200">
|
||||
<i class="${alertIcons[type]} mr-3 text-lg"></i>
|
||||
<span class="flex-1">${message}</span>
|
||||
<button onclick="this.parentElement.parentElement.remove()" class="ml-4 text-current hover:opacity-70 transition-opacity">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<html lang="en">
|
||||
{{template "views/partials/head" .}}
|
||||
|
||||
<body class="bg-gradient-to-br from-gray-900 to-gray-950 text-gray-200">
|
||||
<body class="bg-gradient-to-br from-gray-900 via-gray-950 to-black text-gray-200">
|
||||
<div class="flex flex-col min-h-screen">
|
||||
|
||||
{{template "views/partials/navbar" .}}
|
||||
@@ -10,136 +10,183 @@
|
||||
<div class="container mx-auto px-4 py-8 flex-grow">
|
||||
|
||||
<!-- Hero Header -->
|
||||
<div class="bg-gradient-to-r from-indigo-900/30 to-purple-900/30 rounded-2xl shadow-xl p-6 mb-8">
|
||||
<div class="max-w-4xl mx-auto text-center">
|
||||
<h1 class="text-3xl md:text-4xl font-bold text-white mb-3">
|
||||
<span class="bg-clip-text text-transparent bg-gradient-to-r from-indigo-400 to-purple-400">
|
||||
<div class="relative bg-gradient-to-r from-indigo-900/40 via-purple-900/30 to-pink-900/40 rounded-3xl shadow-2xl p-8 mb-12 overflow-hidden">
|
||||
<!-- Background Pattern -->
|
||||
<div class="absolute inset-0 opacity-10">
|
||||
<div class="absolute inset-0 bg-gradient-to-r from-indigo-500/20 to-purple-500/20"></div>
|
||||
<div class="absolute top-0 left-0 w-full h-full" style="background-image: radial-gradient(circle at 1px 1px, rgba(255,255,255,0.15) 1px, transparent 0); background-size: 20px 20px;"></div>
|
||||
</div>
|
||||
|
||||
<div class="relative max-w-5xl mx-auto text-center">
|
||||
<h1 class="text-4xl md:text-5xl font-bold text-white mb-4">
|
||||
<span class="bg-clip-text text-transparent bg-gradient-to-r from-indigo-400 via-purple-400 to-pink-400">
|
||||
Model Gallery
|
||||
</span>
|
||||
</h1>
|
||||
<p class="text-lg text-gray-300 mb-2">
|
||||
<span class="font-semibold text-indigo-300">{{.AvailableModels}}</span> models from
|
||||
<span class="font-semibold text-purple-300">{{ len .Repositories }}</span> repositories
|
||||
<a href="https://localai.io/models/" target="_blank" class="ml-2 text-blue-400 hover:text-blue-300 transition">
|
||||
<i class="fas fa-circle-info"></i>
|
||||
</a>
|
||||
<p class="text-lg md:text-xl text-gray-300 mb-6 font-light">
|
||||
Discover and install AI models from our curated collection
|
||||
</p>
|
||||
<div class="flex flex-wrap justify-center items-center gap-6 text-sm md:text-base">
|
||||
<div class="flex items-center bg-white/10 rounded-full px-4 py-2">
|
||||
<div class="w-2 h-2 bg-indigo-400 rounded-full mr-2 animate-pulse"></div>
|
||||
<span class="font-semibold text-indigo-300">{{.AvailableModels}}</span>
|
||||
<span class="text-gray-300 ml-1">models available</span>
|
||||
</div>
|
||||
<div class="flex items-center bg-white/10 rounded-full px-4 py-2">
|
||||
<div class="w-2 h-2 bg-purple-400 rounded-full mr-2 animate-pulse"></div>
|
||||
<span class="font-semibold text-purple-300">{{ len .Repositories }}</span>
|
||||
<span class="text-gray-300 ml-1">repositories</span>
|
||||
</div>
|
||||
<a href="https://localai.io/models/" target="_blank"
|
||||
class="flex items-center bg-blue-600/80 hover:bg-blue-600 text-white px-4 py-2 rounded-full transition-all duration-300 hover:scale-105">
|
||||
<i class="fas fa-info-circle mr-2"></i>
|
||||
<span>Documentation</span>
|
||||
<i class="fas fa-external-link-alt ml-2 text-xs"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{template "views/partials/inprogress" .}}
|
||||
|
||||
<!-- Search and Filter Section -->
|
||||
<div class="bg-gray-800/70 rounded-xl p-6 mb-8 shadow-lg border border-gray-700/50">
|
||||
<!-- Search Input -->
|
||||
<div class="relative mb-6">
|
||||
<div class="absolute inset-y-0 start-0 flex items-center ps-3 pointer-events-none">
|
||||
<i class="fas fa-search text-gray-400"></i>
|
||||
</div>
|
||||
<input class="form-control block w-full pl-10 px-4 py-3 text-base font-normal text-gray-300 bg-gray-900/80 bg-clip-padding border border-gray-700/70 rounded-lg transition ease-in-out focus:text-gray-200 focus:bg-gray-900 focus:border-blue-500 focus:ring-1 focus:ring-blue-500/50 focus:outline-none"
|
||||
type="search"
|
||||
name="search"
|
||||
placeholder="Search models by name, tag, or description..."
|
||||
hx-post="browse/search/models"
|
||||
hx-trigger="input changed delay:500ms, search"
|
||||
hx-target="#search-results"
|
||||
oninput="hidePagination()"
|
||||
onchange="hidePagination()"
|
||||
onsearch="hidePagination()"
|
||||
hx-indicator=".htmx-indicator">
|
||||
<span class="htmx-indicator absolute right-3 top-3">
|
||||
<svg class="animate-spin h-5 w-5 text-blue-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
<div class="relative bg-gradient-to-br from-gray-800/80 to-gray-900/80 rounded-2xl p-8 mb-8 shadow-xl border border-gray-700/50 backdrop-blur-sm">
|
||||
<div class="absolute inset-0 rounded-2xl bg-gradient-to-br from-blue-500/5 to-purple-500/5"></div>
|
||||
|
||||
<!-- Filter by Type -->
|
||||
<div class="mb-4">
|
||||
<h3 class="text-gray-200 font-medium mb-3">Filter by type:</h3>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button hx-post="browse/search/models"
|
||||
class="inline-flex items-center rounded-full px-4 py-2 text-sm font-medium bg-indigo-900/60 text-indigo-200 border border-indigo-700/50 hover:bg-indigo-800 transition duration-200 ease-in-out"
|
||||
hx-target="#search-results"
|
||||
hx-vals='{"search": "tts"}'
|
||||
onclick="hidePagination()"
|
||||
hx-indicator=".htmx-indicator">
|
||||
<i class="fas fa-microphone mr-2"></i>TTS
|
||||
</button>
|
||||
<button hx-post="browse/search/models"
|
||||
class="inline-flex items-center rounded-full px-4 py-2 text-sm font-medium bg-purple-900/60 text-purple-200 border border-purple-700/50 hover:bg-purple-800 transition duration-200 ease-in-out"
|
||||
hx-target="#search-results"
|
||||
hx-vals='{"search": "stablediffusion"}'
|
||||
onclick="hidePagination()"
|
||||
hx-indicator=".htmx-indicator">
|
||||
<i class="fas fa-image mr-2"></i>Image generation
|
||||
</button>
|
||||
<button hx-post="browse/search/models"
|
||||
class="inline-flex items-center rounded-full px-4 py-2 text-sm font-medium bg-blue-900/60 text-blue-200 border border-blue-700/50 hover:bg-blue-800 transition duration-200 ease-in-out"
|
||||
hx-target="#search-results"
|
||||
hx-vals='{"search": "llm"}'
|
||||
onclick="hidePagination()"
|
||||
hx-indicator=".htmx-indicator">
|
||||
<i class="fas fa-comment-alt mr-2"></i>Text generation
|
||||
</button>
|
||||
<button hx-post="browse/search/models"
|
||||
class="inline-flex items-center rounded-full px-4 py-2 text-sm font-medium bg-green-900/60 text-green-200 border border-green-700/50 hover:bg-green-800 transition duration-200 ease-in-out"
|
||||
hx-target="#search-results"
|
||||
hx-vals='{"search": "multimodal"}'
|
||||
onclick="hidePagination()"
|
||||
hx-indicator=".htmx-indicator">
|
||||
<i class="fas fa-object-group mr-2"></i>Multimodal
|
||||
</button>
|
||||
<button hx-post="browse/search/models"
|
||||
class="inline-flex items-center rounded-full px-4 py-2 text-sm font-medium bg-cyan-900/60 text-cyan-200 border border-cyan-700/50 hover:bg-cyan-800 transition duration-200 ease-in-out"
|
||||
hx-target="#search-results"
|
||||
hx-vals='{"search": "embedding"}'
|
||||
onclick="hidePagination()"
|
||||
hx-indicator=".htmx-indicator">
|
||||
<i class="fas fa-vector-square mr-2"></i>Embeddings
|
||||
</button>
|
||||
<button hx-post="browse/search/models"
|
||||
class="inline-flex items-center rounded-full px-4 py-2 text-sm font-medium bg-amber-900/60 text-amber-200 border border-amber-700/50 hover:bg-amber-800 transition duration-200 ease-in-out"
|
||||
hx-target="#search-results"
|
||||
hx-vals='{"search": "rerank"}'
|
||||
onclick="hidePagination()"
|
||||
hx-indicator=".htmx-indicator">
|
||||
<i class="fas fa-sort-amount-up mr-2"></i>Rerankers
|
||||
</button>
|
||||
<button hx-post="browse/search/models"
|
||||
class="inline-flex items-center rounded-full px-4 py-2 text-sm font-medium bg-teal-900/60 text-teal-200 border border-teal-700/50 hover:bg-teal-800 transition duration-200 ease-in-out"
|
||||
hx-target="#search-results"
|
||||
hx-vals='{"search": "whisper"}'
|
||||
onclick="hidePagination()"
|
||||
hx-indicator=".htmx-indicator">
|
||||
<i class="fas fa-headphones mr-2"></i>Audio transcription
|
||||
</button>
|
||||
<button hx-post="browse/search/models"
|
||||
class="inline-flex items-center rounded-full px-4 py-2 text-sm font-medium bg-red-900/60 text-red-200 border border-red-700/50 hover:bg-red-800 transition duration-200 ease-in-out"
|
||||
hx-target="#search-results"
|
||||
hx-vals='{"search": "object-detection"}'
|
||||
onclick="hidePagination()"
|
||||
hx-indicator=".htmx-indicator">
|
||||
<i class="fas fa-eye mr-2"></i>Object detection
|
||||
</button>
|
||||
<div class="relative">
|
||||
<!-- Search Input -->
|
||||
<div class="mb-8">
|
||||
<h3 class="text-xl font-semibold text-white mb-4 flex items-center">
|
||||
<i class="fas fa-search mr-3 text-blue-400"></i>
|
||||
Find Your Perfect Model
|
||||
</h3>
|
||||
<div class="relative">
|
||||
<div class="absolute inset-y-0 start-0 flex items-center ps-4 pointer-events-none">
|
||||
<i class="fas fa-search text-gray-400"></i>
|
||||
</div>
|
||||
<input class="w-full pl-12 pr-16 py-4 text-base font-normal text-gray-300 bg-gray-900/90 border border-gray-700/70 rounded-xl transition-all duration-300 focus:text-gray-200 focus:bg-gray-900 focus:border-blue-500 focus:ring-2 focus:ring-blue-500/50 focus:outline-none"
|
||||
type="search"
|
||||
name="search"
|
||||
placeholder="Search models by name, tag, or description..."
|
||||
hx-post="browse/search/models"
|
||||
hx-trigger="input changed delay:500ms, search"
|
||||
hx-target="#search-results"
|
||||
oninput="hidePagination()"
|
||||
onchange="hidePagination()"
|
||||
onsearch="hidePagination()"
|
||||
hx-indicator=".htmx-indicator">
|
||||
<span class="htmx-indicator absolute right-4 top-4">
|
||||
<svg class="animate-spin h-6 w-6 text-blue-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filter by Tags -->
|
||||
<div class="mt-5">
|
||||
<h3 class="text-gray-200 font-medium mb-2">Filter by tags:</h3>
|
||||
<div class="flex flex-wrap gap-2 max-h-24 overflow-y-auto scrollbar-thin scrollbar-thumb-gray-700 scrollbar-track-gray-900 pr-2">
|
||||
{{ range .AllTags }}
|
||||
<button hx-post="browse/search/models"
|
||||
class="inline-flex items-center text-xs px-3 py-1 rounded-full bg-gray-700/60 text-gray-300 border border-gray-600/50 hover:bg-gray-600 hover:text-gray-100 transition duration-200 ease-in-out"
|
||||
|
||||
<!-- Filter by Type -->
|
||||
<div class="mb-8">
|
||||
<h3 class="text-lg font-semibold text-white mb-4 flex items-center">
|
||||
<i class="fas fa-filter mr-3 text-purple-400"></i>
|
||||
Filter by Model Type
|
||||
</h3>
|
||||
<div class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-8 gap-3">
|
||||
<button hx-post="browse/search/models"
|
||||
class="group flex items-center justify-center rounded-xl px-4 py-3 text-sm font-semibold bg-gradient-to-r from-indigo-600/80 to-indigo-700/80 hover:from-indigo-600 hover:to-indigo-700 text-indigo-100 border border-indigo-500/30 hover:border-indigo-400/50 transition-all duration-300 transform hover:scale-105 hover:shadow-lg hover:shadow-indigo-500/25"
|
||||
hx-target="#search-results"
|
||||
hx-vals='{"search": "{{.}}"}'
|
||||
hx-vals='{"search": "tts"}'
|
||||
onclick="hidePagination()"
|
||||
hx-indicator=".htmx-indicator">
|
||||
<i class="fas fa-tag text-xs mr-1.5"></i>{{.}}
|
||||
<i class="fas fa-microphone mr-2 group-hover:animate-pulse"></i>
|
||||
<span>TTS</span>
|
||||
</button>
|
||||
{{ end }}
|
||||
<button hx-post="browse/search/models"
|
||||
class="group flex items-center justify-center rounded-xl px-4 py-3 text-sm font-semibold bg-gradient-to-r from-purple-600/80 to-purple-700/80 hover:from-purple-600 hover:to-purple-700 text-purple-100 border border-purple-500/30 hover:border-purple-400/50 transition-all duration-300 transform hover:scale-105 hover:shadow-lg hover:shadow-purple-500/25"
|
||||
hx-target="#search-results"
|
||||
hx-vals='{"search": "stablediffusion"}'
|
||||
onclick="hidePagination()"
|
||||
hx-indicator=".htmx-indicator">
|
||||
<i class="fas fa-image mr-2 group-hover:animate-pulse"></i>
|
||||
<span>Image</span>
|
||||
</button>
|
||||
<button hx-post="browse/search/models"
|
||||
class="group flex items-center justify-center rounded-xl px-4 py-3 text-sm font-semibold bg-gradient-to-r from-blue-600/80 to-blue-700/80 hover:from-blue-600 hover:to-blue-700 text-blue-100 border border-blue-500/30 hover:border-blue-400/50 transition-all duration-300 transform hover:scale-105 hover:shadow-lg hover:shadow-blue-500/25"
|
||||
hx-target="#search-results"
|
||||
hx-vals='{"search": "llm"}'
|
||||
onclick="hidePagination()"
|
||||
hx-indicator=".htmx-indicator">
|
||||
<i class="fas fa-comment-alt mr-2 group-hover:animate-bounce"></i>
|
||||
<span>LLM</span>
|
||||
</button>
|
||||
<button hx-post="browse/search/models"
|
||||
class="group flex items-center justify-center rounded-xl px-4 py-3 text-sm font-semibold bg-gradient-to-r from-green-600/80 to-green-700/80 hover:from-green-600 hover:to-green-700 text-green-100 border border-green-500/30 hover:border-green-400/50 transition-all duration-300 transform hover:scale-105 hover:shadow-lg hover:shadow-green-500/25"
|
||||
hx-target="#search-results"
|
||||
hx-vals='{"search": "multimodal"}'
|
||||
onclick="hidePagination()"
|
||||
hx-indicator=".htmx-indicator">
|
||||
<i class="fas fa-object-group mr-2 group-hover:animate-pulse"></i>
|
||||
<span>Multimodal</span>
|
||||
</button>
|
||||
<button hx-post="browse/search/models"
|
||||
class="group flex items-center justify-center rounded-xl px-4 py-3 text-sm font-semibold bg-gradient-to-r from-cyan-600/80 to-cyan-700/80 hover:from-cyan-600 hover:to-cyan-700 text-cyan-100 border border-cyan-500/30 hover:border-cyan-400/50 transition-all duration-300 transform hover:scale-105 hover:shadow-lg hover:shadow-cyan-500/25"
|
||||
hx-target="#search-results"
|
||||
hx-vals='{"search": "embedding"}'
|
||||
onclick="hidePagination()"
|
||||
hx-indicator=".htmx-indicator">
|
||||
<i class="fas fa-vector-square mr-2 group-hover:animate-pulse"></i>
|
||||
<span>Embedding</span>
|
||||
</button>
|
||||
<button hx-post="browse/search/models"
|
||||
class="group flex items-center justify-center rounded-xl px-4 py-3 text-sm font-semibold bg-gradient-to-r from-amber-600/80 to-amber-700/80 hover:from-amber-600 hover:to-amber-700 text-amber-100 border border-amber-500/30 hover:border-amber-400/50 transition-all duration-300 transform hover:scale-105 hover:shadow-lg hover:shadow-amber-500/25"
|
||||
hx-target="#search-results"
|
||||
hx-vals='{"search": "rerank"}'
|
||||
onclick="hidePagination()"
|
||||
hx-indicator=".htmx-indicator">
|
||||
<i class="fas fa-sort-amount-up mr-2 group-hover:animate-pulse"></i>
|
||||
<span>Rerank</span>
|
||||
</button>
|
||||
<button hx-post="browse/search/models"
|
||||
class="group flex items-center justify-center rounded-xl px-4 py-3 text-sm font-semibold bg-gradient-to-r from-teal-600/80 to-teal-700/80 hover:from-teal-600 hover:to-teal-700 text-teal-100 border border-teal-500/30 hover:border-teal-400/50 transition-all duration-300 transform hover:scale-105 hover:shadow-lg hover:shadow-teal-500/25"
|
||||
hx-target="#search-results"
|
||||
hx-vals='{"search": "whisper"}'
|
||||
onclick="hidePagination()"
|
||||
hx-indicator=".htmx-indicator">
|
||||
<i class="fas fa-headphones mr-2 group-hover:animate-pulse"></i>
|
||||
<span>Whisper</span>
|
||||
</button>
|
||||
<button hx-post="browse/search/models"
|
||||
class="group flex items-center justify-center rounded-xl px-4 py-3 text-sm font-semibold bg-gradient-to-r from-red-600/80 to-red-700/80 hover:from-red-600 hover:to-red-700 text-red-100 border border-red-500/30 hover:border-red-400/50 transition-all duration-300 transform hover:scale-105 hover:shadow-lg hover:shadow-red-500/25"
|
||||
hx-target="#search-results"
|
||||
hx-vals='{"search": "object-detection"}'
|
||||
onclick="hidePagination()"
|
||||
hx-indicator=".htmx-indicator">
|
||||
<i class="fas fa-eye mr-2 group-hover:animate-pulse"></i>
|
||||
<span>Vision</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filter by Tags -->
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-white mb-4 flex items-center">
|
||||
<i class="fas fa-tags mr-3 text-pink-400"></i>
|
||||
Browse by Tags
|
||||
</h3>
|
||||
<div class="max-h-32 overflow-y-auto scrollbar-thin scrollbar-thumb-gray-600/50 scrollbar-track-gray-800/50 pr-2">
|
||||
<div class="flex flex-wrap gap-2">
|
||||
{{ range .AllTags }}
|
||||
<button hx-post="browse/search/models"
|
||||
class="group inline-flex items-center text-xs px-3 py-2 rounded-full bg-gray-700/60 hover:bg-gray-600/80 text-gray-300 hover:text-white border border-gray-600/50 hover:border-gray-500/70 transition-all duration-200 ease-in-out transform hover:scale-105"
|
||||
hx-target="#search-results"
|
||||
hx-vals='{"search": "{{.}}"}'
|
||||
onclick="hidePagination()"
|
||||
hx-indicator=".htmx-indicator">
|
||||
<i class="fas fa-tag text-xs mr-2 group-hover:animate-pulse"></i>
|
||||
<span>{{.}}</span>
|
||||
</button>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -151,20 +198,22 @@
|
||||
|
||||
<!-- Pagination -->
|
||||
{{ if gt .AvailableModels $numModelsPerPage }}
|
||||
<div id="paginate" class="flex justify-center mt-8">
|
||||
<div class="flex items-center gap-4">
|
||||
<div id="paginate" class="flex justify-center mt-12">
|
||||
<div class="flex items-center gap-4 bg-gray-800/60 rounded-2xl p-4 backdrop-blur-sm border border-gray-700/50">
|
||||
<button onclick="window.location.href='browse?page={{.PrevPage}}'"
|
||||
class="flex items-center justify-center h-10 w-10 bg-gray-800/80 text-gray-300 hover:bg-indigo-900/70 hover:text-white rounded-lg shadow transition duration-300 ease-in-out {{if not .PrevPage}}opacity-50 cursor-not-allowed{{end}}"
|
||||
class="group flex items-center justify-center h-12 w-12 bg-gray-700/80 hover:bg-indigo-600 text-gray-300 hover:text-white rounded-xl shadow-lg transition-all duration-300 ease-in-out transform hover:scale-110 {{if not .PrevPage}}opacity-50 cursor-not-allowed{{end}}"
|
||||
{{if not .PrevPage}}disabled{{end}}>
|
||||
<i class="fas fa-chevron-left"></i>
|
||||
<i class="fas fa-chevron-left group-hover:animate-pulse"></i>
|
||||
</button>
|
||||
<div class="text-gray-400 text-sm">
|
||||
Page <span class="text-white font-medium">{{add .PrevPage 1}}</span>
|
||||
<div class="text-gray-300 text-sm font-medium px-4">
|
||||
<span class="text-gray-400">Page</span>
|
||||
<span class="text-white font-bold text-lg mx-2">{{add .PrevPage 1}}</span>
|
||||
<span class="text-gray-400">of many</span>
|
||||
</div>
|
||||
<button onclick="window.location.href='browse?page={{.NextPage}}'"
|
||||
class="flex items-center justify-center h-10 w-10 bg-gray-800/80 text-gray-300 hover:bg-indigo-900/70 hover:text-white rounded-lg shadow transition duration-300 ease-in-out {{if not .NextPage}}opacity-50 cursor-not-allowed{{end}}"
|
||||
class="group flex items-center justify-center h-12 w-12 bg-gray-700/80 hover:bg-indigo-600 text-gray-300 hover:text-white rounded-xl shadow-lg transition-all duration-300 ease-in-out transform hover:scale-110 {{if not .NextPage}}opacity-50 cursor-not-allowed{{end}}"
|
||||
{{if not .NextPage}}disabled{{end}}>
|
||||
<i class="fas fa-chevron-right"></i>
|
||||
<i class="fas fa-chevron-right group-hover:animate-pulse"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -174,6 +223,135 @@
|
||||
{{template "views/partials/footer" .}}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* Enhanced scrollbar styling */
|
||||
.scrollbar-thin::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.scrollbar-thin::-webkit-scrollbar-track {
|
||||
background: rgba(31, 41, 55, 0.5);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.scrollbar-thin::-webkit-scrollbar-thumb {
|
||||
background: rgba(107, 114, 128, 0.5);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.scrollbar-thin::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(107, 114, 128, 0.8);
|
||||
}
|
||||
|
||||
/* Add some custom CSS for gallery model cards to match our theme */
|
||||
#search-results .dark\:bg-gray-800 {
|
||||
background: linear-gradient(135deg, rgba(31, 41, 55, 0.9) 0%, rgba(17, 24, 39, 0.9) 100%) !important;
|
||||
border: 1px solid rgba(75, 85, 99, 0.5) !important;
|
||||
border-radius: 1rem !important;
|
||||
transition: all 0.5s ease !important;
|
||||
backdrop-filter: blur(8px) !important;
|
||||
}
|
||||
|
||||
#search-results .dark\:bg-gray-800:hover {
|
||||
transform: translateY(-8px) !important;
|
||||
box-shadow: 0 25px 50px -12px rgba(59, 130, 246, 0.1) !important;
|
||||
border-color: rgba(59, 130, 246, 0.3) !important;
|
||||
}
|
||||
|
||||
/* Style the install buttons */
|
||||
#search-results .bg-blue-600 {
|
||||
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%) !important;
|
||||
border-radius: 0.75rem !important;
|
||||
padding: 0.75rem 1.5rem !important;
|
||||
font-weight: 600 !important;
|
||||
transition: all 0.3s ease !important;
|
||||
box-shadow: 0 4px 15px rgba(59, 130, 246, 0.25) !important;
|
||||
}
|
||||
|
||||
#search-results .bg-blue-600:hover {
|
||||
background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%) !important;
|
||||
transform: scale(1.05) !important;
|
||||
box-shadow: 0 8px 25px rgba(59, 130, 246, 0.4) !important;
|
||||
}
|
||||
|
||||
/* Style the model images */
|
||||
#search-results img.rounded-t-lg {
|
||||
border-radius: 1rem !important;
|
||||
transition: transform 0.3s ease !important;
|
||||
}
|
||||
|
||||
#search-results .dark\:bg-gray-800:hover img.rounded-t-lg {
|
||||
transform: scale(1.05) !important;
|
||||
}
|
||||
|
||||
/* Style the progress bars */
|
||||
#search-results .progress {
|
||||
background: linear-gradient(135deg, rgba(59, 130, 246, 0.2) 0%, rgba(99, 102, 241, 0.2) 100%) !important;
|
||||
border-radius: 0.5rem !important;
|
||||
border: 1px solid rgba(59, 130, 246, 0.3) !important;
|
||||
}
|
||||
|
||||
/* Style action buttons */
|
||||
#search-results button[class*="primary"] {
|
||||
background: linear-gradient(135deg, #6366f1 0%, #4f46e5 100%) !important;
|
||||
border-radius: 0.5rem !important;
|
||||
transition: all 0.2s ease !important;
|
||||
}
|
||||
|
||||
#search-results button[class*="primary"]:hover {
|
||||
transform: scale(1.05) !important;
|
||||
box-shadow: 0 4px 15px rgba(99, 102, 241, 0.3) !important;
|
||||
}
|
||||
|
||||
/* Style the reinstall buttons specifically */
|
||||
#search-results .bg-primary {
|
||||
background: linear-gradient(135deg, #059669 0%, #047857 100%) !important;
|
||||
border-radius: 0.75rem !important;
|
||||
padding: 0.75rem 1.5rem !important;
|
||||
font-weight: 600 !important;
|
||||
transition: all 0.3s ease !important;
|
||||
box-shadow: 0 4px 15px rgba(5, 150, 105, 0.25) !important;
|
||||
}
|
||||
|
||||
#search-results .bg-primary:hover {
|
||||
background: linear-gradient(135deg, #047857 0%, #065f46 100%) !important;
|
||||
transform: scale(1.05) !important;
|
||||
box-shadow: 0 8px 25px rgba(5, 150, 105, 0.4) !important;
|
||||
}
|
||||
|
||||
/* Style the delete buttons */
|
||||
#search-results .bg-red-800 {
|
||||
background: linear-gradient(135deg, #dc2626 0%, #b91c1c 100%) !important;
|
||||
border-radius: 0.75rem !important;
|
||||
padding: 0.75rem 1.5rem !important;
|
||||
font-weight: 600 !important;
|
||||
transition: all 0.3s ease !important;
|
||||
box-shadow: 0 4px 15px rgba(220, 38, 38, 0.25) !important;
|
||||
}
|
||||
|
||||
#search-results .bg-red-800:hover {
|
||||
background: linear-gradient(135deg, #b91c1c 0%, #991b1b 100%) !important;
|
||||
transform: scale(1.05) !important;
|
||||
box-shadow: 0 8px 25px rgba(220, 38, 38, 0.4) !important;
|
||||
}
|
||||
|
||||
/* Style the info buttons */
|
||||
#search-results .bg-gray-700 {
|
||||
background: linear-gradient(135deg, #374151 0%, #1f2937 100%) !important;
|
||||
border-radius: 0.75rem !important;
|
||||
padding: 0.75rem 1.5rem !important;
|
||||
font-weight: 600 !important;
|
||||
transition: all 0.3s ease !important;
|
||||
box-shadow: 0 4px 15px rgba(55, 65, 81, 0.25) !important;
|
||||
}
|
||||
|
||||
#search-results .bg-gray-700:hover {
|
||||
background: linear-gradient(135deg, #1f2937 0%, #111827 100%) !important;
|
||||
transform: scale(1.05) !important;
|
||||
box-shadow: 0 8px 25px rgba(55, 65, 81, 0.4) !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
function hidePagination() {
|
||||
const paginateDiv = document.getElementById('paginate');
|
||||
@@ -188,6 +366,21 @@
|
||||
hidePagination();
|
||||
}
|
||||
});
|
||||
|
||||
// Add loading state animation
|
||||
document.body.addEventListener('htmx:beforeRequest', function(event) {
|
||||
const searchInput = document.querySelector('input[name="search"]');
|
||||
if (searchInput && event.detail.elt === searchInput) {
|
||||
searchInput.classList.add('animate-pulse');
|
||||
}
|
||||
});
|
||||
|
||||
document.body.addEventListener('htmx:afterRequest', function(event) {
|
||||
const searchInput = document.querySelector('input[name="search"]');
|
||||
if (searchInput) {
|
||||
searchInput.classList.remove('animate-pulse');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
@@ -29,23 +29,6 @@ func NewBackendMonitorService(modelLoader *model.ModelLoader, configLoader *conf
|
||||
}
|
||||
}
|
||||
|
||||
func (bms BackendMonitorService) getModelLoaderIDFromModelName(modelName string) (string, error) {
|
||||
config, exists := bms.modelConfigLoader.GetModelConfig(modelName)
|
||||
var backendId string
|
||||
if exists {
|
||||
backendId = config.Model
|
||||
} else {
|
||||
// Last ditch effort: use it raw, see if a backend happens to match.
|
||||
backendId = modelName
|
||||
}
|
||||
|
||||
if !strings.HasSuffix(backendId, ".bin") {
|
||||
backendId = fmt.Sprintf("%s.bin", backendId)
|
||||
}
|
||||
|
||||
return backendId, nil
|
||||
}
|
||||
|
||||
func (bms *BackendMonitorService) SampleLocalBackendProcess(model string) (*schema.BackendMonitorResponse, error) {
|
||||
config, exists := bms.modelConfigLoader.GetModelConfig(model)
|
||||
var backend string
|
||||
@@ -102,21 +85,17 @@ func (bms *BackendMonitorService) SampleLocalBackendProcess(model string) (*sche
|
||||
}
|
||||
|
||||
func (bms BackendMonitorService) CheckAndSample(modelName string) (*proto.StatusResponse, error) {
|
||||
backendId, err := bms.getModelLoaderIDFromModelName(modelName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
modelAddr := bms.modelLoader.CheckIsLoaded(backendId)
|
||||
modelAddr := bms.modelLoader.CheckIsLoaded(modelName)
|
||||
if modelAddr == nil {
|
||||
return nil, fmt.Errorf("backend %s is not currently loaded", backendId)
|
||||
return nil, fmt.Errorf("backend %s is not currently loaded", modelName)
|
||||
}
|
||||
|
||||
status, rpcErr := modelAddr.GRPC(false, nil).Status(context.TODO())
|
||||
if rpcErr != nil {
|
||||
log.Warn().Msgf("backend %s experienced an error retrieving status info: %s", backendId, rpcErr.Error())
|
||||
val, slbErr := bms.SampleLocalBackendProcess(backendId)
|
||||
log.Warn().Msgf("backend %s experienced an error retrieving status info: %s", modelName, rpcErr.Error())
|
||||
val, slbErr := bms.SampleLocalBackendProcess(modelName)
|
||||
if slbErr != nil {
|
||||
return nil, fmt.Errorf("backend %s experienced an error retrieving status info via rpc: %s, then failed local node process sample: %s", backendId, rpcErr.Error(), slbErr.Error())
|
||||
return nil, fmt.Errorf("backend %s experienced an error retrieving status info via rpc: %s, then failed local node process sample: %s", modelName, rpcErr.Error(), slbErr.Error())
|
||||
}
|
||||
return &proto.StatusResponse{
|
||||
State: proto.StatusResponse_ERROR,
|
||||
@@ -132,9 +111,5 @@ func (bms BackendMonitorService) CheckAndSample(modelName string) (*proto.Status
|
||||
}
|
||||
|
||||
func (bms BackendMonitorService) ShutdownModel(modelName string) error {
|
||||
backendId, err := bms.getModelLoaderIDFromModelName(modelName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return bms.modelLoader.ShutdownModel(backendId)
|
||||
return bms.modelLoader.ShutdownModel(modelName)
|
||||
}
|
||||
|
||||
@@ -135,7 +135,7 @@ FILE:
|
||||
return models, nil
|
||||
}
|
||||
|
||||
func (ml *ModelLoader) ListModels() []*Model {
|
||||
func (ml *ModelLoader) ListLoadedModels() []*Model {
|
||||
ml.mu.Lock()
|
||||
defer ml.mu.Unlock()
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ var _ = Describe("ModelLoader", func() {
|
||||
It("should create a new ModelLoader with an empty model map", func() {
|
||||
Expect(modelLoader).ToNot(BeNil())
|
||||
Expect(modelLoader.ModelPath).To(Equal(modelPath))
|
||||
Expect(modelLoader.ListModels()).To(BeEmpty())
|
||||
Expect(modelLoader.ListLoadedModels()).To(BeEmpty())
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user