Spaces:
Build error
Build error
File size: 4,551 Bytes
d6703a1 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 | <script lang="ts">
import { onMount, getContext } from 'svelte';
import { getUserAnalytics } from '$lib/apis/analytics';
import Spinner from '$lib/components/common/Spinner.svelte';
import ChevronUp from '$lib/components/icons/ChevronUp.svelte';
import ChevronDown from '$lib/components/icons/ChevronDown.svelte';
const i18n = getContext('i18n');
let userStats: Array<{ user_id: string; count: number }> = [];
let loading = true;
let orderBy = 'count';
let direction: 'asc' | 'desc' = 'desc';
const toggleSort = (key: string) => {
if (orderBy === key) {
direction = direction === 'asc' ? 'desc' : 'asc';
} else {
orderBy = key;
direction = key === 'user_id' ? 'asc' : 'desc';
}
};
const loadAnalytics = async () => {
loading = true;
try {
const result = await getUserAnalytics(localStorage.token, null, null, 100);
userStats = result?.users ?? [];
} catch (err) {
console.error('User analytics load failed:', err);
}
loading = false;
};
$: sortedUsers = [...userStats].sort((a, b) => {
if (orderBy === 'user_id') {
return direction === 'asc'
? a.user_id.localeCompare(b.user_id)
: b.user_id.localeCompare(a.user_id);
}
return direction === 'asc' ? a.count - b.count : b.count - a.count;
});
$: totalMessages = userStats.reduce((sum, u) => sum + u.count, 0);
onMount(loadAnalytics);
</script>
<div
class="pt-0.5 pb-1 gap-1 flex flex-col md:flex-row justify-between sticky top-0 z-10 bg-white dark:bg-gray-900"
>
<div class="flex items-center text-xl font-medium px-0.5 gap-2 shrink-0">
{$i18n.t('User Activity')}
<span class="text-lg text-gray-500">{userStats.length} {$i18n.t('users')}</span>
</div>
</div>
<div
class="scrollbar-hidden relative whitespace-nowrap overflow-x-auto max-w-full rounded-sm min-h-[100px]"
>
{#if loading}
<div
class="absolute inset-0 flex items-center justify-center z-10 bg-white/50 dark:bg-gray-900/50"
>
<Spinner className="size-5" />
</div>
{/if}
{#if !userStats.length && !loading}
<div class="text-center text-xs text-gray-500 py-1">{$i18n.t('No data found')}</div>
{:else if userStats.length}
<table
class="w-full text-sm text-left text-gray-500 dark:text-gray-400 {loading
? 'opacity-20'
: ''}"
>
<thead class="text-xs text-gray-800 uppercase bg-transparent dark:text-gray-200">
<tr class="border-b-[1.5px] border-gray-50 dark:border-gray-850/30">
<th scope="col" class="px-2.5 py-2 w-8">#</th>
<th
scope="col"
class="px-2.5 py-2 cursor-pointer select-none"
on:click={() => toggleSort('user_id')}
>
<div class="flex gap-1.5 items-center">
{$i18n.t('User')}
{#if orderBy === 'user_id'}
{#if direction === 'asc'}<ChevronUp className="size-2" />{:else}<ChevronDown
className="size-2"
/>{/if}
{:else}
<span class="invisible"><ChevronUp className="size-2" /></span>
{/if}
</div>
</th>
<th
scope="col"
class="px-2.5 py-2 cursor-pointer select-none text-right"
on:click={() => toggleSort('count')}
>
<div class="flex gap-1.5 items-center justify-end">
{$i18n.t('Messages')}
{#if orderBy === 'count'}
{#if direction === 'asc'}<ChevronUp className="size-2" />{:else}<ChevronDown
className="size-2"
/>{/if}
{:else}
<span class="invisible"><ChevronUp className="size-2" /></span>
{/if}
</div>
</th>
<th scope="col" class="px-2.5 py-2 text-right w-24">{$i18n.t('Share')}</th>
</tr>
</thead>
<tbody>
{#each sortedUsers as user, idx (user.user_id)}
<tr
class="bg-white dark:bg-gray-900 text-xs hover:bg-gray-50 dark:hover:bg-gray-850/50 transition"
>
<td class="px-3 py-1.5 font-medium text-gray-900 dark:text-white">
{idx + 1}
</td>
<td class="px-3 py-1.5">
<span class="font-medium text-gray-800 dark:text-gray-200 font-mono text-xs">
{user.user_id.substring(0, 8)}...
</span>
</td>
<td class="px-3 py-1.5 text-right font-medium text-gray-900 dark:text-white">
{user.count.toLocaleString()}
</td>
<td class="px-3 py-1.5 text-right font-medium text-blue-500">
{((user.count / totalMessages) * 100).toFixed(1)}%
</td>
</tr>
{/each}
</tbody>
</table>
{/if}
</div>
<div class="text-gray-500 text-xs mt-1.5 w-full flex justify-end">
<div class="text-right">
ⓘ {$i18n.t('Showing all messages (user + assistant) per user.')}
</div>
</div>
|