SPB Git forge

spb/polyllm

Public
15commits 1branches 0releases
2.2 MBsize
maindefault branch
13 days agolast push
TypeScript 97.4% SQL 1% JavaScript 0.9% CSS 0.6%
4.5 KB · 100 lines tsx
Raw Blame History
1"use client";2import * as React from "react";3import { Plus, Trophy, X } from "lucide-react";4import { PromptDialog } from "@/components/common/prompt-dialog";5import { Tooltip } from "@/components/ui/tooltip";6import { useLocalStorage } from "@/lib/client/hooks";7import { BUILTIN_CRITERIA, customCriterion, type Criterion } from "@/lib/arena/scoring";8import { cn } from "@/lib/utils";910export const CRITERIA_STORAGE_KEY = "polyllm:arena-criteria";1112/** Custom criteria persisted in localStorage (`polyllm:arena-criteria`). */13export function useCustomCriteria() {14  const [stored, setStored] = useLocalStorage<{ id: string; label: string }[]>(CRITERIA_STORAGE_KEY, []);15  const custom = React.useMemo<Criterion[]>(() => stored.map((c) => customCriterion(c.label) ?? { id: c.id, label: c.label, short: c.label, ratingKey: "bestCustom", custom: true }).filter((c) => c.id), [stored]);16  const add = React.useCallback(17    (label: string): Criterion | null => {18      const c = customCriterion(label);19      if (!c) return null;20      setStored((prev) => (prev.some((p) => p.id === c.id) ? prev : [...prev, { id: c.id, label: c.label }].slice(0, 12)));21      return c;22    },23    [setStored],24  );25  const remove = React.useCallback((id: string) => setStored((prev) => prev.filter((p) => p.id !== id)), [setStored]);26  const all = React.useMemo(() => [...BUILTIN_CRITERIA, ...custom], [custom]);27  return { custom, all, add, remove };28}2930interface Props {31  criteria: Criterion[];32  /** Criterion ids won by this response. */33  won: ReadonlySet<string>;34  onToggle: (criterionId: string, on: boolean) => void;35  onAddCriterion?: (label: string) => void;36  onRemoveCriterion?: (id: string) => void;37  busy?: boolean;38  disabled?: boolean;39  className?: string;40}4142/** Criteria chips under a response. One vote per criterion per session — voting here moves the vote. */43export function VotePanel({ criteria, won, onToggle, onAddCriterion, onRemoveCriterion, busy, disabled, className }: Props) {44  const [adding, setAdding] = React.useState(false);45  return (46    <div className={cn("flex flex-wrap items-center gap-1", className)} role="group" aria-label="Vote for this response">47      {criteria.map((c) => {48        const on = won.has(c.id);49        return (50          <span key={c.id} className="group/chip relative inline-flex">51            <Tooltip content={on ? `Retract “${c.label}”` : c.label}>52              <button53                type="button"54                disabled={busy || disabled}55                aria-pressed={on}56                onClick={() => onToggle(c.id, !on)}57                className={cn(58                  "tap inline-flex h-7 items-center gap-1 rounded-md border px-2 text-[11.5px] font-medium transition-colors disabled:opacity-50",59                  on ? "border-accent bg-accent-soft text-accent" : "border-border text-fg-muted hover:border-border-strong hover:text-fg",60                  c.custom && onRemoveCriterion && "pr-5",61                )}62              >63                {c.id === "best" ? <Trophy className="size-3" /> : null}64                {c.short}65              </button>66            </Tooltip>67            {c.custom && onRemoveCriterion ? (68              <button type="button" onClick={() => onRemoveCriterion(c.id)} className="hover-reveal absolute right-0.5 top-1/2 -translate-y-1/2 rounded p-0.5 text-fg-subtle hover:text-danger" aria-label={`Remove criterion ${c.label}`}>69                <X className="size-3" />70              </button>71            ) : null}72          </span>73        );74      })}75      {onAddCriterion ? (76        <>77          <Tooltip content="Add a custom criterion">78            <button type="button" disabled={disabled} onClick={() => setAdding(true)} className="tap inline-flex h-7 items-center gap-1 rounded-md border border-dashed border-border px-2 text-[11.5px] font-medium text-fg-subtle transition-colors hover:border-border-strong hover:text-fg" aria-label="Add criterion">79              <Plus className="size-3" /> Criterion80            </button>81          </Tooltip>82          <PromptDialog83            open={adding}84            onOpenChange={setAdding}85            title="New criterion"86            description="Name what you are judging — e.g. “Best tone”, “Most concise”, “Follows format”. Saved on this device."87            label="Criterion"88            placeholder="Most concise"89            confirmLabel="Add"90            maxLength={40}91            onSubmit={(v) => {92              onAddCriterion(v);93            }}94          />95        </>96      ) : null}97    </div>98  );99}100