ai / frontend /src /lib /components /admin /Evaluations /Leaderboard.svelte
fdrah
initial-commit
9e93b10
Raw
History Blame Contribute Delete
7.87 kB
<script lang="ts">
import { onMount, getContext } from "svelte";
import { models } from "$lib/stores";
import { getLeaderboard } from "$lib/apis/evaluations";
import ModelModal from "./LeaderboardModal.svelte";
import Spinner from "$lib/components/common/Spinner.svelte";
import Tooltip from "$lib/components/common/Tooltip.svelte";
import Search from "$lib/components/icons/Search.svelte";
import ChevronUp from "$lib/components/icons/ChevronUp.svelte";
import ChevronDown from "$lib/components/icons/ChevronDown.svelte";
import { REXPRO_API_BASE_URL } from "$lib/constants";
const i18n = getContext("i18n");
let rankedModels = [];
let query = "";
let loading = true;
let debounceTimer: ReturnType<typeof setTimeout>;
let orderBy = "rating";
let direction: "asc" | "desc" = "desc";
let showModal = false;
let selectedModel = null;
const toggleSort = (key: string) => {
if (orderBy === key) {
direction = direction === "asc" ? "desc" : "asc";
} else {
orderBy = key;
direction = key === "name" ? "asc" : "desc";
}
};
const openModal = (model) => {
selectedModel = model;
showModal = true;
};
const closeModal = () => {
selectedModel = null;
showModal = false;
};
const loadLeaderboard = async (searchQuery = "") => {
loading = true;
try {
const result = await getLeaderboard(
localStorage.token,
searchQuery,
);
const statsMap = new Map(
(result?.entries ?? []).map((e) => [e.model_id, e]),
);
rankedModels = $models
.filter(
(m) => m?.owned_by !== "arena" && !m?.info?.meta?.hidden,
)
.map((model) => {
const s = statsMap.get(model.id);
return {
...model,
rating: s?.rating ?? "-",
stats: {
count: s ? s.won + s.lost : 0,
won: s?.won?.toString() ?? "-",
lost: s?.lost?.toString() ?? "-",
},
top_tags: s?.top_tags ?? [],
};
})
.sort((a, b) => {
if (a.rating === "-") return 1;
if (b.rating === "-") return -1;
return b.rating - a.rating;
});
} catch (err) {
console.error("Leaderboard load failed:", err);
}
loading = false;
};
const debouncedLoad = () => {
loading = true;
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => loadLeaderboard(query), 500);
};
$: if (query !== null) {
debouncedLoad();
}
$: sortedModels = [...rankedModels].sort((a, b) => {
const getValue = (m, key) => {
if (key === "name") return m.name ?? m.id ?? "";
if (key === "rating")
return m.rating === "-" ? -Infinity : m.rating;
if (key === "won" || key === "lost") {
const v = m.stats[key];
return v === "-" ? -Infinity : Number(v);
}
return 0;
};
const aVal = getValue(a, orderBy);
const bVal = getValue(b, orderBy);
if (orderBy === "name") {
return direction === "asc"
? aVal.localeCompare(bVal)
: bVal.localeCompare(aVal);
}
return direction === "asc" ? aVal - bVal : bVal - aVal;
});
</script>
<ModelModal bind:show={showModal} model={selectedModel} onClose={closeModal} />
<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("Leaderboard")}
<span class="text-lg text-gray-500">{rankedModels.length}</span>
</div>
<Tooltip content={$i18n.t("Re-rank models by topic similarity")}>
<div class="flex flex-1">
<Search className="size-3 ml-1 mr-3 self-center" />
<input
class="w-full text-sm pr-4 py-1 rounded-r-xl outline-hidden bg-transparent"
bind:value={query}
placeholder={$i18n.t("Search")}
/>
</div>
</Tooltip>
</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 !rankedModels.length && !loading}
<div class="text-center text-xs text-gray-500 py-1">
{$i18n.t("No models found")}
</div>
{:else if rankedModels.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"
>
{#each [{ key: "rating", label: "RK", class: "w-3" }, { key: "name", label: "Model", class: "" }, { key: "rating", label: "Rating", class: "text-right w-fit" }, { key: "won", label: "Won", class: "text-right w-5" }, { key: "lost", label: "Lost", class: "text-right w-5" }] as col}
<th
scope="col"
class="px-2.5 py-2 cursor-pointer select-none {col.class}"
on:click={() => toggleSort(col.key)}
>
<div
class="flex gap-1.5 items-center {col.class.includes(
'right',
)
? 'justify-end'
: ''}"
>
{$i18n.t(col.label)}
{#if orderBy === col.key}
{#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>
{/each}
</tr>
</thead>
<tbody>
{#each sortedModels as model, idx (model.id)}
<tr
class="bg-white dark:bg-gray-900 text-xs group cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-850/50 transition"
on:click={() => openModal(model)}
>
<td
class="px-3 py-1.5 font-medium text-gray-900 dark:text-white"
>
{model.rating !== "-" ? idx + 1 : "-"}
</td>
<td class="px-3 py-1.5">
<div class="flex items-center gap-2">
<img
src="{REXPRO_API_BASE_URL}/models/model/profile/image?id={model.id}"
alt={model.name}
class="size-5 rounded-full object-cover shrink-0"
on:error={(e) => {
e.target.src = "/favicon.png";
}}
/>
<Tooltip
content={`${model.name} (${model.id})`}
placement="top-start"
>
<span
class="font-medium text-gray-800 dark:text-gray-200 line-clamp-1"
>{model.name}</span
>
</Tooltip>
</div>
</td>
<td
class="px-3 py-1.5 text-right font-medium text-gray-900 dark:text-white"
>
{model.rating}
</td>
<td
class="px-3 py-1.5 text-right font-medium text-green-500 w-10"
>
{#if model.stats.won === "-"}-{:else}
<span class="hidden group-hover:inline"
>{(
(Number(model.stats.won) /
model.stats.count) *
100
).toFixed(1)}%</span
>
<span class="group-hover:hidden"
>{model.stats.won}</span
>
{/if}
</td>
<td
class="px-3 py-1.5 text-right font-medium text-red-500 w-10"
>
{#if model.stats.lost === "-"}-{:else}
<span class="hidden group-hover:inline"
>{(
(Number(model.stats.lost) /
model.stats.count) *
100
).toFixed(1)}%</span
>
<span class="group-hover:hidden"
>{model.stats.lost}</span
>
{/if}
</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">
<div class="line-clamp-1">
{$i18n.t(
"The evaluation leaderboard is based on the Elo rating system and is updated in real-time.",
)}
</div>
{$i18n.t(
"The leaderboard is currently in beta, and we may adjust the rating calculations as we refine the algorithm.",
)}
</div>
</div>