refac
This commit is contained in:
@@ -31,7 +31,7 @@
|
||||
import ChevronUp from '$lib/components/icons/ChevronUp.svelte';
|
||||
import ChevronDown from '$lib/components/icons/ChevronDown.svelte';
|
||||
import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants';
|
||||
import { config } from '$lib/stores';
|
||||
import { adminFeedbackCount, config } from '$lib/stores';
|
||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||
import Select from '$lib/components/common/Select.svelte';
|
||||
import Check from '$lib/components/icons/Check.svelte';
|
||||
@@ -90,6 +90,7 @@
|
||||
if (res) {
|
||||
items = res.items;
|
||||
total = res.total;
|
||||
adminFeedbackCount.set(total);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
@@ -216,27 +217,61 @@
|
||||
<Spinner className="size-5" />
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex flex-col gap-1 mt-0.5 mb-3">
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="flex items-center md:self-center text-xl font-normal px-0.5 gap-2 shrink-0">
|
||||
<div>
|
||||
{$i18n.t('Feedback History')}
|
||||
<div class="space-y-1">
|
||||
{#if modelIds.length > 0 || total > 0}
|
||||
<div class="flex h-8 flex-1 items-center w-full gap-2">
|
||||
<div
|
||||
class="flex min-w-0 flex-1 bg-transparent overflow-x-auto scrollbar-none"
|
||||
on:wheel={(e) => {
|
||||
if (e.deltaY !== 0) {
|
||||
e.preventDefault();
|
||||
e.currentTarget.scrollLeft += e.deltaY;
|
||||
}
|
||||
}}
|
||||
>
|
||||
{#if modelIds.length > 0}
|
||||
<div
|
||||
class="flex gap-0.5 w-fit text-center text-sm rounded-full bg-transparent whitespace-nowrap"
|
||||
>
|
||||
<Select
|
||||
bind:value={selectedModelId}
|
||||
items={[
|
||||
{ value: '', label: $i18n.t('All') },
|
||||
...modelIds.map((mid) => ({ value: mid, label: mid }))
|
||||
]}
|
||||
placeholder={$i18n.t('All')}
|
||||
triggerClass="relative w-full flex items-center gap-0.5 px-2.5 py-1.5 bg-transparent rounded-xl text-[13px] font-normal text-gray-700 transition hover:text-gray-900 dark:text-gray-200 dark:hover:text-gray-100"
|
||||
onChange={() => {
|
||||
page = 1;
|
||||
getFeedbacks();
|
||||
}}
|
||||
>
|
||||
<svelte:fragment slot="trigger" let:selectedLabel>
|
||||
<span
|
||||
class="inline-flex h-input px-0.5 w-full outline-hidden bg-transparent truncate placeholder-gray-400 focus:outline-hidden"
|
||||
>
|
||||
{selectedLabel}
|
||||
</span>
|
||||
<ChevronDown className="size-3.5" strokeWidth="2.5" />
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="item" let:item let:selected>
|
||||
{item.label}
|
||||
<div class="ml-auto {selected ? '' : 'invisible'}">
|
||||
<Check />
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</Select>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="text-lg font-normal text-gray-500 dark:text-gray-500">
|
||||
{total}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex w-full justify-end gap-1.5">
|
||||
{#if total > 0}
|
||||
<Dropdown align="end">
|
||||
<button
|
||||
class="flex text-xs items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-200 transition"
|
||||
class="flex h-8 shrink-0 items-center gap-1 px-2 py-1.5 rounded-xl hover:bg-gray-100 dark:hover:bg-gray-850 dark:text-gray-200 transition text-xs"
|
||||
>
|
||||
<div class="self-center font-normal line-clamp-1">
|
||||
{$i18n.t('Export')}
|
||||
</div>
|
||||
{$i18n.t('Export')}
|
||||
<ChevronDown className="size-3" strokeWidth="2.5" />
|
||||
</button>
|
||||
|
||||
@@ -262,59 +297,9 @@
|
||||
</Dropdown>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="py-2 bg-white dark:bg-gray-900 rounded-3xl border border-gray-100/30 dark:border-gray-850/30"
|
||||
>
|
||||
{#if modelIds.length > 0}
|
||||
<div
|
||||
class="px-2.5 flex w-full bg-transparent overflow-x-auto scrollbar-none mb-1"
|
||||
on:wheel={(e) => {
|
||||
if (e.deltaY !== 0) {
|
||||
e.preventDefault();
|
||||
e.currentTarget.scrollLeft += e.deltaY;
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div
|
||||
class="flex gap-0.5 w-fit text-center text-sm rounded-full bg-transparent whitespace-nowrap"
|
||||
>
|
||||
<Select
|
||||
bind:value={selectedModelId}
|
||||
items={[
|
||||
{ value: '', label: $i18n.t('All') },
|
||||
...modelIds.map((mid) => ({ value: mid, label: mid }))
|
||||
]}
|
||||
placeholder={$i18n.t('All')}
|
||||
triggerClass="relative w-full flex items-center gap-0.5 px-2.5 py-1.5 bg-gray-50 dark:bg-gray-850 rounded-xl"
|
||||
onChange={() => {
|
||||
page = 1;
|
||||
getFeedbacks();
|
||||
}}
|
||||
>
|
||||
<svelte:fragment slot="trigger" let:selectedLabel>
|
||||
<span
|
||||
class="inline-flex h-input px-0.5 w-full outline-hidden bg-transparent truncate placeholder-gray-400 focus:outline-hidden"
|
||||
>
|
||||
{selectedLabel}
|
||||
</span>
|
||||
<ChevronDown className="size-3.5" strokeWidth="2.5" />
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="item" let:item let:selected>
|
||||
{item.label}
|
||||
<div class="ml-auto {selected ? '' : 'invisible'}">
|
||||
<Check />
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="scrollbar-hidden relative whitespace-nowrap overflow-x-auto max-w-full px-2">
|
||||
<div class="scrollbar-hidden relative whitespace-nowrap overflow-x-auto max-w-full">
|
||||
{#if (items ?? []).length === 0}
|
||||
<div class="w-full h-full flex flex-col justify-center items-center my-16 mb-24">
|
||||
<div class="max-w-md text-center">
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { onMount, getContext } from 'svelte';
|
||||
import { models } from '$lib/stores';
|
||||
import { getContext } from 'svelte';
|
||||
import { adminLeaderboardCount, 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 XMark from '$lib/components/icons/XMark.svelte';
|
||||
import ChevronUp from '$lib/components/icons/ChevronUp.svelte';
|
||||
import ChevronDown from '$lib/components/icons/ChevronDown.svelte';
|
||||
import { WEBUI_API_BASE_URL } from '$lib/constants';
|
||||
@@ -83,6 +84,7 @@
|
||||
if (b.rating === '-') return -1;
|
||||
return b.rating - a.rating;
|
||||
});
|
||||
adminLeaderboardCount.set(rankedModels.length);
|
||||
} catch (err) {
|
||||
console.error('Leaderboard load failed:', err);
|
||||
}
|
||||
@@ -120,118 +122,133 @@
|
||||
|
||||
<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-normal 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="space-y-1">
|
||||
<div class="pt-0.5 pb-1 sticky top-0 z-10 bg-white dark:bg-gray-900">
|
||||
<div class="flex h-8 flex-1 items-center w-full gap-2">
|
||||
<div class="flex min-w-0 flex-1 items-center">
|
||||
<div class="self-center ml-1 mr-3">
|
||||
<Search className="size-3.5" />
|
||||
</div>
|
||||
<input
|
||||
class="w-full text-sm pr-4 py-1 rounded-r-xl outline-hidden bg-transparent"
|
||||
bind:value={query}
|
||||
aria-label={$i18n.t('Search')}
|
||||
placeholder={$i18n.t('Search')}
|
||||
/>
|
||||
|
||||
<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)}
|
||||
{#if query}
|
||||
<div class="self-center pl-1.5 translate-y-[0.5px] rounded-l-xl bg-transparent">
|
||||
<button
|
||||
class="p-0.5 rounded-full hover:bg-gray-100 dark:hover:bg-gray-900 transition"
|
||||
aria-label={$i18n.t('Clear search')}
|
||||
on:click={() => {
|
||||
query = '';
|
||||
}}
|
||||
>
|
||||
<div
|
||||
class="flex gap-1.5 items-center {col.class.includes('right') ? 'justify-end' : ''}"
|
||||
<XMark className="size-3" strokeWidth="2" />
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</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 !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)}
|
||||
>
|
||||
{$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-normal 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="{WEBUI_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-normal 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-normal text-gray-900 dark:text-white">
|
||||
{model.rating}
|
||||
</td>
|
||||
<td class="px-3 py-1.5 text-right font-normal 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
|
||||
<div
|
||||
class="flex gap-1.5 items-center {col.class.includes('right')
|
||||
? 'justify-end'
|
||||
: ''}"
|
||||
>
|
||||
<span class="group-hover:hidden">{model.stats.won}</span>
|
||||
{/if}
|
||||
</td>
|
||||
<td class="px-3 py-1.5 text-right font-normal 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>
|
||||
{$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>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
{/if}
|
||||
</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-normal 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="{WEBUI_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-normal 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-normal text-gray-900 dark:text-white">
|
||||
{model.rating}
|
||||
</td>
|
||||
<td class="px-3 py-1.5 text-right font-normal 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-normal 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>
|
||||
|
||||
<div class="text-gray-500 text-xs mt-1.5 w-full flex justify-end">
|
||||
|
||||
Reference in New Issue
Block a user