new layout for model rankings
This commit is contained in:
98
broadcast.ts
98
broadcast.ts
@@ -32,6 +32,7 @@ type GameState = {
|
||||
lastCompleted: RoundState | null;
|
||||
active: RoundState | null;
|
||||
scores: Record<string, number>;
|
||||
viewerScores: Record<string, number>;
|
||||
done: boolean;
|
||||
isPaused: boolean;
|
||||
generation: number;
|
||||
@@ -294,9 +295,61 @@ function drawHeader() {
|
||||
|
||||
}
|
||||
|
||||
function drawScoreboard(scores: Record<string, number>) {
|
||||
const entries = Object.entries(scores).sort((a, b) => b[1] - a[1]);
|
||||
|
||||
function drawScoreboardSection(
|
||||
entries: [string, number][],
|
||||
label: string,
|
||||
startY: number,
|
||||
entryHeight: number,
|
||||
) {
|
||||
const maxScore = entries[0]?.[1] || 1;
|
||||
|
||||
// Section label
|
||||
ctx.font = '700 13px "JetBrains Mono", monospace';
|
||||
ctx.fillStyle = "#555";
|
||||
ctx.fillText(label, WIDTH - 348, startY);
|
||||
|
||||
// Divider line under label
|
||||
ctx.fillStyle = "#1c1c1c";
|
||||
ctx.fillRect(WIDTH - 348, startY + 8, 296, 1);
|
||||
|
||||
entries.forEach(([name, score], index) => {
|
||||
const y = startY + 20 + index * entryHeight;
|
||||
const color = getColor(name);
|
||||
const pct = maxScore > 0 ? score / maxScore : 0;
|
||||
|
||||
ctx.font = '600 16px "JetBrains Mono", monospace';
|
||||
ctx.fillStyle = "#555";
|
||||
const rank = index === 0 && score > 0 ? "👑" : String(index + 1);
|
||||
ctx.fillText(rank, WIDTH - 348, y + 18);
|
||||
|
||||
ctx.font = '600 16px "Inter", sans-serif';
|
||||
ctx.fillStyle = color;
|
||||
const nameText = name.length > 18 ? `${name.slice(0, 18)}...` : name;
|
||||
|
||||
const drewLogo = drawModelLogo(name, WIDTH - 310, y + 4, 20);
|
||||
if (drewLogo) {
|
||||
ctx.fillText(nameText, WIDTH - 310 + 26, y + 18);
|
||||
} else {
|
||||
ctx.fillText(nameText, WIDTH - 310, y + 18);
|
||||
}
|
||||
|
||||
roundRect(WIDTH - 310, y + 30, 216, 3, 2, "#1c1c1c");
|
||||
if (pct > 0) {
|
||||
roundRect(WIDTH - 310, y + 30, Math.max(6, 216 * pct), 3, 2, color);
|
||||
}
|
||||
|
||||
ctx.font = '700 16px "JetBrains Mono", monospace';
|
||||
ctx.fillStyle = "#666";
|
||||
const scoreText = String(score);
|
||||
const scoreWidth = ctx.measureText(scoreText).width;
|
||||
ctx.fillText(scoreText, WIDTH - 48 - scoreWidth, y + 18);
|
||||
});
|
||||
}
|
||||
|
||||
function drawScoreboard(scores: Record<string, number>, viewerScores: Record<string, number>) {
|
||||
const modelEntries = Object.entries(scores).sort((a, b) => b[1] - a[1]) as [string, number][];
|
||||
const viewerEntries = Object.entries(viewerScores).sort((a, b) => b[1] - a[1]) as [string, number][];
|
||||
|
||||
roundRect(WIDTH - 380, 0, 380, HEIGHT, 0, "#111");
|
||||
ctx.fillStyle = "#1c1c1c";
|
||||
ctx.fillRect(WIDTH - 380, 0, 1, HEIGHT);
|
||||
@@ -305,40 +358,11 @@ function drawScoreboard(scores: Record<string, number>) {
|
||||
ctx.fillStyle = "#888";
|
||||
ctx.fillText("STANDINGS", WIDTH - 348, 76);
|
||||
|
||||
const maxScore = entries[0]?.[1] || 1;
|
||||
const entryHeight = 52;
|
||||
drawScoreboardSection(modelEntries, "AI JUDGES", 110, entryHeight);
|
||||
|
||||
entries.slice(0, 10).forEach(([name, score], index) => {
|
||||
const y = 140 + index * 68;
|
||||
const color = getColor(name);
|
||||
const pct = maxScore > 0 ? (score / maxScore) : 0;
|
||||
|
||||
ctx.font = '600 20px "JetBrains Mono", monospace';
|
||||
ctx.fillStyle = "#888";
|
||||
const rank = index === 0 && score > 0 ? "👑" : String(index + 1);
|
||||
ctx.fillText(rank, WIDTH - 348, y + 24);
|
||||
|
||||
ctx.font = '600 20px "Inter", sans-serif';
|
||||
ctx.fillStyle = color;
|
||||
const nameText = name.length > 18 ? `${name.slice(0, 18)}...` : name;
|
||||
|
||||
const drewLogo = drawModelLogo(name, WIDTH - 304, y + 6, 24);
|
||||
if (drewLogo) {
|
||||
ctx.fillText(nameText, WIDTH - 304 + 32, y + 24);
|
||||
} else {
|
||||
ctx.fillText(nameText, WIDTH - 304, y + 24);
|
||||
}
|
||||
|
||||
roundRect(WIDTH - 304, y + 42, 208, 4, 2, "#1c1c1c");
|
||||
if (pct > 0) {
|
||||
roundRect(WIDTH - 304, y + 42, Math.max(8, 208 * pct), 4, 2, color);
|
||||
}
|
||||
|
||||
ctx.font = '700 20px "JetBrains Mono", monospace';
|
||||
ctx.fillStyle = "#888";
|
||||
const scoreText = String(score);
|
||||
const scoreWidth = ctx.measureText(scoreText).width;
|
||||
ctx.fillText(scoreText, WIDTH - 48 - scoreWidth, y + 24);
|
||||
});
|
||||
const viewerStartY = 110 + 28 + modelEntries.length * entryHeight + 16;
|
||||
drawScoreboardSection(viewerEntries, "VIEWERS", viewerStartY, entryHeight);
|
||||
}
|
||||
|
||||
function drawRound(round: RoundState) {
|
||||
@@ -608,7 +632,7 @@ function draw() {
|
||||
return;
|
||||
}
|
||||
|
||||
drawScoreboard(state.scores);
|
||||
drawScoreboard(state.scores, state.viewerScores ?? {});
|
||||
|
||||
const isNextPrompting = state.active?.phase === "prompting" && !state.active.prompt;
|
||||
const displayRound = isNextPrompting && state.lastCompleted ? state.lastCompleted : (state.active ?? state.lastCompleted ?? null);
|
||||
|
||||
92
frontend.css
92
frontend.css
@@ -417,8 +417,8 @@ body {
|
||||
padding: 16px 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
max-height: 220px;
|
||||
gap: 20px;
|
||||
max-height: 50vh;
|
||||
overflow-y: auto;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
@@ -455,56 +455,87 @@ body {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.standings__list {
|
||||
/* ── Leaderboard Section ─────────────────────────────────────── */
|
||||
|
||||
.lb-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.standing {
|
||||
.lb-section__head {
|
||||
padding-bottom: 6px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.lb-section__label {
|
||||
font-family: var(--mono);
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 1.5px;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.lb-section__list {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 5px 0;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.standing--active {
|
||||
/* ── Leaderboard Entry ───────────────────────────────────────── */
|
||||
|
||||
.lb-entry {
|
||||
padding: 6px 0 4px;
|
||||
opacity: 0.55;
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
|
||||
.lb-entry--active,
|
||||
.lb-entry:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.standing__rank {
|
||||
width: 22px;
|
||||
.lb-entry__top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.lb-entry__rank {
|
||||
width: 20px;
|
||||
flex-shrink: 0;
|
||||
text-align: center;
|
||||
font-family: var(--mono);
|
||||
font-size: 12px;
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.standing__bar {
|
||||
flex: 1;
|
||||
height: 3px;
|
||||
background: var(--border);
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
min-width: 40px;
|
||||
}
|
||||
|
||||
.standing__fill {
|
||||
height: 100%;
|
||||
border-radius: 2px;
|
||||
transition: width 0.6s cubic-bezier(0.22, 1, 0.36, 1);
|
||||
}
|
||||
|
||||
.standing__score {
|
||||
.lb-entry__score {
|
||||
margin-left: auto;
|
||||
font-family: var(--mono);
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
color: var(--text-dim);
|
||||
min-width: 16px;
|
||||
min-width: 14px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.lb-entry__bar {
|
||||
margin-left: 26px;
|
||||
height: 3px;
|
||||
background: var(--border);
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.lb-entry__fill {
|
||||
height: 100%;
|
||||
border-radius: 2px;
|
||||
transition: width 0.6s cubic-bezier(0.22, 1, 0.36, 1);
|
||||
}
|
||||
|
||||
/* ── Connecting ───────────────────────────────────────────────── */
|
||||
|
||||
.connecting {
|
||||
@@ -720,5 +751,6 @@ body {
|
||||
max-height: none;
|
||||
overflow-y: auto;
|
||||
padding: 24px;
|
||||
gap: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
91
frontend.tsx
91
frontend.tsx
@@ -38,6 +38,7 @@ type GameState = {
|
||||
lastCompleted: RoundState | null;
|
||||
active: RoundState | null;
|
||||
scores: Record<string, number>;
|
||||
viewerScores: Record<string, number>;
|
||||
done: boolean;
|
||||
isPaused: boolean;
|
||||
generation: number;
|
||||
@@ -382,16 +383,63 @@ function GameOver({ scores }: { scores: Record<string, number> }) {
|
||||
|
||||
// ── Standings ────────────────────────────────────────────────────────────────
|
||||
|
||||
function Standings({
|
||||
function LeaderboardSection({
|
||||
label,
|
||||
scores,
|
||||
activeRound,
|
||||
competing,
|
||||
}: {
|
||||
label: string;
|
||||
scores: Record<string, number>;
|
||||
activeRound: RoundState | null;
|
||||
competing: Set<string>;
|
||||
}) {
|
||||
const sorted = Object.entries(scores).sort((a, b) => b[1] - a[1]);
|
||||
const maxScore = sorted[0]?.[1] || 1;
|
||||
|
||||
return (
|
||||
<div className="lb-section">
|
||||
<div className="lb-section__head">
|
||||
<span className="lb-section__label">{label}</span>
|
||||
</div>
|
||||
<div className="lb-section__list">
|
||||
{sorted.map(([name, score], i) => {
|
||||
const pct = maxScore > 0 ? Math.round((score / maxScore) * 100) : 0;
|
||||
const color = getColor(name);
|
||||
const active = competing.has(name);
|
||||
return (
|
||||
<div
|
||||
key={name}
|
||||
className={`lb-entry ${active ? "lb-entry--active" : ""}`}
|
||||
>
|
||||
<div className="lb-entry__top">
|
||||
<span className="lb-entry__rank">
|
||||
{i === 0 && score > 0 ? "👑" : i + 1}
|
||||
</span>
|
||||
<ModelTag model={{ id: name, name }} small />
|
||||
<span className="lb-entry__score">{score}</span>
|
||||
</div>
|
||||
<div className="lb-entry__bar">
|
||||
<div
|
||||
className="lb-entry__fill"
|
||||
style={{ width: `${pct}%`, background: color }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Standings({
|
||||
scores,
|
||||
viewerScores,
|
||||
activeRound,
|
||||
}: {
|
||||
scores: Record<string, number>;
|
||||
viewerScores: Record<string, number>;
|
||||
activeRound: RoundState | null;
|
||||
}) {
|
||||
const competing = activeRound
|
||||
? new Set([
|
||||
activeRound.contestants[0].name,
|
||||
@@ -415,31 +463,16 @@ function Standings({
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div className="standings__list">
|
||||
{sorted.map(([name, score], i) => {
|
||||
const pct = maxScore > 0 ? Math.round((score / maxScore) * 100) : 0;
|
||||
const color = getColor(name);
|
||||
const active = competing.has(name);
|
||||
return (
|
||||
<div
|
||||
key={name}
|
||||
className={`standing ${active ? "standing--active" : ""}`}
|
||||
>
|
||||
<span className="standing__rank">
|
||||
{i === 0 && score > 0 ? "👑" : i + 1}
|
||||
</span>
|
||||
<ModelTag model={{ id: name, name }} small />
|
||||
<div className="standing__bar">
|
||||
<div
|
||||
className="standing__fill"
|
||||
style={{ width: `${pct}%`, background: color }}
|
||||
/>
|
||||
</div>
|
||||
<span className="standing__score">{score}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<LeaderboardSection
|
||||
label="AI Judges"
|
||||
scores={scores}
|
||||
competing={competing}
|
||||
/>
|
||||
<LeaderboardSection
|
||||
label="Viewers"
|
||||
scores={viewerScores}
|
||||
competing={competing}
|
||||
/>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
@@ -578,7 +611,7 @@ function App() {
|
||||
)}
|
||||
</main>
|
||||
|
||||
<Standings scores={state.scores} activeRound={state.active} />
|
||||
<Standings scores={state.scores} viewerScores={state.viewerScores ?? {}} activeRound={state.active} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
9
game.ts
9
game.ts
@@ -73,6 +73,7 @@ export type GameState = {
|
||||
completed: RoundState[];
|
||||
active: RoundState | null;
|
||||
scores: Record<string, number>;
|
||||
viewerScores: Record<string, number>;
|
||||
done: boolean;
|
||||
isPaused: boolean;
|
||||
generation: number;
|
||||
@@ -471,6 +472,14 @@ export async function runGame(
|
||||
} else if (votesB > votesA) {
|
||||
state.scores[contB.name] = (state.scores[contB.name] || 0) + 1;
|
||||
}
|
||||
// Viewer vote scoring
|
||||
const vvA = round.viewerVotesA ?? 0;
|
||||
const vvB = round.viewerVotesB ?? 0;
|
||||
if (vvA > vvB) {
|
||||
state.viewerScores[contA.name] = (state.viewerScores[contA.name] || 0) + 1;
|
||||
} else if (vvB > vvA) {
|
||||
state.viewerScores[contB.name] = (state.viewerScores[contB.name] || 0) + 1;
|
||||
}
|
||||
rerender();
|
||||
|
||||
await new Promise((r) => setTimeout(r, 5000));
|
||||
|
||||
11
quipslop.tsx
11
quipslop.tsx
@@ -223,11 +223,12 @@ function Game({ runs }: { runs: number }) {
|
||||
const stateRef = useRef<GameState>({
|
||||
completed: [],
|
||||
active: null,
|
||||
scores: Object.fromEntries(MODELS.map((m) => [m.name, 0])),
|
||||
done: false,
|
||||
isPaused: false,
|
||||
generation: 0,
|
||||
});
|
||||
scores: Object.fromEntries(MODELS.map((m) => [m.name, 0])),
|
||||
viewerScores: Object.fromEntries(MODELS.map((m) => [m.name, 0])),
|
||||
done: false,
|
||||
isPaused: false,
|
||||
generation: 0,
|
||||
});
|
||||
const [, setTick] = useState(0);
|
||||
const rerender = useCallback(() => setTick((t) => t + 1), []);
|
||||
|
||||
|
||||
13
server.ts
13
server.ts
@@ -30,6 +30,7 @@ if (!process.env.OPENROUTER_API_KEY) {
|
||||
|
||||
const allRounds = getAllRounds();
|
||||
const initialScores = Object.fromEntries(MODELS.map((m) => [m.name, 0]));
|
||||
const initialViewerScores = Object.fromEntries(MODELS.map((m) => [m.name, 0]));
|
||||
|
||||
let initialCompleted: RoundState[] = [];
|
||||
if (allRounds.length > 0) {
|
||||
@@ -43,6 +44,15 @@ if (allRounds.length > 0) {
|
||||
(initialScores[round.contestants[1].name] || 0) + 1;
|
||||
}
|
||||
}
|
||||
const vvA = round.viewerVotesA ?? 0;
|
||||
const vvB = round.viewerVotesB ?? 0;
|
||||
if (vvA > vvB) {
|
||||
initialViewerScores[round.contestants[0].name] =
|
||||
(initialViewerScores[round.contestants[0].name] || 0) + 1;
|
||||
} else if (vvB > vvA) {
|
||||
initialViewerScores[round.contestants[1].name] =
|
||||
(initialViewerScores[round.contestants[1].name] || 0) + 1;
|
||||
}
|
||||
}
|
||||
const lastRound = allRounds[allRounds.length - 1];
|
||||
if (lastRound) {
|
||||
@@ -54,6 +64,7 @@ const gameState: GameState = {
|
||||
completed: initialCompleted,
|
||||
active: null,
|
||||
scores: initialScores,
|
||||
viewerScores: initialViewerScores,
|
||||
done: false,
|
||||
isPaused: false,
|
||||
generation: 0,
|
||||
@@ -349,6 +360,7 @@ function getClientState() {
|
||||
active: gameState.active,
|
||||
lastCompleted: gameState.completed.at(-1) ?? null,
|
||||
scores: gameState.scores,
|
||||
viewerScores: gameState.viewerScores,
|
||||
done: gameState.done,
|
||||
isPaused: gameState.isPaused,
|
||||
generation: gameState.generation,
|
||||
@@ -635,6 +647,7 @@ const server = Bun.serve<WsData>({
|
||||
gameState.completed = [];
|
||||
gameState.active = null;
|
||||
gameState.scores = Object.fromEntries(MODELS.map((m) => [m.name, 0]));
|
||||
gameState.viewerScores = Object.fromEntries(MODELS.map((m) => [m.name, 0]));
|
||||
gameState.done = false;
|
||||
gameState.isPaused = true;
|
||||
gameState.generation += 1;
|
||||
|
||||
Reference in New Issue
Block a user