Files
laravel-llm-gateway/laravel-app/resources/views/dashboard.blade.php
wtrinkl b1363aeab9 Initial commit: Any-LLM Gateway with Laravel Admin Interface
- Any-LLM Gateway setup with Docker Compose
- Laravel 11 admin interface with Livewire
- Dashboard with usage statistics and charts
- Gateway Users management with budget tracking
- API Keys management with revocation
- Budget templates with assignment
- Usage Logs with filtering and CSV export
- Model Pricing management with calculator
- PostgreSQL database integration
- Complete authentication system for admins
2025-11-16 12:38:05 +01:00

339 lines
17 KiB
PHP

<x-app-layout>
<x-slot name="header">
<h2 class="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight">
{{ __('Dashboard') }} - Any-LLM Gateway
</h2>
</x-slot>
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8 space-y-6">
<!-- Stats Cards -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<!-- Total Users -->
<div class="bg-white dark:bg-gray-800 overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-gray-600 dark:text-gray-400">Total Users</p>
<p class="text-3xl font-bold text-gray-900 dark:text-gray-100">
{{ number_format($stats['total_users']) }}
</p>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">
{{ $stats['active_users'] }} active, {{ $stats['blocked_users'] }} blocked
</p>
</div>
<div class="text-blue-500">
<svg class="w-12 h-12" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z" />
</svg>
</div>
</div>
</div>
</div>
<!-- Requests Today -->
<div class="bg-white dark:bg-gray-800 overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-gray-600 dark:text-gray-400">Requests Today</p>
<p class="text-3xl font-bold text-blue-600 dark:text-blue-400">
{{ number_format($stats['total_requests_today']) }}
</p>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">
{{ number_format($stats['total_requests_month']) }} this month
</p>
</div>
<div class="text-blue-500">
<svg class="w-12 h-12" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
</div>
</div>
</div>
</div>
<!-- Spend Today -->
<div class="bg-white dark:bg-gray-800 overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-gray-600 dark:text-gray-400">Spend Today</p>
<p class="text-3xl font-bold text-green-600 dark:text-green-400">
${{ number_format($stats['total_spend_today'], 2) }}
</p>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">
${{ number_format($stats['total_spend_month'], 2) }} this month
</p>
</div>
<div class="text-green-500">
<svg class="w-12 h-12" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
</div>
</div>
</div>
<!-- Tokens Today -->
<div class="bg-white dark:bg-gray-800 overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-gray-600 dark:text-gray-400">Tokens Today</p>
<p class="text-3xl font-bold text-purple-600 dark:text-purple-400">
{{ number_format($stats['total_tokens_today']) }}
</p>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">
Prompt + Completion
</p>
</div>
<div class="text-purple-500">
<svg class="w-12 h-12" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
</svg>
</div>
</div>
</div>
</div>
</div>
<!-- Usage Trend Chart -->
<div class="bg-white dark:bg-gray-800 overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6">
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">
Usage Trend (Last 30 Days)
</h3>
<canvas id="usageChart" height="80"></canvas>
</div>
</div>
<!-- Provider Stats & Top Users -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- Provider Breakdown -->
<div class="bg-white dark:bg-gray-800 overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6">
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">
Usage by Provider
</h3>
<canvas id="providerChart" height="250"></canvas>
</div>
</div>
<!-- Top Users -->
<div class="bg-white dark:bg-gray-800 overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6">
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">
Top Users by Spend
</h3>
<div class="space-y-4">
@forelse($topUsers as $user)
<div class="flex items-center justify-between border-b border-gray-200 dark:border-gray-700 pb-3">
<div class="flex-1">
<p class="font-medium text-gray-900 dark:text-gray-100">
{{ $user->alias ?? $user->user_id }}
</p>
<p class="text-sm text-gray-500 dark:text-gray-400">
{{ number_format($user->usage_logs_count ?? 0) }} requests
</p>
</div>
<div class="text-right">
<p class="font-semibold text-green-600 dark:text-green-400">
${{ number_format($user->usage_logs_sum_cost ?? 0, 2) }}
</p>
</div>
</div>
@empty
<p class="text-gray-500 dark:text-gray-400 text-center py-4">
No usage data yet
</p>
@endforelse
</div>
</div>
</div>
</div>
<!-- Model Stats -->
<div class="bg-white dark:bg-gray-800 overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6">
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">
Most Used Models
</h3>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead class="bg-gray-50 dark:bg-gray-900">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Model</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Requests</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Tokens</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Cost</th>
</tr>
</thead>
<tbody class="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
@forelse($modelStats as $model)
<tr>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900 dark:text-gray-100">
{{ $model->model }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
{{ number_format($model->count) }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
{{ number_format($model->tokens ?? 0) }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
${{ number_format($model->total_cost ?? 0, 4) }}
</td>
</tr>
@empty
<tr>
<td colspan="4" class="px-6 py-4 text-center text-gray-500 dark:text-gray-400">
No usage data yet
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
@push('scripts')
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
// Usage Trend Chart
const usageCtx = document.getElementById('usageChart').getContext('2d');
new Chart(usageCtx, {
type: 'line',
data: {
labels: @json($dailyUsage->pluck('date')),
datasets: [
{
label: 'Requests',
data: @json($dailyUsage->pluck('requests')),
borderColor: 'rgb(59, 130, 246)',
backgroundColor: 'rgba(59, 130, 246, 0.1)',
tension: 0.4,
yAxisID: 'y',
},
{
label: 'Cost ($)',
data: @json($dailyUsage->pluck('cost')),
borderColor: 'rgb(16, 185, 129)',
backgroundColor: 'rgba(16, 185, 129, 0.1)',
tension: 0.4,
yAxisID: 'y1',
}
]
},
options: {
responsive: true,
maintainAspectRatio: true,
interaction: {
mode: 'index',
intersect: false,
},
scales: {
y: {
type: 'linear',
display: true,
position: 'left',
title: {
display: true,
text: 'Requests'
},
ticks: {
color: getComputedStyle(document.documentElement).getPropertyValue('--tw-text-opacity') ? '#9CA3AF' : '#6B7280'
},
grid: {
color: 'rgba(156, 163, 175, 0.1)'
}
},
y1: {
type: 'linear',
display: true,
position: 'right',
title: {
display: true,
text: 'Cost ($)'
},
ticks: {
color: getComputedStyle(document.documentElement).getPropertyValue('--tw-text-opacity') ? '#9CA3AF' : '#6B7280'
},
grid: {
drawOnChartArea: false,
},
},
x: {
ticks: {
color: getComputedStyle(document.documentElement).getPropertyValue('--tw-text-opacity') ? '#9CA3AF' : '#6B7280'
},
grid: {
color: 'rgba(156, 163, 175, 0.1)'
}
}
},
plugins: {
legend: {
labels: {
color: getComputedStyle(document.documentElement).getPropertyValue('--tw-text-opacity') ? '#9CA3AF' : '#6B7280'
}
}
}
}
});
// Provider Breakdown Chart
const providerCtx = document.getElementById('providerChart').getContext('2d');
new Chart(providerCtx, {
type: 'doughnut',
data: {
labels: @json($providerStats->pluck('provider')),
datasets: [{
data: @json($providerStats->pluck('count')),
backgroundColor: [
'rgba(59, 130, 246, 0.8)',
'rgba(16, 185, 129, 0.8)',
'rgba(249, 115, 22, 0.8)',
'rgba(168, 85, 247, 0.8)',
'rgba(236, 72, 153, 0.8)',
'rgba(245, 158, 11, 0.8)',
],
borderWidth: 2,
borderColor: '#1f2937'
}]
},
options: {
responsive: true,
maintainAspectRatio: true,
plugins: {
legend: {
position: 'bottom',
labels: {
color: getComputedStyle(document.documentElement).getPropertyValue('--tw-text-opacity') ? '#9CA3AF' : '#6B7280',
padding: 15
}
},
tooltip: {
callbacks: {
label: function(context) {
let label = context.label || '';
if (label) {
label += ': ';
}
label += context.parsed + ' requests';
return label;
}
}
}
}
}
});
</script>
@endpush
</x-app-layout>