Files
LocalAI/core/http/views/index.html
Ettore Di Giacinto eebda7204e feat(ui): add front-page stats
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2025-10-28 15:58:00 +01:00

510 lines
25 KiB
HTML

<!DOCTYPE html>
<html lang="en">
{{template "views/partials/head" .}}
<body class="bg-[#101827] text-[#E5E7EB]">
<div class="flex flex-col min-h-screen" x-data="dashboardMetrics()">
{{template "views/partials/navbar" .}}
<div class="container mx-auto px-4 py-8 flex-grow">
{{ if eq (len .ModelsConfig) 0 }}
<!-- Welcome/Wizard Section (No Models Installed) -->
<div class="relative bg-[#1E293B] border border-[#38BDF8]/20 rounded-3xl shadow-2xl shadow-[#38BDF8]/10 p-12 mb-12 overflow-hidden">
<!-- Background Pattern -->
<div class="absolute inset-0 opacity-10">
<div class="absolute inset-0 bg-gradient-to-r from-[#38BDF8]/20 to-[#8B5CF6]/20"></div>
<div class="absolute top-0 left-0 w-full h-full" style="background-image: radial-gradient(circle at 1px 1px, rgba(56,189,248,0.15) 1px, transparent 0); background-size: 20px 20px;"></div>
</div>
<div class="relative text-center max-w-4xl mx-auto">
<div class="inline-flex items-center justify-center w-24 h-24 rounded-full bg-[#38BDF8]/10 border border-[#38BDF8]/20 mb-8">
<i class="text-[#38BDF8] text-4xl fas fa-robot"></i>
</div>
<h1 class="text-5xl md:text-6xl font-bold text-[#E5E7EB] mb-6">
<span class="bg-clip-text text-transparent bg-gradient-to-r from-[#38BDF8] via-[#8B5CF6] to-[#38BDF8]">
Welcome to <em class="not-italic font-black">your</em> LocalAI
</span>
</h1>
<p class="text-xl md:text-2xl text-[#94A3B8] mb-8 leading-relaxed">Get started by installing your first AI model</p>
<div class="flex flex-wrap justify-center gap-4 mb-12">
<a href="browse" class="group inline-flex items-center bg-[#38BDF8] hover:bg-[#38BDF8]/90 text-[#101827] py-4 px-8 rounded-xl font-semibold text-lg transition-all duration-300 transform hover:scale-105 hover:shadow-[0_0_20px_rgba(56,189,248,0.4)]">
<i class="fas fa-images mr-3 text-xl"></i>
Browse Model Gallery
<i class="fas fa-arrow-right ml-3 opacity-0 group-hover:opacity-100 group-hover:translate-x-1 transition-all duration-300"></i>
</a>
<a href="https://localai.io/basics/getting_started/" target="_blank" class="group inline-flex items-center bg-[#1E293B] hover:bg-[#1E293B]/80 border border-[#38BDF8]/20 text-[#E5E7EB] py-4 px-8 rounded-xl font-semibold text-lg transition-all duration-300 transform hover:scale-105">
<i class="fas fa-book mr-3 text-xl"></i>
Documentation
<i class="fas fa-external-link-alt ml-3 text-sm"></i>
</a>
</div>
<div class="bg-[#101827]/50 rounded-2xl p-8 border border-[#38BDF8]/10">
<h3 class="text-2xl font-bold text-[#E5E7EB] mb-4">Quick Start Guide</h3>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 text-left">
<div class="flex items-start space-x-4">
<div class="flex-shrink-0 w-10 h-10 rounded-lg bg-[#38BDF8]/20 flex items-center justify-center">
<span class="text-[#38BDF8] font-bold text-lg">1</span>
</div>
<div>
<h4 class="font-semibold text-[#E5E7EB] mb-1">Browse Models</h4>
<p class="text-sm text-[#94A3B8]">Explore our curated gallery of AI models</p>
</div>
</div>
<div class="flex items-start space-x-4">
<div class="flex-shrink-0 w-10 h-10 rounded-lg bg-[#8B5CF6]/20 flex items-center justify-center">
<span class="text-[#8B5CF6] font-bold text-lg">2</span>
</div>
<div>
<h4 class="font-semibold text-[#E5E7EB] mb-1">Install Model</h4>
<p class="text-sm text-[#94A3B8]">One-click installation from the gallery</p>
</div>
</div>
<div class="flex items-start space-x-4">
<div class="flex-shrink-0 w-10 h-10 rounded-lg bg-green-500/20 flex items-center justify-center">
<span class="text-green-400 font-bold text-lg">3</span>
</div>
<div>
<h4 class="font-semibold text-[#E5E7EB] mb-1">Start Using</h4>
<p class="text-sm text-[#94A3B8]">Begin chatting or generating content</p>
</div>
</div>
</div>
</div>
</div>
</div>
{{ else }}
<!-- Dashboard View (Models Installed) -->
<!-- Hero Section -->
<div class="relative bg-[#1E293B] border border-[#38BDF8]/20 rounded-3xl shadow-2xl shadow-[#38BDF8]/10 p-8 mb-12 overflow-hidden">
<div class="absolute inset-0 opacity-10">
<div class="absolute inset-0 bg-gradient-to-r from-[#38BDF8]/20 to-[#8B5CF6]/20"></div>
<div class="absolute top-0 left-0 w-full h-full" style="background-image: radial-gradient(circle at 1px 1px, rgba(56,189,248,0.15) 1px, transparent 0); background-size: 20px 20px;"></div>
</div>
<div class="relative max-w-5xl mx-auto">
<div class="flex items-center justify-between mb-6">
<div>
<h1 class="text-4xl md:text-5xl font-bold text-[#E5E7EB] mb-2">
<span class="bg-clip-text text-transparent bg-gradient-to-r from-[#38BDF8] via-[#8B5CF6] to-[#38BDF8]">
Dashboard
</span>
</h1>
<p class="text-lg text-[#94A3B8]">Monitor your LocalAI instance</p>
</div>
<a href="/settings" class="inline-flex items-center bg-[#8B5CF6] hover:bg-[#8B5CF6]/90 text-white py-3 px-6 rounded-xl font-semibold transition-all duration-300 transform hover:scale-105 hover:shadow-[0_0_15px_rgba(139,92,246,0.4)]">
<i class="fas fa-cog mr-2"></i>
Settings
</a>
</div>
</div>
</div>
<!-- Stats Cards -->
<div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-6 mb-12">
<!-- Models Card -->
<a href="/settings" class="group relative bg-[#1E293B] border border-[#38BDF8]/20 rounded-2xl p-6 transition-all duration-300 hover:shadow-[0_0_20px_rgba(56,189,248,0.2)] hover:-translate-y-1 hover:border-[#38BDF8]/50">
<div class="flex items-center justify-between mb-4">
<div class="w-12 h-12 rounded-xl bg-[#38BDF8]/20 flex items-center justify-center group-hover:scale-110 transition-transform">
<i class="fas fa-brain text-2xl text-[#38BDF8]"></i>
</div>
<i class="fas fa-arrow-right text-[#38BDF8] opacity-0 group-hover:opacity-100 transition-opacity"></i>
</div>
<h3 class="text-3xl font-bold text-[#E5E7EB] mb-1">{{len .ModelsConfig}}</h3>
<p class="text-[#94A3B8] text-sm">Installed Models</p>
</a>
<!-- Backends Card -->
<a href="/settings" class="group relative bg-[#1E293B] border border-[#8B5CF6]/20 rounded-2xl p-6 transition-all duration-300 hover:shadow-[0_0_20px_rgba(139,92,246,0.2)] hover:-translate-y-1 hover:border-[#8B5CF6]/50">
<div class="flex items-center justify-between mb-4">
<div class="w-12 h-12 rounded-xl bg-[#8B5CF6]/20 flex items-center justify-center group-hover:scale-110 transition-transform">
<i class="fas fa-cog text-2xl text-[#8B5CF6]"></i>
</div>
<i class="fas fa-arrow-right text-[#8B5CF6] opacity-0 group-hover:opacity-100 transition-opacity"></i>
</div>
<h3 class="text-3xl font-bold text-[#E5E7EB] mb-1">{{len .InstalledBackends}}</h3>
<p class="text-[#94A3B8] text-sm">Active Backends</p>
</a>
<!-- API Requests Card -->
<div class="relative bg-[#1E293B] border border-green-500/20 rounded-2xl p-6 transition-all duration-300 hover:shadow-[0_0_20px_rgba(34,197,94,0.2)]">
<div class="flex items-center justify-between mb-4">
<div class="w-12 h-12 rounded-xl bg-green-500/20 flex items-center justify-center">
<i class="fas fa-chart-line text-2xl text-green-400"></i>
</div>
</div>
<h3 class="text-3xl font-bold text-[#E5E7EB] mb-1" x-text="totalRequests.toLocaleString()">0</h3>
<p class="text-[#94A3B8] text-sm">Total API Requests</p>
<div class="mt-3 pt-3 border-t border-[#101827]">
<p class="text-green-400 font-semibold" x-text="successRate.toFixed(1) + '% success'">0% success</p>
</div>
</div>
<!-- Active Models Card -->
<div class="relative bg-[#1E293B] border border-orange-500/20 rounded-2xl p-6 transition-all duration-300 hover:shadow-[0_0_20px_rgba(234,88,12,0.2)]">
<div class="flex items-center justify-between mb-4">
<div class="w-12 h-12 rounded-xl bg-orange-500/20 flex items-center justify-center">
<i class="fas fa-play text-2xl text-orange-400"></i>
</div>
</div>
<h3 class="text-3xl font-bold text-[#E5E7EB] mb-1">{{ len .LoadedModels }}</h3>
<p class="text-[#94A3B8] text-sm">Currently Loaded</p>
</div>
</div>
<!-- Charts Section -->
<div class="grid grid-cols-1 xl:grid-cols-2 gap-6 mb-12">
<!-- Endpoint Usage Chart -->
<div class="bg-[#1E293B] border border-[#38BDF8]/20 rounded-2xl p-6">
<h3 class="text-xl font-bold text-[#E5E7EB] mb-4 flex items-center">
<i class="fas fa-chart-pie mr-3 text-[#38BDF8]"></i>
Endpoint Usage
</h3>
<div class="relative" style="height: 300px;">
<canvas id="endpointChart" x-show="hasEndpointData"></canvas>
<div x-show="!hasEndpointData" class="flex items-center justify-center h-full text-[#94A3B8]">
<div class="text-center">
<i class="fas fa-chart-pie text-4xl mb-2 opacity-50"></i>
<p>No API requests yet</p>
</div>
</div>
</div>
</div>
<!-- Model Usage Chart -->
<div class="bg-[#1E293B] border border-[#8B5CF6]/20 rounded-2xl p-6">
<h3 class="text-xl font-bold text-[#E5E7EB] mb-4 flex items-center">
<i class="fas fa-brain mr-3 text-[#8B5CF6]"></i>
Model Usage
</h3>
<div class="relative" style="height: 300px;">
<canvas id="modelChart" x-show="hasModelData"></canvas>
<div x-show="!hasModelData" class="flex items-center justify-center h-full text-[#94A3B8]">
<div class="text-center">
<i class="fas fa-brain text-4xl mb-2 opacity-50"></i>
<p>No model requests yet</p>
</div>
</div>
</div>
</div>
</div>
<!-- Timeline Chart -->
<div class="bg-[#1E293B] border border-green-500/20 rounded-2xl p-6 mb-12">
<h3 class="text-xl font-bold text-[#E5E7EB] mb-4 flex items-center">
<i class="fas fa-chart-line mr-3 text-green-400"></i>
Requests Over Time (Last 24 Hours)
</h3>
<div class="relative" style="height: 300px;">
<canvas id="timelineChart" x-show="hasTimelineData"></canvas>
<div x-show="!hasTimelineData" class="flex items-center justify-center h-full text-[#94A3B8]">
<div class="text-center">
<i class="fas fa-chart-line text-4xl mb-2 opacity-50"></i>
<p>No timeline data yet</p>
</div>
</div>
</div>
</div>
<!-- Quick Links Section -->
<div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-6">
<a href="/settings" class="group bg-[#1E293B] border border-[#38BDF8]/20 rounded-2xl p-6 transition-all duration-300 hover:shadow-[0_0_15px_rgba(56,189,248,0.2)] hover:-translate-y-1">
<div class="flex items-center space-x-4">
<div class="w-12 h-12 rounded-xl bg-[#38BDF8]/20 flex items-center justify-center group-hover:scale-110 transition-transform">
<i class="fas fa-cog text-xl text-[#38BDF8]"></i>
</div>
<div>
<h4 class="font-semibold text-[#E5E7EB] group-hover:text-[#38BDF8] transition-colors">Manage</h4>
<p class="text-sm text-[#94A3B8]">Models & Backends</p>
</div>
</div>
</a>
<a href="/browse" class="group bg-[#1E293B] border border-[#8B5CF6]/20 rounded-2xl p-6 transition-all duration-300 hover:shadow-[0_0_15px_rgba(139,92,246,0.2)] hover:-translate-y-1">
<div class="flex items-center space-x-4">
<div class="w-12 h-12 rounded-xl bg-[#8B5CF6]/20 flex items-center justify-center group-hover:scale-110 transition-transform">
<i class="fas fa-images text-xl text-[#8B5CF6]"></i>
</div>
<div>
<h4 class="font-semibold text-[#E5E7EB] group-hover:text-[#8B5CF6] transition-colors">Gallery</h4>
<p class="text-sm text-[#94A3B8]">Browse Models</p>
</div>
</div>
</a>
<a href="https://localai.io" target="_blank" class="group bg-[#1E293B] border border-green-500/20 rounded-2xl p-6 transition-all duration-300 hover:shadow-[0_0_15px_rgba(34,197,94,0.2)] hover:-translate-y-1">
<div class="flex items-center space-x-4">
<div class="w-12 h-12 rounded-xl bg-green-500/20 flex items-center justify-center group-hover:scale-110 transition-transform">
<i class="fas fa-book text-xl text-green-400"></i>
</div>
<div>
<h4 class="font-semibold text-[#E5E7EB] group-hover:text-green-400 transition-colors">Docs</h4>
<p class="text-sm text-[#94A3B8]">Documentation</p>
</div>
</div>
</a>
<a href="https://localai.io/api-reference/" target="_blank" class="group bg-[#1E293B] border border-orange-500/20 rounded-2xl p-6 transition-all duration-300 hover:shadow-[0_0_15px_rgba(234,88,12,0.2)] hover:-translate-y-1">
<div class="flex items-center space-x-4">
<div class="w-12 h-12 rounded-xl bg-orange-500/20 flex items-center justify-center group-hover:scale-110 transition-transform">
<i class="fas fa-code text-xl text-orange-400"></i>
</div>
<div>
<h4 class="font-semibold text-[#E5E7EB] group-hover:text-orange-400 transition-colors">API</h4>
<p class="text-sm text-[#94A3B8]">Reference</p>
</div>
</div>
</a>
</div>
{{ end }}
</div>
{{template "views/partials/footer" .}}
</div>
<!-- Chart.js -->
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
<script>
function dashboardMetrics() {
return {
totalRequests: 0,
successRate: 0,
hasEndpointData: false,
hasModelData: false,
hasTimelineData: false,
endpointChart: null,
modelChart: null,
timelineChart: null,
init() {
// Only fetch metrics if we have models installed
{{ if gt (len .ModelsConfig) 0 }}
this.fetchMetrics();
// Auto-refresh every 30 seconds
setInterval(() => this.fetchMetrics(), 30000);
{{ end }}
},
async fetchMetrics() {
try {
const response = await fetch('/api/metrics/summary');
const data = await response.json();
this.totalRequests = data.totalRequests || 0;
this.successRate = data.successRate || 0;
// Update charts
this.updateEndpointChart(data.topEndpoints || []);
this.updateModelChart(data.topModels || []);
await this.updateTimelineChart();
} catch (error) {
console.error('Error fetching metrics:', error);
}
},
updateEndpointChart(endpoints) {
this.hasEndpointData = endpoints.length > 0;
if (!this.hasEndpointData) return;
const labels = endpoints.map(e => e.name);
const data = endpoints.map(e => e.count);
const colors = ['#38BDF8', '#8B5CF6', '#10B981', '#F59E0B', '#EF4444'];
const ctx = document.getElementById('endpointChart');
if (!ctx) return;
if (this.endpointChart) {
this.endpointChart.destroy();
}
this.endpointChart = new Chart(ctx, {
type: 'doughnut',
data: {
labels: labels,
datasets: [{
data: data,
backgroundColor: colors,
borderColor: '#1E293B',
borderWidth: 2
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'bottom',
labels: {
color: '#E5E7EB',
padding: 15,
font: {
size: 12
}
}
},
tooltip: {
backgroundColor: '#1E293B',
titleColor: '#E5E7EB',
bodyColor: '#94A3B8',
borderColor: '#38BDF8',
borderWidth: 1
}
}
}
});
},
updateModelChart(models) {
this.hasModelData = models.length > 0;
if (!this.hasModelData) return;
const labels = models.map(m => m.name);
const data = models.map(m => m.count);
const ctx = document.getElementById('modelChart');
if (!ctx) return;
if (this.modelChart) {
this.modelChart.destroy();
}
this.modelChart = new Chart(ctx, {
type: 'bar',
data: {
labels: labels,
datasets: [{
label: 'Requests',
data: data,
backgroundColor: '#8B5CF6',
borderColor: '#8B5CF6',
borderWidth: 1
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true,
ticks: {
color: '#94A3B8'
},
grid: {
color: '#1E293B'
}
},
x: {
ticks: {
color: '#94A3B8'
},
grid: {
display: false
}
}
},
plugins: {
legend: {
display: false
},
tooltip: {
backgroundColor: '#1E293B',
titleColor: '#E5E7EB',
bodyColor: '#94A3B8',
borderColor: '#8B5CF6',
borderWidth: 1
}
}
}
});
},
async updateTimelineChart() {
try {
const response = await fetch('/api/metrics/timeseries?hours=24');
const data = await response.json();
const timeseries = data.timeseries || [];
this.hasTimelineData = timeseries.length > 0;
if (!this.hasTimelineData) return;
const labels = timeseries.map(t => {
const date = new Date(t.timestamp);
return date.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' });
});
const counts = timeseries.map(t => t.count);
const ctx = document.getElementById('timelineChart');
if (!ctx) return;
if (this.timelineChart) {
this.timelineChart.destroy();
}
this.timelineChart = new Chart(ctx, {
type: 'line',
data: {
labels: labels,
datasets: [{
label: 'Requests',
data: counts,
borderColor: '#10B981',
backgroundColor: 'rgba(16, 185, 129, 0.1)',
fill: true,
tension: 0.4,
borderWidth: 2
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true,
ticks: {
color: '#94A3B8'
},
grid: {
color: '#1E293B'
}
},
x: {
ticks: {
color: '#94A3B8',
maxRotation: 45,
minRotation: 45
},
grid: {
display: false
}
}
},
plugins: {
legend: {
display: false
},
tooltip: {
backgroundColor: '#1E293B',
titleColor: '#E5E7EB',
bodyColor: '#94A3B8',
borderColor: '#10B981',
borderWidth: 1
}
}
}
});
} catch (error) {
console.error('Error fetching timeline:', error);
}
}
}
}
</script>
</body>
</html>