File size: 4,037 Bytes
e355be5
 
 
 
 
6d55c38
e355be5
 
 
6d55c38
 
 
 
 
 
 
e355be5
6d55c38
e355be5
 
 
 
 
 
 
 
 
 
6d55c38
 
 
 
e355be5
6d55c38
 
 
 
 
 
 
e355be5
 
6d55c38
 
 
 
 
 
 
e355be5
6d55c38
e355be5
 
 
 
 
 
6d55c38
 
 
 
 
 
e355be5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6d55c38
 
e355be5
 
 
 
 
 
 
 
 
6d55c38
 
 
e355be5
6d55c38
e355be5
 
 
6d55c38
e355be5
 
6d55c38
 
 
 
e355be5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6d55c38
 
 
 
 
 
 
 
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
<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++}
/>