blanchon's picture
Evaluation: 4-round policy, validations store, prev/next, copy/clear, build flag
6d55c38
<script lang="ts">
import { Button } from '$lib/components/ui/button';
import * as Tooltip from '$lib/components/ui/tooltip';
import FlagIcon from 'phosphor-svelte/lib/FlagIcon';
import ArrowRightIcon from 'phosphor-svelte/lib/ArrowRightIcon';
import ArrowLeftIcon from 'phosphor-svelte/lib/ArrowLeftIcon';
import XIcon from 'phosphor-svelte/lib/XIcon';
import { goto } from '$app/navigation';
import { page } from '$app/state';
import {
addValidation,
evalUrl,
nextUnreviewed,
reviewedKeySet,
type EvalCandidate
} from '$lib/eval';
import { toast } from 'svelte-sonner';
import EvalFlagDialog from './eval-flag-dialog.svelte';
interface Props {
queue: EvalCandidate[];
index: number;
matchId: number;
mapName: string;
round: number;
}
let { queue, index, matchId, mapName, round }: Props = $props();
let flagOpen = $state(false);
// Bumped after each review action so the dialog count and the Next-skip
// logic re-read localStorage without needing a reactive store.
let reviewBump = $state(0);
const reviewedCount = $derived.by(() => {
void reviewBump;
const reviewed = reviewedKeySet();
let n = 0;
for (const c of queue) if (reviewed.has(`${c.matchId}|${c.mapName}|${c.round}`)) n++;
return n;
});
function next() {
// Hitting Next without flagging implicitly validates the current candidate.
if (round && index >= 0) {
addValidation({ matchId, mapName, round });
reviewBump++;
}
const ni = nextUnreviewed(queue, index < 0 ? -1 : index, reviewedKeySet());
if (ni < 0) {
toast.success('Evaluation complete', {
description: `Reviewed ${reviewedCount + 1} / ${queue.length} candidates.`
});
return;
}
goto(evalUrl(queue[ni], ni));
}
function previous() {
if (index <= 0) return;
const pi = index - 1;
goto(evalUrl(queue[pi], pi));
}
function exitEval() {
const url = new URL(page.url);
url.searchParams.delete('eval');
url.searchParams.delete('i');
goto(url.pathname + (url.searchParams.size ? '?' + url.searchParams.toString() : ''));
}
const total = $derived(queue.length);
const positionLabel = $derived(index < 0 ? `— / ${total}` : `${index + 1} / ${total}`);
</script>
<div
class="border-b border-amber-500/30 bg-amber-500/10 dark:border-amber-500/25 dark:bg-amber-500/[0.07]"
>
<div class="mx-auto flex h-10 w-full max-w-[1600px] items-center gap-2 px-4 text-xs">
<span class="font-mono font-semibold tracking-wide text-amber-700 uppercase dark:text-amber-300"
>Eval</span
>
<span class="text-muted-foreground tabular-nums">{positionLabel}</span>
<span class="text-muted-foreground/60 tabular-nums">·</span>
<span class="text-muted-foreground tabular-nums">{reviewedCount} reviewed</span>
<div class="ml-auto flex items-center gap-1">
<Tooltip.Root>
<Tooltip.Trigger>
{#snippet child({ props })}
<Button
{...props}
variant="ghost"
size="icon-sm"
onclick={previous}
disabled={index <= 0}
aria-label="Previous candidate"
>
<ArrowLeftIcon size={14} weight="bold" />
</Button>
{/snippet}
</Tooltip.Trigger>
<Tooltip.Content side="bottom">Previous candidate</Tooltip.Content>
</Tooltip.Root>
<Button variant="outline" size="sm" onclick={() => (flagOpen = true)}>
<FlagIcon size={14} weight="fill" /> Flag
</Button>
<Button onclick={next} size="sm" disabled={total === 0}>
Next <ArrowRightIcon size={14} weight="bold" />
</Button>
<Tooltip.Root>
<Tooltip.Trigger>
{#snippet child({ props })}
<Button
{...props}
variant="ghost"
size="icon-sm"
onclick={exitEval}
aria-label="Exit evaluation"
>
<XIcon size={14} weight="bold" />
</Button>
{/snippet}
</Tooltip.Trigger>
<Tooltip.Content side="bottom">Exit evaluation</Tooltip.Content>
</Tooltip.Root>
</div>
</div>
</div>
<EvalFlagDialog
bind:open={flagOpen}
{matchId}
{mapName}
{round}
queueLength={total}
onChange={() => reviewBump++}
/>