Files
LocalAI/core/http/views/models.html
Ettore Di Giacinto bef4c10629 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>
2025-08-16 07:44:50 +02:00

387 lines
21 KiB
HTML

<!DOCTYPE html>
<html lang="en">
{{template "views/partials/head" .}}
<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" .}}
{{ $numModelsPerPage := 21 }}
<div class="container mx-auto px-4 py-8 flex-grow">
<!-- Hero Header -->
<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 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="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>
<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>
<!-- 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": "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/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>
<!-- Results Section -->
<div id="search-results" class="transition-all duration-300">
{{.Models}}
</div>
<!-- Pagination -->
{{ if gt .AvailableModels $numModelsPerPage }}
<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="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 group-hover:animate-pulse"></i>
</button>
<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="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 group-hover:animate-pulse"></i>
</button>
</div>
</div>
{{ end }}
</div>
{{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');
if (paginateDiv) {
paginateDiv.style.display = 'none';
}
}
// Listen for the htmx:afterSwap event to handle cases when the search results are updated
document.body.addEventListener('htmx:afterSwap', function(event) {
if (event.detail.target.id === 'search-results') {
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>
</html>