123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329 |
- <script lang="ts">
- import { onMount, getContext } from 'svelte';
- import dayjs from 'dayjs';
- import relativeTime from 'dayjs/plugin/relativeTime';
- dayjs.extend(relativeTime);
- import { models } from '$lib/stores';
- import { getAllFeedbacks } from '$lib/apis/evaluations';
- import FeedbackMenu from './Evaluations/FeedbackMenu.svelte';
- import EllipsisHorizontal from '../icons/EllipsisHorizontal.svelte';
- import Tooltip from '../common/Tooltip.svelte';
- import Badge from '../common/Badge.svelte';
- const i18n = getContext('i18n');
- let rankedModels = [];
- let feedbacks = [];
- type Feedback = {
- model_id: string;
- sibling_model_ids?: string[];
- rating: number;
- };
- type ModelStats = {
- rating: number;
- won: number;
- draw: number;
- lost: number;
- };
- function calculateModelStats(feedbacks: Feedback[]): Map<string, ModelStats> {
- const stats = new Map<string, ModelStats>();
- const K = 32;
- function getOrDefaultStats(modelId: string): ModelStats {
- return stats.get(modelId) || { rating: 1000, won: 0, draw: 0, lost: 0 };
- }
- function updateStats(modelId: string, ratingChange: number, outcome: number) {
- const currentStats = getOrDefaultStats(modelId);
- currentStats.rating += ratingChange;
- if (outcome === 1) currentStats.won++;
- else if (outcome === 0.5) currentStats.draw++;
- else if (outcome === 0) currentStats.lost++;
- stats.set(modelId, currentStats);
- }
- function calculateEloChange(ratingA: number, ratingB: number, outcome: number): number {
- const expectedScore = 1 / (1 + Math.pow(10, (ratingB - ratingA) / 400));
- return K * (outcome - expectedScore);
- }
- feedbacks.forEach((feedback) => {
- const modelA = feedback.data.model_id;
- const statsA = getOrDefaultStats(modelA);
- let outcome: number;
- switch (feedback.data.rating.toString()) {
- case '1':
- outcome = 1;
- break;
- case '0':
- outcome = 0.5;
- break;
- case '-1':
- outcome = 0;
- break;
- default:
- return; // Skip invalid ratings
- }
- const opponents = feedback.data.sibling_model_ids || [];
- opponents.forEach((modelB) => {
- const statsB = getOrDefaultStats(modelB);
- const changeA = calculateEloChange(statsA.rating, statsB.rating, outcome);
- const changeB = calculateEloChange(statsB.rating, statsA.rating, 1 - outcome);
- updateStats(modelA, changeA, outcome);
- updateStats(modelB, changeB, 1 - outcome);
- });
- });
- return stats;
- }
- let loaded = false;
- onMount(async () => {
- feedbacks = await getAllFeedbacks(localStorage.token);
- const modelStats = calculateModelStats(feedbacks);
- rankedModels = $models
- .filter((m) => m?.owned_by !== 'arena' && (m?.info?.meta?.hidden ?? false) !== true)
- .map((model) => {
- const stats = modelStats.get(model.name);
- return {
- ...model,
- rating: stats ? Math.round(stats.rating) : '-',
- stats: {
- won: stats ? stats.won.toString() : '-',
- draw: stats ? stats.draw.toString() : '-',
- lost: stats ? stats.lost.toString() : '-'
- }
- };
- })
- .sort((a, b) => {
- // Handle sorting by rating ('-' goes to the end)
- if (a.rating === '-' && b.rating !== '-') return 1;
- if (b.rating === '-' && a.rating !== '-') return -1;
- // If both have ratings (non '-'), sort by rating numerically (descending)
- if (a.rating !== '-' && b.rating !== '-') return b.rating - a.rating;
- // If both ratings are '-', sort alphabetically (by 'name')
- return a.name.localeCompare(b.name);
- });
- loaded = true;
- });
- </script>
- {#if loaded}
- <div class="mt-0.5 mb-2 gap-1 flex flex-col md:flex-row justify-between">
- <div class="flex md:self-center text-lg font-medium px-0.5">
- {$i18n.t('Leaderboard')}
- <div class="flex self-center w-[1px] h-6 mx-2.5 bg-gray-50 dark:bg-gray-850" />
- <span class="text-lg font-medium text-gray-500 dark:text-gray-300">{rankedModels.length}</span
- >
- </div>
- </div>
- <div
- class="scrollbar-hidden relative whitespace-nowrap overflow-x-auto max-w-full rounded pt-0.5"
- >
- {#if (rankedModels ?? []).length === 0}
- <div class="text-center text-xs text-gray-500 dark:text-gray-400 py-1">
- {$i18n.t('No models found')}
- </div>
- {:else}
- <table
- class="w-full text-sm text-left text-gray-500 dark:text-gray-400 table-auto max-w-full rounded"
- >
- <thead
- class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-850 dark:text-gray-400 -translate-y-0.5"
- >
- <tr class="">
- <th scope="col" class="px-3 py-1.5 cursor-pointer select-none w-3">
- {$i18n.t('RK')}
- </th>
- <th scope="col" class="px-3 py-1.5 cursor-pointer select-none">
- {$i18n.t('Model')}
- </th>
- <th scope="col" class="px-3 py-1.5 text-right cursor-pointer select-none w-fit">
- {$i18n.t('Rating')}
- </th>
- <th scope="col" class="px-3 py-1.5 text-right cursor-pointer select-none w-fit">
- {$i18n.t('Won')}
- </th>
- <th scope="col" class="px-3 py-1.5 text-right cursor-pointer select-none w-fit">
- {$i18n.t('Draw')}
- </th>
- <th scope="col" class="px-3 py-1.5 text-right cursor-pointer select-none w-fit">
- {$i18n.t('Lost')}
- </th>
- </tr>
- </thead>
- <tbody class="">
- {#each rankedModels as model, modelIdx (model.id)}
- <tr class="bg-white dark:bg-gray-900 dark:border-gray-850 text-xs">
- <td class="px-3 py-1.5 text-left font-medium text-gray-900 dark:text-white w-fit">
- <div class=" line-clamp-1">
- {model?.rating !== '-' ? modelIdx + 1 : '-'}
- </div>
- </td>
- <td class="px-3 py-1.5 flex flex-col justify-center">
- <div class="flex items-center gap-2">
- <div class="flex-shrink-0">
- <img
- src={model?.info?.meta?.profile_image_url ?? '/favicon.png'}
- alt={model.name}
- class="size-5 rounded-full object-cover shrink-0"
- />
- </div>
- <div class="font-medium text-gray-800 dark:text-gray-200 pr-4">
- {model.name}
- </div>
- </div>
- </td>
- <td class="px-3 py-1.5 text-right font-medium text-gray-900 dark:text-white w-max">
- {model.rating}
- </td>
- <td class=" px-3 py-1.5 text-right font-semibold text-green-500">
- {model.stats.won}
- </td>
- <td class=" px-3 py-1.5 text-right font-semibold">
- {model.stats.draw}
- </td>
- <td class="px-3 py-1.5 text-right font-semibold text-red-500">
- {model.stats.lost}
- </td>
- </tr>
- {/each}
- </tbody>
- </table>
- {/if}
- </div>
- <div class="pb-4"></div>
- <div class="mt-0.5 mb-2 gap-1 flex flex-col md:flex-row justify-between">
- <div class="flex md:self-center text-lg font-medium px-0.5">
- {$i18n.t('Feedback History')}
- </div>
- </div>
- <div
- class="scrollbar-hidden relative whitespace-nowrap overflow-x-auto max-w-full rounded pt-0.5"
- >
- {#if (feedbacks ?? []).length === 0}
- <div class="text-center text-xs text-gray-500 dark:text-gray-400 py-1">
- {$i18n.t('No feedbacks found')}
- </div>
- {:else}
- <table
- class="w-full text-sm text-left text-gray-500 dark:text-gray-400 table-auto max-w-full rounded"
- >
- <thead
- class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-850 dark:text-gray-400 -translate-y-0.5"
- >
- <tr class="">
- <th scope="col" class="px-3 py-1.5 text-right cursor-pointer select-none w-0">
- {$i18n.t('User')}
- </th>
- <th scope="col" class="px-3 py-1.5 cursor-pointer select-none">
- {$i18n.t('Models')}
- </th>
- <th scope="col" class="px-3 py-1.5 text-right cursor-pointer select-none w-fit">
- {$i18n.t('Result')}
- </th>
- <th scope="col" class="px-3 py-1.5 text-right cursor-pointer select-none w-0">
- {$i18n.t('Updated At')}
- </th>
- <th scope="col" class="px-3 py-1.5 text-right cursor-pointer select-none w-0"> </th>
- </tr>
- </thead>
- <tbody class="">
- {#each feedbacks as feedback (feedback.id)}
- <tr class="bg-white dark:bg-gray-900 dark:border-gray-850 text-xs">
- <td class=" py-1 text-right font-semibold">
- <div class="flex justify-center">
- <Tooltip content={feedback?.user?.name}>
- <div class="flex-shrink-0">
- <img
- src={feedback?.user?.profile_image_url ?? '/user.png'}
- alt={feedback?.user?.name}
- class="size-6 rounded-full object-cover shrink-0"
- />
- </div>
- </Tooltip>
- </div>
- </td>
- <td class="px-3 py-1 flex flex-col">
- <div class="flex flex-col items-start gap-0.5 h-full">
- <div class="flex flex-col h-full">
- {#if feedback.data?.sibling_model_ids}
- <div class="font-semibold text-gray-600 dark:text-gray-400 flex-1">
- {feedback.data?.model_id}
- </div>
- <div class=" text-[0.65rem] text-gray-600 dark:text-gray-400 line-clamp-1">
- {feedback.data.sibling_model_ids.join(', ')}
- </div>
- {:else}
- <div
- class=" text-sm font-medium text-gray-600 dark:text-gray-400 flex-1 py-2"
- >
- {feedback.data?.model_id}
- </div>
- {/if}
- </div>
- </div>
- </td>
- <td class="px-3 py-1 text-right font-medium text-gray-900 dark:text-white w-max">
- <div class=" flex justify-end">
- {#if feedback.data.rating.toString() === '1'}
- <Badge type="info" content={$i18n.t('Won')} />
- {:else if feedback.data.rating.toString() === '0'}
- <Badge type="muted" content={$i18n.t('Draw')} />
- {:else if feedback.data.rating.toString() === '-1'}
- <Badge type="error" content={$i18n.t('Lost')} />
- {/if}
- </div>
- </td>
- <td class=" px-3 py-1 text-right font-medium">
- {dayjs(feedback.updated_at * 1000).fromNow()}
- </td>
- <td class=" px-3 py-1 text-right font-semibold">
- <FeedbackMenu>
- <button
- class="self-center w-fit text-sm p-1.5 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
- >
- <EllipsisHorizontal />
- </button>
- </FeedbackMenu>
- </td>
- </tr>
- {/each}
- </tbody>
- </table>
- {/if}
- </div>
- <div class="pb-8"></div>
- {/if}
|