enh: analytics

This commit is contained in:
Timothy Jaeryang Baek
2026-02-05 00:00:49 -06:00
parent 0e60c757ce
commit e62649f940
4 changed files with 95 additions and 28 deletions
+15 -5
View File
@@ -3,13 +3,15 @@ import { WEBUI_API_BASE_URL } from '$lib/constants';
export const getModelAnalytics = async (
token: string = '',
startDate: number | null = null,
endDate: number | null = null
endDate: number | null = null,
groupId: string | null = null
) => {
let error = null;
const searchParams = new URLSearchParams();
if (startDate) searchParams.append('start_date', startDate.toString());
if (endDate) searchParams.append('end_date', endDate.toString());
if (groupId) searchParams.append('group_id', groupId);
const res = await fetch(`${WEBUI_API_BASE_URL}/analytics/models?${searchParams.toString()}`, {
method: 'GET',
@@ -40,7 +42,8 @@ export const getUserAnalytics = async (
token: string = '',
startDate: number | null = null,
endDate: number | null = null,
limit: number = 50
limit: number = 50,
groupId: string | null = null
) => {
let error = null;
@@ -48,6 +51,7 @@ export const getUserAnalytics = async (
if (startDate) searchParams.append('start_date', startDate.toString());
if (endDate) searchParams.append('end_date', endDate.toString());
if (limit) searchParams.append('limit', limit.toString());
if (groupId) searchParams.append('group_id', groupId);
const res = await fetch(`${WEBUI_API_BASE_URL}/analytics/users?${searchParams.toString()}`, {
method: 'GET',
@@ -123,13 +127,15 @@ export const getMessages = async (
export const getSummary = async (
token: string = '',
startDate: number | null = null,
endDate: number | null = null
endDate: number | null = null,
groupId: string | null = null
) => {
let error = null;
const searchParams = new URLSearchParams();
if (startDate) searchParams.append('start_date', startDate.toString());
if (endDate) searchParams.append('end_date', endDate.toString());
if (groupId) searchParams.append('group_id', groupId);
const res = await fetch(`${WEBUI_API_BASE_URL}/analytics/summary?${searchParams.toString()}`, {
method: 'GET',
@@ -160,7 +166,8 @@ export const getDailyStats = async (
token: string = '',
startDate: number | null = null,
endDate: number | null = null,
granularity: 'hourly' | 'daily' = 'daily'
granularity: 'hourly' | 'daily' = 'daily',
groupId: string | null = null
) => {
let error = null;
@@ -168,6 +175,7 @@ export const getDailyStats = async (
if (startDate) searchParams.append('start_date', startDate.toString());
if (endDate) searchParams.append('end_date', endDate.toString());
searchParams.append('granularity', granularity);
if (groupId) searchParams.append('group_id', groupId);
const res = await fetch(`${WEBUI_API_BASE_URL}/analytics/daily?${searchParams.toString()}`, {
method: 'GET',
@@ -197,13 +205,15 @@ export const getDailyStats = async (
export const getTokenUsage = async (
token: string = '',
startDate: number | null = null,
endDate: number | null = null
endDate: number | null = null,
groupId: string | null = null
) => {
let error = null;
const searchParams = new URLSearchParams();
if (startDate) searchParams.append('start_date', startDate.toString());
if (endDate) searchParams.append('end_date', endDate.toString());
if (groupId) searchParams.append('group_id', groupId);
const res = await fetch(`${WEBUI_API_BASE_URL}/analytics/tokens?${searchParams.toString()}`, {
method: 'GET',
@@ -2,6 +2,7 @@
import { onMount, getContext } from 'svelte';
import { models } from '$lib/stores';
import { getSummary, getModelAnalytics, getUserAnalytics, getDailyStats, getTokenUsage } from '$lib/apis/analytics';
import { getGroups } from '$lib/apis/groups';
import Spinner from '$lib/components/common/Spinner.svelte';
import ChevronUp from '$lib/components/icons/ChevronUp.svelte';
import ChevronDown from '$lib/components/icons/ChevronDown.svelte';
@@ -24,6 +25,10 @@
{ value: 'all', label: 'All time' }
];
// User group filter
let groups: Array<{ id: string; name: string }> = [];
let selectedGroupId: string | null = null;
const getDateRange = (period: string): { start: number | null; end: number | null } => {
const now = Math.floor(Date.now() / 1000);
const day = 86400;
@@ -80,11 +85,11 @@
const { start, end } = getDateRange(selectedPeriod);
const granularity = selectedPeriod === '24h' ? 'hourly' : 'daily';
const [summaryRes, modelsRes, usersRes, dailyRes, tokensRes] = await Promise.all([
getSummary(localStorage.token, start, end),
getModelAnalytics(localStorage.token, start, end),
getUserAnalytics(localStorage.token, start, end, 50),
getDailyStats(localStorage.token, start, end, granularity),
getTokenUsage(localStorage.token, start, end)
getSummary(localStorage.token, start, end, selectedGroupId),
getModelAnalytics(localStorage.token, start, end, selectedGroupId),
getUserAnalytics(localStorage.token, start, end, 50, selectedGroupId),
getDailyStats(localStorage.token, start, end, granularity, selectedGroupId),
getTokenUsage(localStorage.token, start, end, selectedGroupId)
]);
summary = summaryRes ?? summary;
@@ -120,10 +125,20 @@
loading = false;
};
$: if (selectedPeriod) {
$: if (selectedPeriod || selectedGroupId !== undefined) {
loadDashboard();
}
onMount(async () => {
// Load groups for filter
try {
const res = await getGroups(localStorage.token);
groups = res ?? [];
} catch (e) {
console.error('Failed to load groups:', e);
}
});
$: sortedModels = [...modelStats].sort((a, b) => {
if (modelOrderBy === 'name') {
return modelDirection === 'asc'
@@ -159,14 +174,27 @@
<div class="text-lg font-medium px-0.5">
{$i18n.t('Analytics')}
</div>
<select
bind:value={selectedPeriod}
class="dark:bg-gray-900 w-fit pr-8 rounded-sm px-2 text-xs bg-transparent outline-none text-right"
>
{#each periods as period}
<option value={period.value}>{$i18n.t(period.label)}</option>
{/each}
</select>
<div class="flex items-center gap-2">
{#if groups.length > 0}
<select
bind:value={selectedGroupId}
class="dark:bg-gray-900 w-fit pr-8 rounded-sm px-2 text-xs bg-transparent outline-none text-right"
>
<option value={null}>{$i18n.t('All Users')}</option>
{#each groups as group}
<option value={group.id}>{group.name}</option>
{/each}
</select>
{/if}
<select
bind:value={selectedPeriod}
class="dark:bg-gray-900 w-fit pr-8 rounded-sm px-2 text-xs bg-transparent outline-none text-right"
>
{#each periods as period}
<option value={period.value}>{$i18n.t(period.label)}</option>
{/each}
</select>
</div>
</div>
<!-- Model Details Modal -->