Better frontend kind of

This commit is contained in:
Theo Browne
2026-02-19 23:28:03 -08:00
parent bca390ae75
commit ce1ef83926
11 changed files with 1241 additions and 361 deletions

View File

@@ -1,262 +1,706 @@
/* ── Reset & Base ─────────────────────────────────────────────── */
* { margin: 0; padding: 0; box-sizing: border-box; }
:root { :root {
--bg: #0d1117; --bg: #050505;
--surface: #161b22; --surface: #0a0a0a;
--border: #30363d; --surface-2: #141414;
--text: #e6edf3; --border: #222222;
--text-dim: #7d8590; --border-light: #333333;
--accent: #58a6ff; --text: #ffffff;
--text-dim: #a1a1a1;
--cyan: #56d4dd; --text-muted: #555555;
--green: #3fb950;
--magenta: #db61a2;
--green-bright: #7ee787;
--cyan-bright: #79dafa;
--yellow: #e3b341;
--blue: #58a6ff;
--red: #f85149;
--white: #e6edf3;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
} }
body { body {
background: var(--bg); background: var(--bg);
color: var(--text); color: var(--text);
font-family: "SF Mono", "Fira Code", "JetBrains Mono", "Cascadia Code", monospace; font-family: 'Inter', -apple-system, sans-serif;
font-size: 14px; font-size: 15px;
line-height: 1.6; line-height: 1.5;
padding: 24px;
min-height: 100vh; min-height: 100vh;
overflow: hidden;
-webkit-font-smoothing: antialiased;
} }
#root { /* ── App Layout ───────────────────────────────────────────────── */
max-width: 720px;
margin: 0 auto; .app {
height: 100vh;
display: flex;
flex-direction: column;
} }
/* ── Header ─────────────────────────────────────────────────────── */ /* ── Header ───────────────────────────────────────────────────── */
.header { .header {
margin-bottom: 24px; padding: 24px 48px;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid var(--border);
background: var(--bg);
flex-shrink: 0;
} }
.header-title { .header__logo {
display: inline-block; font-family: 'JetBrains Mono', monospace;
background: #8b5cf6; font-size: 24px;
color: #000;
font-weight: 700; font-weight: 700;
font-size: 18px; letter-spacing: -1px;
padding: 4px 12px; color: var(--text);
}
.header__tagline {
color: var(--text-muted);
font-family: 'JetBrains Mono', monospace;
font-size: 12px;
text-transform: uppercase;
letter-spacing: 1px; letter-spacing: 1px;
} }
.header-sub { /* ── Layout Grid ──────────────────────────────────────────────── */
color: var(--text-dim);
margin-top: 4px;
font-size: 13px;
}
/* ── Round ──────────────────────────────────────────────────────── */ .layout {
flex: 1;
.round {
margin-bottom: 24px;
}
.round-header {
display: inline-block;
background: #1f6feb;
color: #fff;
font-weight: 700;
padding: 2px 10px;
font-size: 13px;
}
.divider {
color: var(--border);
margin: 4px 0;
user-select: none;
}
/* ── Phase badges ──────────────────────────────────────────────── */
.badge {
display: inline-block;
font-weight: 700;
padding: 1px 8px;
font-size: 12px;
margin-right: 8px;
}
.badge-prompt { background: #a855f7; color: #000; }
.badge-answers { background: #22d3ee; color: #000; }
.badge-votes { background: #eab308; color: #000; }
.badge-scores { background: #f43f5e; color: #fff; }
/* ── Phase sections ────────────────────────────────────────────── */
.phase {
margin-top: 12px;
}
.phase-row {
display: flex; display: flex;
align-items: baseline; overflow: hidden;
gap: 8px;
padding: 2px 0 2px 16px;
} }
.prompt-text { .main {
color: var(--yellow); flex: 1;
font-weight: 700; overflow-y: auto;
padding: 4px 0 4px 16px; padding: 48px 64px;
font-size: 15px; display: flex;
flex-direction: column;
align-items: center;
scroll-behavior: smooth;
} }
.dim { color: var(--text-dim); } /* ── Sidebar ──────────────────────────────────────────────────── */
.bold { font-weight: 700; }
.error { color: var(--red); }
.answer-text { font-weight: 700; } .sidebar {
width: 320px;
.vote-arrow { color: var(--text-dim); } border-left: 1px solid var(--border);
background: var(--surface);
/* ── Result ────────────────────────────────────────────────────── */ display: flex;
flex-direction: column;
.round-result { overflow-y: auto;
margin-top: 8px; flex-shrink: 0;
padding: 8px 16px;
border-top: 1px solid var(--border);
border-bottom: 1px solid var(--border);
} }
.result-winner { font-weight: 700; } .sidebar__header {
.result-detail { color: var(--text-dim); font-size: 13px; padding-left: 16px; } font-family: 'JetBrains Mono', monospace;
/* ── Timer ─────────────────────────────────────────────────────── */
.timer {
color: var(--text-dim);
font-size: 12px;
}
/* ── Scoreboard ────────────────────────────────────────────────── */
.scoreboard {
margin-top: 24px;
}
.scoreboard-title {
display: inline-block;
background: #a855f7;
color: #000;
font-weight: 700;
padding: 2px 10px;
font-size: 14px; font-size: 14px;
margin-bottom: 12px; font-weight: 700;
letter-spacing: 1px;
text-transform: uppercase;
padding: 32px 32px 16px;
color: var(--text-dim);
} }
.score-row { .sidebar__list {
flex: 1;
padding: 0 16px 32px;
display: flex;
flex-direction: column;
gap: 4px;
}
.standing {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 10px; gap: 16px;
padding: 3px 0 3px 16px; padding: 16px;
border-radius: 8px;
transition: background 0.2s;
} }
.score-rank { .standing--active {
color: var(--text-dim); background: var(--surface-2);
min-width: 24px;
text-align: right;
} }
.score-name { .standing__rank {
width: 20px;
text-align: center;
font-family: 'JetBrains Mono', monospace;
font-weight: 700; font-weight: 700;
min-width: 160px; color: var(--text-muted);
font-size: 13px;
flex-shrink: 0;
} }
.score-bar-track { .standing__info {
width: 200px; flex: 1;
height: 14px; min-width: 0;
background: var(--surface); }
.standing__name-row {
display: flex;
align-items: center;
gap: 8px;
}
.standing__name {
font-weight: 600;
font-size: 14px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
display: flex;
align-items: center;
gap: 8px;
}
.standing__role {
font-size: 12px;
flex-shrink: 0;
}
.standing__bar-row {
display: flex;
align-items: center;
gap: 12px;
margin-top: 8px;
}
.standing__bar {
flex: 1;
height: 4px;
background: var(--border);
border-radius: 2px; border-radius: 2px;
overflow: hidden; overflow: hidden;
} }
.score-bar-fill { .standing__bar-fill {
height: 100%; height: 100%;
border-radius: 2px; border-radius: 2px;
transition: width 0.4s ease; transition: width 0.6s cubic-bezier(0.22, 1, 0.36, 1);
} }
.score-value { .standing__score {
font-family: 'JetBrains Mono', monospace;
font-size: 12px;
font-weight: 700; font-weight: 700;
min-width: 40px;
}
.score-medal { font-size: 16px; }
.winner-banner {
margin-top: 12px;
padding-left: 16px;
font-size: 16px;
}
/* ── Model colors ──────────────────────────────────────────────── */
.model-gemini-3-1-pro { color: var(--cyan); }
.model-kimi-k2 { color: var(--green); }
.model-kimi-k2-5 { color: var(--magenta); }
.model-deepseek-3-2 { color: var(--green-bright); }
.model-glm-5 { color: var(--cyan-bright); }
.model-gpt-5-2 { color: var(--yellow); }
.model-opus-4-6 { color: var(--blue); }
.model-sonnet-4-6 { color: var(--red); }
.model-grok-4-1 { color: var(--white); }
/* Bar fill colors */
.bar-gemini-3-1-pro { background: var(--cyan); }
.bar-kimi-k2 { background: var(--green); }
.bar-kimi-k2-5 { background: var(--magenta); }
.bar-deepseek-3-2 { background: var(--green-bright); }
.bar-glm-5 { background: var(--cyan-bright); }
.bar-gpt-5-2 { background: var(--yellow); }
.bar-opus-4-6 { background: var(--blue); }
.bar-sonnet-4-6 { background: var(--red); }
.bar-grok-4-1 { background: var(--white); }
/* ── Connecting state ──────────────────────────────────────────── */
.connecting {
color: var(--text-dim); color: var(--text-dim);
padding: 40px 0; min-width: 32px;
text-align: right;
}
.sidebar__legend {
padding: 24px 32px;
border-top: 1px solid var(--border);
display: flex;
flex-direction: column;
gap: 12px;
font-family: 'JetBrains Mono', monospace;
font-size: 11px;
color: var(--text-muted);
}
/* ── Arena ─────────────────────────────────────────────────────── */
.arena {
width: 100%;
max-width: 1100px;
margin: 0 auto;
display: flex;
flex-direction: column;
}
.arena__header-row {
display: flex;
justify-content: space-between;
align-items: baseline;
margin-bottom: 48px;
padding-bottom: 24px;
border-bottom: 1px solid var(--border);
}
.arena__round-badge {
font-family: 'JetBrains Mono', monospace;
font-size: 14px;
font-weight: 700;
color: var(--text);
letter-spacing: 1px;
}
.arena__round-of {
color: var(--text-muted);
}
.arena__phase {
font-family: 'JetBrains Mono', monospace;
font-size: 12px;
letter-spacing: 1px;
text-transform: uppercase;
color: var(--text-dim);
}
/* ── Prompt Card ──────────────────────────────────────────────── */
.prompt-card {
margin-bottom: 64px;
}
.prompt-card__by {
font-family: 'JetBrains Mono', monospace;
font-size: 12px;
color: var(--text-muted);
margin-bottom: 24px;
text-transform: uppercase;
letter-spacing: 1px;
display: flex;
align-items: center;
gap: 8px;
}
.prompt-card__text {
font-family: 'DM Serif Display', serif;
font-size: 56px;
line-height: 1.1;
color: var(--text);
max-width: 95%;
letter-spacing: -1px;
}
.prompt-card__text--loading {
color: var(--text-muted);
}
/* ── Showdown ─────────────────────────────────────────────────── */
.showdown {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 48px;
margin-bottom: 64px;
}
.contestant {
background: var(--bg);
display: flex;
flex-direction: column;
position: relative;
border-top: 4px solid var(--border);
padding-top: 24px;
}
.contestant__header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 32px;
padding-bottom: 24px;
border-bottom: 1px solid var(--border);
}
.contestant__name {
font-family: 'Inter', sans-serif;
font-weight: 600;
font-size: 18px;
display: flex;
align-items: center;
gap: 12px;
}
.contestant__winner-badge {
font-family: 'JetBrains Mono', monospace;
font-size: 11px;
font-weight: 700;
padding: 4px 8px;
background: var(--text);
color: var(--bg);
border-radius: 4px;
letter-spacing: 1px;
}
.contestant__answer {
flex: 1;
min-height: 180px;
}
.contestant__text {
font-family: 'DM Serif Display', serif;
font-size: 36px;
line-height: 1.3;
color: var(--text-dim);
letter-spacing: -0.5px;
}
.contestant--winner .contestant__text {
color: var(--text);
}
.contestant__thinking {
color: var(--text-muted);
font-family: 'DM Serif Display', serif;
font-size: 36px;
}
.contestant__error {
color: #ef4444;
font-family: 'JetBrains Mono', monospace;
font-size: 14px;
}
.contestant__votes {
margin-top: 48px;
}
/* ── Vote Bar ─────────────────────────────────────────────────── */
.vote-bar {
height: 2px;
background: var(--border);
width: 100%;
}
.vote-bar__fill {
height: 100%;
transition: width 0.6s cubic-bezier(0.22, 1, 0.36, 1);
}
.vote-bar__label {
display: flex;
justify-content: space-between;
margin-top: 16px;
font-family: 'JetBrains Mono', monospace;
font-size: 13px;
color: var(--text-dim);
}
.vote-bar__count {
font-weight: 700;
color: var(--text);
font-size: 18px;
}
/* ── Vote Ticker ──────────────────────────────────────────────── */
.vote-ticker {
border-top: 1px solid var(--border);
padding-top: 48px;
margin-bottom: 64px;
}
.vote-ticker__header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 32px;
}
.vote-ticker__title {
font-family: 'JetBrains Mono', monospace;
font-size: 13px;
font-weight: 700;
letter-spacing: 1px;
color: var(--text-dim);
}
.vote-ticker__status {
font-family: 'JetBrains Mono', monospace;
font-size: 13px;
color: var(--text-muted);
}
.vote-ticker__list {
display: flex;
flex-wrap: wrap;
gap: 16px;
}
.vote-entry {
display: flex;
align-items: center;
gap: 12px;
font-family: 'Inter', sans-serif;
font-size: 14px;
font-weight: 500;
padding: 12px 16px;
border-radius: 8px;
border: 1px solid var(--border);
background: var(--surface);
}
.vote-entry--pending {
opacity: 0.5;
border-style: dashed;
}
.vote-entry__arrow {
color: var(--text-muted);
}
.vote-entry__pending {
color: var(--text-muted);
font-style: italic;
font-size: 13px;
}
.vote-entry__error {
color: var(--text-muted);
font-style: italic;
font-size: 13px;
}
/* ── Round Result (Tie) ───────────────────────────────────────── */
.round-result {
text-align: center;
font-family: 'JetBrains Mono', monospace;
font-size: 16px;
font-weight: 700;
letter-spacing: 2px;
padding: 32px;
color: var(--text-dim);
border: 1px solid var(--border);
border-radius: 8px;
}
/* ── Waiting State ────────────────────────────────────────────── */
.arena-waiting {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
font-family: 'DM Serif Display', serif;
color: var(--text-muted);
font-size: 48px;
opacity: 0.5;
}
/* ── History ──────────────────────────────────────────────────── */
.history {
width: 100%;
max-width: 1100px;
margin: 80px auto 0;
}
.history__title {
font-family: 'JetBrains Mono', monospace;
font-size: 14px;
font-weight: 700;
letter-spacing: 1px;
color: var(--text-dim);
margin-bottom: 32px;
padding-bottom: 24px;
border-bottom: 1px solid var(--border);
}
.past-round {
padding: 32px 0;
border-bottom: 1px solid var(--border);
display: flex;
flex-direction: column;
gap: 24px;
}
.past-round__header {
display: flex;
gap: 24px;
align-items: baseline;
}
.past-round__num {
font-family: 'JetBrains Mono', monospace;
font-size: 14px;
color: var(--text-muted);
width: 32px;
}
.past-round__prompt {
font-family: 'Inter', sans-serif;
font-size: 18px;
font-weight: 600;
color: var(--text);
flex: 1;
}
.past-round__detail {
padding-left: 56px;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 48px;
}
.past-round__competitor {
display: flex;
flex-direction: column;
gap: 12px;
}
.past-round__competitor-header {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 14px;
}
.past-round__answer {
font-family: 'DM Serif Display', serif;
font-size: 20px;
color: var(--text-dim);
line-height: 1.4;
}
.past-round__competitor--winner .past-round__answer {
color: var(--text);
}
.past-round__score {
font-family: 'JetBrains Mono', monospace;
font-weight: 700;
color: var(--text-muted);
}
.past-round__winner-tag {
font-family: 'JetBrains Mono', monospace;
font-weight: 700;
font-size: 10px;
padding: 2px 6px;
background: var(--text);
color: var(--bg);
border-radius: 4px;
text-transform: uppercase;
}
/* ── Game Over ────────────────────────────────────────────────── */
.game-over {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center; text-align: center;
} }
.connecting-dot { .game-over__title {
animation: blink 1s infinite; font-family: 'JetBrains Mono', monospace;
font-size: 24px;
font-weight: 700;
letter-spacing: 4px;
color: var(--text-muted);
margin-bottom: 48px;
} }
@keyframes blink { .game-over__champion {
0%, 100% { opacity: 1; } display: flex;
50% { opacity: 0.3; } flex-direction: column;
align-items: center;
gap: 24px;
} }
/* ── Spinner ───────────────────────────────────────────────────── */ .game-over__crown {
font-size: 48px;
@keyframes spin {
to { transform: rotate(360deg); }
} }
.spinner { .game-over__name {
display: inline-block; font-family: 'DM Serif Display', serif;
font-size: 72px;
line-height: 1;
display: flex;
align-items: center;
gap: 24px;
}
.game-over__name img {
width: 64px;
height: 64px;
}
.game-over__subtitle {
font-family: 'Inter', sans-serif;
font-size: 16px;
color: var(--text-dim); color: var(--text-dim);
animation: pulse 1.5s ease-in-out infinite;
} }
@keyframes pulse { /* ── Connecting ───────────────────────────────────────────────── */
0%, 100% { opacity: 1; }
50% { opacity: 0.4; } .connecting {
height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 24px;
background: var(--bg);
} }
.connecting__logo {
font-family: 'JetBrains Mono', monospace;
font-size: 48px;
font-weight: 700;
letter-spacing: -2px;
color: var(--text);
}
.connecting__text {
color: var(--text-muted);
font-family: 'JetBrains Mono', monospace;
font-size: 16px;
letter-spacing: 2px;
text-transform: uppercase;
}
/* ── Utility ──────────────────────────────────────────────────── */
.model-name {
display: inline-flex;
align-items: center;
gap: 8px;
font-weight: 600;
}
.model-logo {
width: 18px;
height: 18px;
object-fit: contain;
}
.model-logo.invert-dark {
/* Some logos might need this if they are dark naturally, but SVG styling handles it or we'll assume they're visible. */
}
.timer {
color: var(--text-muted);
font-family: 'JetBrains Mono', monospace;
}
.dots span {
animation: dot-blink 1.4s infinite;
}
.dots span:nth-child(2) { animation-delay: 0.2s; }
.dots span:nth-child(3) { animation-delay: 0.4s; }
@keyframes dot-blink {
0%, 80%, 100% { opacity: 0.15; }
40% { opacity: 1; }
}
/* ── Scrollbar ────────────────────────────────────────────────── */
::-webkit-scrollbar { width: 8px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; }
::-webkit-scrollbar-thumb:hover { background: var(--border-light); }
/* ── Responsive ───────────────────────────────────────────────── */
@media (max-width: 1100px) {
.layout { flex-direction: column; }
.sidebar {
width: 100%;
border-left: none;
border-top: 1px solid var(--border);
max-height: 300px;
}
.showdown { grid-template-columns: 1fr; gap: 48px; }
.past-round__detail { grid-template-columns: 1fr; }
}

View File

@@ -5,23 +5,8 @@ import "./frontend.css";
// ── Types (mirrors game.ts) ───────────────────────────────────────────────── // ── Types (mirrors game.ts) ─────────────────────────────────────────────────
type Model = { id: string; name: string }; type Model = { id: string; name: string };
type TaskInfo = { model: Model; startedAt: number; finishedAt?: number; result?: string; error?: string };
type TaskInfo = { type VoteInfo = { voter: Model; startedAt: number; finishedAt?: number; votedFor?: Model; error?: boolean };
model: Model;
startedAt: number;
finishedAt?: number;
result?: string;
error?: string;
};
type VoteInfo = {
voter: Model;
startedAt: number;
finishedAt?: number;
votedFor?: Model;
error?: boolean;
};
type RoundState = { type RoundState = {
num: number; num: number;
phase: "prompting" | "answering" | "voting" | "done"; phase: "prompting" | "answering" | "voting" | "done";
@@ -34,28 +19,35 @@ type RoundState = {
scoreA?: number; scoreA?: number;
scoreB?: number; scoreB?: number;
}; };
type GameState = { completed: RoundState[]; active: RoundState | null; scores: Record<string, number>; done: boolean };
type ServerMessage = { type: "state"; data: GameState; totalRounds: number };
type GameState = { // ── Model Assets & Colors ───────────────────────────────────────────────────
completed: RoundState[];
active: RoundState | null; const MODEL_COLORS: Record<string, string> = {
scores: Record<string, number>; "Gemini 3.1 Pro": "#4285F4",
done: boolean; "Kimi K2": "#00E599",
"DeepSeek 3.2": "#4D6BFE",
"GLM-5": "#1F63EC",
"GPT-5.2": "#10A37F",
"Opus 4.6": "#D97757",
"Sonnet 4.6": "#D97757",
"Grok 4.1": "#FFFFFF",
}; };
type ServerMessage = { function getColor(name: string): string {
type: "state"; return MODEL_COLORS[name] ?? "#A1A1A1";
data: GameState;
totalRounds: number;
};
// ── Helpers ──────────────────────────────────────────────────────────────────
function modelClass(name: string): string {
return "model-" + name.toLowerCase().replace(/[\s.]+/g, "-");
} }
function barClass(name: string): string { function getLogo(name: string): string | null {
return "bar-" + name.toLowerCase().replace(/[\s.]+/g, "-"); if (name.includes("Gemini")) return "/assets/logos/gemini.svg";
if (name.includes("Kimi")) return "/assets/logos/kimi.svg";
if (name.includes("DeepSeek")) return "/assets/logos/deepseek.svg";
if (name.includes("GLM")) return "/assets/logos/glm.svg";
if (name.includes("GPT")) return "/assets/logos/openai.svg";
if (name.includes("Opus") || name.includes("Sonnet")) return "/assets/logos/claude.svg";
if (name.includes("Grok")) return "/assets/logos/grok.svg";
return null;
} }
// ── Components ────────────────────────────────────────────────────────────── // ── Components ──────────────────────────────────────────────────────────────
@@ -68,101 +60,97 @@ function Timer({ startedAt, finishedAt }: { startedAt: number; finishedAt?: numb
return () => clearInterval(id); return () => clearInterval(id);
}, [finishedAt]); }, [finishedAt]);
const elapsed = ((finishedAt ?? now) - startedAt) / 1000; const elapsed = ((finishedAt ?? now) - startedAt) / 1000;
return <span className="timer">({elapsed.toFixed(1)}s)</span>; return <span className="timer">{elapsed.toFixed(1)}s</span>;
} }
function MName({ model }: { model: Model }) { function ModelName({ model, className = "", showLogo = true }: { model: Model; className?: string, showLogo?: boolean }) {
return <span className={`bold ${modelClass(model.name)}`}>{model.name}</span>; const logo = getLogo(model.name);
const color = getColor(model.name);
return (
<span className={`model-name ${className}`} style={{ color }}>
{showLogo && logo && <img src={logo} alt="" className="model-logo" />}
{model.name}
</span>
);
} }
function RoundView({ round, total }: { round: RoundState; total: number }) { function PromptCard({ round }: { round: RoundState }) {
const [contA, contB] = round.contestants; if (round.phase === "prompting" && !round.prompt) {
return (
<div className="prompt-card prompt-card--loading">
<div className="prompt-card__by">
<ModelName model={round.prompter} /> is cooking up a prompt
</div>
<div className="prompt-card__text prompt-card__text--loading">
<span className="dots"><span>.</span><span>.</span><span>.</span></span>
</div>
</div>
);
}
if (round.promptTask.error) {
return (
<div className="prompt-card prompt-card--error">
<div className="prompt-card__text" style={{ color: "#ef4444" }}>Prompt generation failed</div>
</div>
);
}
return ( return (
<div className="round"> <div className="prompt-card">
<span className="round-header">ROUND {round.num}/{total}</span> <div className="prompt-card__by">
<div className="divider">{"─".repeat(50)}</div> Prompted by <ModelName model={round.prompter} />
</div>
<div className="prompt-card__text">{round.prompt}</div>
</div>
);
}
{/* Prompt */} function ContestantPanel({
<div className="phase"> task,
<div className="phase-row"> voteCount,
<span className="badge badge-prompt">PROMPT</span> totalVotes,
<MName model={round.prompter} /> isWinner,
{!round.prompt && !round.promptTask.error && ( showVotes,
<span className="spinner">writing a prompt...</span> }: {
)} task: TaskInfo;
<Timer startedAt={round.promptTask.startedAt} finishedAt={round.promptTask.finishedAt} /> voteCount: number;
totalVotes: number;
isWinner: boolean;
showVotes: boolean;
}) {
const color = getColor(task.model.name);
const pct = totalVotes > 0 ? Math.round((voteCount / totalVotes) * 100) : 0;
return (
<div className={`contestant ${isWinner ? "contestant--winner" : ""}`} style={{ borderColor: color }}>
<div className="contestant__header">
<div className="contestant__name">
<ModelName model={task.model} />
</div> </div>
{round.promptTask.error && ( {isWinner && <div className="contestant__winner-badge">WINNER</div>}
<div className="phase-row"><span className="error"> {round.promptTask.error}</span></div> </div>
)}
{round.prompt && ( <div className="contestant__answer">
<div className="prompt-text">"{round.prompt}"</div> {!task.finishedAt ? (
<span className="contestant__thinking">
<span className="dots"><span>.</span><span>.</span><span>.</span></span>
</span>
) : task.error ? (
<span className="contestant__error"> {task.error}</span>
) : (
<span className="contestant__text">&ldquo;{task.result}&rdquo;</span>
)} )}
</div> </div>
{/* Answers */} {showVotes && (
{round.phase !== "prompting" && ( <div className="contestant__votes">
<div className="phase"> <div className="vote-bar">
<span className="badge badge-answers">ANSWERS</span> <div className="vote-bar__fill" style={{ width: `${pct}%`, background: color }} />
{round.answerTasks.map((task, i) => (
<div key={i} className="phase-row">
<MName model={task.model} />
{!task.finishedAt ? (
<span className="spinner">thinking...</span>
) : task.error ? (
<span className="error"> {task.error}</span>
) : (
<span className="answer-text">"{task.result}"</span>
)}
{task.startedAt > 0 && (
<Timer startedAt={task.startedAt} finishedAt={task.finishedAt} />
)}
</div>
))}
</div>
)}
{/* Votes */}
{(round.phase === "voting" || round.phase === "done") && (
<div className="phase">
<span className="badge badge-votes">VOTES</span>
{round.votes.map((vote, i) => (
<div key={i} className="phase-row">
<MName model={vote.voter} />
{!vote.finishedAt ? (
<span className="spinner">voting...</span>
) : vote.error || !vote.votedFor ? (
<span className="error"> failed</span>
) : (
<span><span className="vote-arrow"> </span><MName model={vote.votedFor} /></span>
)}
<Timer startedAt={vote.startedAt} finishedAt={vote.finishedAt} />
</div>
))}
</div>
)}
{/* Round result */}
{round.phase === "done" && round.scoreA !== undefined && round.scoreB !== undefined && (
<div className="round-result">
<div>
{round.scoreA > round.scoreB ? (
<span className="result-winner">
<MName model={contA} /> wins! ({round.scoreA / 100} vs {round.scoreB / 100} votes)
</span>
) : round.scoreB > round.scoreA ? (
<span className="result-winner">
<MName model={contB} /> wins! ({round.scoreB / 100} vs {round.scoreA / 100} votes)
</span>
) : (
<span className="result-winner">TIE! ({round.scoreA / 100} - {round.scoreB / 100})</span>
)}
</div> </div>
<div className="result-detail"> <div className="vote-bar__label">
<MName model={contA} /> <span className="dim">+{round.scoreA}</span> <span className="vote-bar__count" style={{ color }}>{voteCount}</span>
{" | "} <span className="vote-bar__pct">{pct}%</span>
<MName model={contB} /> <span className="dim">+{round.scoreB}</span>
</div> </div>
</div> </div>
)} )}
@@ -170,43 +158,241 @@ function RoundView({ round, total }: { round: RoundState; total: number }) {
); );
} }
function Scoreboard({ scores }: { scores: Record<string, number> }) { function VoteTicker({ votes }: { votes: VoteInfo[] }) {
const sorted = Object.entries(scores).sort((a, b) => b[1] - a[1]); const finishedVotes = votes.filter((v) => v.finishedAt);
const maxScore = sorted[0]?.[1] || 1; const pendingVotes = votes.filter((v) => !v.finishedAt);
const medals = ["👑", "🥈", "🥉"];
return ( return (
<div className="scoreboard"> <div className="vote-ticker">
<span className="scoreboard-title">FINAL SCORES</span> <div className="vote-ticker__header">
{sorted.map(([name, score], i) => { <span className="vote-ticker__title">JUDGES</span>
const pct = Math.round((score / maxScore) * 100); <span className="vote-ticker__status">
return ( {finishedVotes.length} / {votes.length}
<div key={name} className="score-row"> </span>
<span className="score-rank">{i + 1}.</span> </div>
<span className={`score-name ${modelClass(name)}`}>{name}</span> <div className="vote-ticker__list">
<div className="score-bar-track"> {finishedVotes.map((vote, i) => (
<div className={`score-bar-fill ${barClass(name)}`} style={{ width: `${pct}%` }} /> <div key={`f-${i}`} className="vote-entry vote-entry--in">
</div> <ModelName model={vote.voter} showLogo={false} />
<span className="score-value">{score}</span> <span className="vote-entry__arrow"></span>
{i < 3 && <span className="score-medal">{medals[i]}</span>} {vote.error || !vote.votedFor ? (
<span className="vote-entry__error">abstained</span>
) : (
<ModelName model={vote.votedFor} showLogo={false} />
)}
</div> </div>
); ))}
})} {pendingVotes.map((vote, i) => (
{sorted[0] && sorted[0][1] > 0 && ( <div key={`p-${i}`} className="vote-entry vote-entry--pending">
<div className="winner-banner"> <ModelName model={vote.voter} showLogo={false} />
🏆 <span className={`bold ${modelClass(sorted[0][0])}`}>{sorted[0][0]}</span> <span className="vote-entry__pending">deliberating</span>
<span className="bold"> is the funniest AI!</span> </div>
))}
</div>
</div>
);
}
function Arena({ round, total }: { round: RoundState; total: number }) {
const [contA, contB] = round.contestants;
const showVotes = round.phase === "voting" || round.phase === "done";
const isDone = round.phase === "done";
let votesA = 0,
votesB = 0;
for (const v of round.votes) {
if (v.votedFor?.name === contA.name) votesA++;
else if (v.votedFor?.name === contB.name) votesB++;
}
const totalVotes = votesA + votesB;
const phaseLabel =
round.phase === "prompting"
? "✍️ WRITING PROMPT"
: round.phase === "answering"
? "💭 ANSWERING"
: round.phase === "voting"
? "🗳️ JUDGES VOTING"
: "✅ ROUND COMPLETE";
return (
<div className="arena">
<div className="arena__header-row">
<div className="arena__round-badge">
ROUND {round.num} <span className="arena__round-of">/ {total}</span>
</div>
<div className="arena__phase">{phaseLabel}</div>
</div>
<PromptCard round={round} />
{round.phase !== "prompting" && (
<>
<div className="showdown">
<ContestantPanel
task={round.answerTasks[0]}
voteCount={votesA}
totalVotes={totalVotes}
isWinner={isDone && votesA > votesB}
showVotes={showVotes}
/>
<ContestantPanel
task={round.answerTasks[1]}
voteCount={votesB}
totalVotes={totalVotes}
isWinner={isDone && votesB > votesA}
showVotes={showVotes}
/>
</div>
{showVotes && <VoteTicker votes={round.votes} />}
{isDone && votesA === votesB && (
<div className="round-result">IT&rsquo;S A TIE!</div>
)}
</>
)}
</div>
);
}
function PastRoundEntry({ round }: { round: RoundState }) {
const [contA, contB] = round.contestants;
let votesA = 0,
votesB = 0;
for (const v of round.votes) {
if (v.votedFor?.name === contA.name) votesA++;
else if (v.votedFor?.name === contB.name) votesB++;
}
const isAWinner = votesA > votesB;
const isBWinner = votesB > votesA;
return (
<div className="past-round">
<div className="past-round__header">
<span className="past-round__num">R{round.num}</span>
<span className="past-round__prompt">{round.prompt}</span>
</div>
<div className="past-round__detail">
<div className={`past-round__competitor ${isAWinner ? 'past-round__competitor--winner' : ''}`}>
<div className="past-round__competitor-header">
<ModelName model={contA} />
<div style={{ display: 'flex', gap: '12px', alignItems: 'center' }}>
<span className="past-round__score">{votesA}</span>
{isAWinner && <span className="past-round__winner-tag">WINNER</span>}
</div>
</div>
<span className="past-round__answer">&ldquo;{round.answerTasks[0].result}&rdquo;</span>
</div>
<div className={`past-round__competitor ${isBWinner ? 'past-round__competitor--winner' : ''}`}>
<div className="past-round__competitor-header">
<ModelName model={contB} />
<div style={{ display: 'flex', gap: '12px', alignItems: 'center' }}>
<span className="past-round__score">{votesB}</span>
{isBWinner && <span className="past-round__winner-tag">WINNER</span>}
</div>
</div>
<span className="past-round__answer">&ldquo;{round.answerTasks[1].result}&rdquo;</span>
</div>
</div>
</div>
);
}
function GameOver({ scores }: { scores: Record<string, number> }) {
const sorted = Object.entries(scores).sort((a, b) => b[1] - a[1]);
const champion = sorted[0];
return (
<div className="game-over">
<div className="game-over__title">GAME OVER</div>
{champion && champion[1] > 0 && (
<div className="game-over__champion">
<div className="game-over__crown">👑</div>
<div className="game-over__name" style={{ color: getColor(champion[0]) }}>
{getLogo(champion[0]) && <img src={getLogo(champion[0])!} alt="" />}
{champion[0]}
</div>
<div className="game-over__subtitle">is the funniest AI!</div>
</div> </div>
)} )}
</div> </div>
); );
} }
function Sidebar({ scores, activeRound }: { scores: Record<string, number>; activeRound: RoundState | null }) {
const sorted = Object.entries(scores).sort((a, b) => b[1] - a[1]);
const maxScore = sorted[0]?.[1] || 1;
const competing = activeRound
? new Set([activeRound.contestants[0].name, activeRound.contestants[1].name])
: new Set<string>();
const judging = activeRound ? new Set(activeRound.votes.map((v) => v.voter.name)) : new Set<string>();
const prompting = activeRound?.prompter.name ?? null;
return (
<aside className="sidebar">
<div className="sidebar__header">STANDINGS</div>
<div className="sidebar__list">
{sorted.map(([name, score], i) => {
const pct = maxScore > 0 ? Math.round((score / maxScore) * 100) : 0;
const color = getColor(name);
const isActive = competing.has(name);
const isJudging = judging.has(name);
const isPrompting = name === prompting;
let role = "";
if (isActive) role = "⚔️";
else if (isPrompting) role = "✍️";
else if (isJudging) role = "🗳️";
return (
<div key={name} className={`standing ${isActive ? "standing--active" : ""}`}>
<div className="standing__rank">{i === 0 && score > 0 ? "👑" : `${i + 1}.`}</div>
<div className="standing__info">
<div className="standing__name-row">
<ModelName model={{id: name, name}} />
{role && <span className="standing__role">{role}</span>}
</div>
<div className="standing__bar-row">
<div className="standing__bar">
<div className="standing__bar-fill" style={{ width: `${pct}%`, background: color }} />
</div>
<span className="standing__score">{score}</span>
</div>
</div>
</div>
);
})}
</div>
{activeRound && (
<div className="sidebar__legend">
<span> COMPETING</span>
<span> PROMPTING</span>
<span>🗳 JUDGING</span>
</div>
)}
</aside>
);
}
function ConnectingScreen() {
return (
<div className="connecting">
<div className="connecting__logo">QUIPSLOP</div>
<div className="connecting__text">Connecting<span className="dots"><span>.</span><span>.</span><span>.</span></span></div>
</div>
);
}
// ── App ─────────────────────────────────────────────────────────────────────
function App() { function App() {
const [state, setState] = useState<GameState | null>(null); const [state, setState] = useState<GameState | null>(null);
const [totalRounds, setTotalRounds] = useState(5); const [totalRounds, setTotalRounds] = useState(5);
const [connected, setConnected] = useState(false); const [connected, setConnected] = useState(false);
const bottomRef = useRef<HTMLDivElement>(null); const mainRef = useRef<HTMLDivElement>(null);
useEffect(() => { useEffect(() => {
const wsUrl = `ws://${window.location.host}/ws`; const wsUrl = `ws://${window.location.host}/ws`;
@@ -237,33 +423,54 @@ function App() {
}, []); }, []);
useEffect(() => { useEffect(() => {
bottomRef.current?.scrollIntoView({ behavior: "smooth" }); if (mainRef.current) {
}, [state]); // Don't auto-scroll aggressively if they are just reading past rounds
// but maybe scroll to top of arena when round changes?
// Leaving this simple for now.
}
}, [state?.active?.num]);
if (!connected || !state) { if (!connected || !state) {
return ( return <ConnectingScreen />;
<div className="connecting">
<span className="connecting-dot"></span> Connecting to Quipslop...
</div>
);
} }
return ( return (
<div> <div className="app">
<div className="header"> <header className="header">
<span className="header-title">QUIPSLOP</span> <h1 className="header__logo">QUIPSLOP</h1>
<div className="header-sub">AI vs AI comedy showdown {totalRounds} rounds</div> <p className="header__tagline">AI vs AI Comedy Showdown</p>
</header>
<div className="layout">
<main className="main" ref={mainRef}>
{state.active && <Arena round={state.active} total={totalRounds} />}
{!state.active && !state.done && state.completed.length > 0 && (
<div className="arena-waiting">
Next round starting<span className="dots"><span>.</span><span>.</span><span>.</span></span>
</div>
)}
{!state.active && !state.done && state.completed.length === 0 && (
<div className="arena-waiting">
Game starting<span className="dots"><span>.</span><span>.</span><span>.</span></span>
</div>
)}
{state.done && <GameOver scores={state.scores} />}
{state.completed.length > 0 && (
<div className="history">
<div className="history__title">PAST ROUNDS</div>
{[...state.completed].reverse().map((round) => (
<PastRoundEntry key={round.num} round={round} />
))}
</div>
)}
</main>
<Sidebar scores={state.scores} activeRound={state.active} />
</div> </div>
{state.completed.map((round) => (
<RoundView key={round.num} round={round} total={totalRounds} />
))}
{state.active && <RoundView round={state.active} total={totalRounds} />}
{state.done && <Scoreboard scores={state.scores} />}
<div ref={bottomRef} />
</div> </div>
); );
} }

View File

@@ -4,6 +4,9 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Quipslop — AI vs AI Comedy Showdown</title> <title>Quipslop — AI vs AI Comedy Showdown</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=DM+Serif+Display&family=Inter:wght@400;500;600;700;900&family=JetBrains+Mono:wght@400;700&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="./frontend.css" /> <link rel="stylesheet" href="./frontend.css" />
</head> </head>
<body> <body>

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="257" preserveAspectRatio="xMidYMid" viewBox="0 0 256 257"><path fill="#D97757" d="m50.228 170.321 50.357-28.257.843-2.463-.843-1.361h-2.462l-8.426-.518-28.775-.778-24.952-1.037-24.175-1.296-6.092-1.297L0 125.796l.583-3.759 5.12-3.434 7.324.648 16.202 1.101 24.304 1.685 17.629 1.037 26.118 2.722h4.148l.583-1.685-1.426-1.037-1.101-1.037-25.147-17.045-27.22-18.017-14.258-10.37-7.713-5.25-3.888-4.925-1.685-10.758 7-7.713 9.397.649 2.398.648 9.527 7.323 20.35 15.75L94.817 91.9l3.889 3.24 1.555-1.102.195-.777-1.75-2.917-14.453-26.118-15.425-26.572-6.87-11.018-1.814-6.61c-.648-2.723-1.102-4.991-1.102-7.778l7.972-10.823L71.42 0 82.05 1.426l4.472 3.888 6.61 15.101 10.694 23.786 16.591 32.34 4.861 9.592 2.592 8.879.973 2.722h1.685v-1.556l1.36-18.211 2.528-22.36 2.463-28.776.843-8.1 4.018-9.722 7.971-5.25 6.222 2.981 5.12 7.324-.713 4.73-3.046 19.768-5.962 30.98-3.889 20.739h2.268l2.593-2.593 10.499-13.934 17.628-22.036 7.778-8.749 9.073-9.657 5.833-4.601h11.018l8.1 12.055-3.628 12.443-11.342 14.388-9.398 12.184-13.48 18.147-8.426 14.518.778 1.166 2.01-.194 30.46-6.481 16.462-2.982 19.637-3.37 8.88 4.148.971 4.213-3.5 8.62-20.998 5.184-24.628 4.926-36.682 8.685-.454.324.519.648 16.526 1.555 7.065.389h17.304l32.21 2.398 8.426 5.574 5.055 6.805-.843 5.184-12.962 6.611-17.498-4.148-40.83-9.721-14-3.5h-1.944v1.167l11.666 11.406 21.387 19.314 26.767 24.887 1.36 6.157-3.434 4.86-3.63-.518-23.526-17.693-9.073-7.972-20.545-17.304h-1.36v1.814l4.73 6.935 25.017 37.59 1.296 11.536-1.814 3.76-6.481 2.268-7.13-1.297-14.647-20.544-15.1-23.138-12.185-20.739-1.49.843-7.194 77.448-3.37 3.953-7.778 2.981-6.48-4.925-3.436-7.972 3.435-15.749 4.148-20.544 3.37-16.333 3.046-20.285 1.815-6.74-.13-.454-1.49.194-15.295 20.999-23.267 31.433-18.406 19.702-4.407 1.75-7.648-3.954.713-7.064 4.277-6.286 25.47-32.405 15.36-20.092 9.917-11.6-.065-1.686h-.583L44.07 198.125l-12.055 1.555-5.185-4.86.648-7.972 2.463-2.593 20.35-13.999-.064.065Z"/></svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" style="flex:none;line-height:1" viewBox="0 0 24 24"><path fill="#4D6BFE" d="M23.748 4.482c-.254-.124-.364.113-.512.234-.051.039-.094.09-.137.136-.372.397-.806.657-1.373.626-.829-.046-1.537.214-2.163.848-.133-.782-.575-1.248-1.247-1.548-.352-.156-.708-.311-.955-.65-.172-.241-.219-.51-.305-.774-.055-.16-.11-.323-.293-.35-.2-.031-.278.136-.356.276-.313.572-.434 1.202-.422 1.84.027 1.436.633 2.58 1.838 3.393.137.093.172.187.129.323-.082.28-.18.552-.266.833-.055.179-.137.217-.329.14a5.526 5.526 0 0 1-1.736-1.18c-.857-.828-1.631-1.742-2.597-2.458a11.365 11.365 0 0 0-.689-.471c-.985-.957.13-1.743.388-1.836.27-.098.093-.432-.779-.428-.872.004-1.67.295-2.687.684a3.055 3.055 0 0 1-.465.137 9.597 9.597 0 0 0-2.883-.102c-1.885.21-3.39 1.102-4.497 2.623C.082 8.606-.231 10.684.152 12.85c.403 2.284 1.569 4.175 3.36 5.653 1.858 1.533 3.997 2.284 6.438 2.14 1.482-.085 3.133-.284 4.994-1.86.47.234.962.327 1.78.397.63.059 1.236-.03 1.705-.128.735-.156.684-.837.419-.961-2.155-1.004-1.682-.595-2.113-.926 1.096-1.296 2.746-2.642 3.392-7.003.05-.347.007-.565 0-.845-.004-.17.035-.237.23-.256a4.173 4.173 0 0 0 1.545-.475c1.396-.763 1.96-2.015 2.093-3.517.02-.23-.004-.467-.247-.588zM11.581 18c-2.089-1.642-3.102-2.183-3.52-2.16-.392.024-.321.471-.235.763.09.288.207.486.371.739.114.167.192.416-.113.603-.673.416-1.842-.14-1.897-.167-1.361-.802-2.5-1.86-3.301-3.307-.774-1.393-1.224-2.887-1.298-4.482-.02-.386.093-.522.477-.592a4.696 4.696 0 0 1 1.529-.039c2.132.312 3.946 1.265 5.468 2.774.868.86 1.525 1.887 2.202 2.891.72 1.066 1.494 2.082 2.48 2.914.348.292.625.514.891.677-.802.09-2.14.11-3.054-.614zm1-6.44a.306.306 0 0 1 .415-.287.302.302 0 0 1 .2.288.306.306 0 0 1-.31.307.303.303 0 0 1-.304-.308zm3.11 1.596c-.2.081-.399.151-.59.16a1.245 1.245 0 0 1-.798-.254c-.274-.23-.47-.358-.552-.758a1.73 1.73 0 0 1 .016-.588c.07-.327-.008-.537-.239-.727-.187-.156-.426-.199-.688-.199a.559.559 0 0 1-.254-.078.253.253 0 0 1-.114-.358c.028-.054.16-.186.192-.21.356-.202.767-.136 1.146.016.352.144.618.408 1.001.782.391.451.462.576.685.914.176.265.336.537.445.848.067.195-.019.354-.25.452z"/></svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@@ -0,0 +1 @@
<svg viewBox="0 0 296 298" xmlns="http://www.w3.org/2000/svg" width="296" height="298" fill="none"><mask id="gemini__a" width="296" height="298" x="0" y="0" maskUnits="userSpaceOnUse" style="mask-type:alpha"><path fill="#3186FF" d="M141.201 4.886c2.282-6.17 11.042-6.071 13.184.148l5.985 17.37a184.004 184.004 0 0 0 111.257 113.049l19.304 6.997c6.143 2.227 6.156 10.91.02 13.155l-19.35 7.082a184.001 184.001 0 0 0-109.495 109.385l-7.573 20.629c-2.241 6.105-10.869 6.121-13.133.025l-7.908-21.296a184 184 0 0 0-109.02-108.658l-19.698-7.239c-6.102-2.243-6.118-10.867-.025-13.132l20.083-7.467A183.998 183.998 0 0 0 133.291 26.28l7.91-21.394Z"/></mask><g mask="url(#gemini__a)"><g filter="url(#gemini__b)"><ellipse cx="163" cy="149" fill="#3689FF" rx="196" ry="159"/></g><g filter="url(#gemini__c)"><ellipse cx="33.5" cy="142.5" fill="#F6C013" rx="68.5" ry="72.5"/></g><g filter="url(#gemini__d)"><ellipse cx="19.5" cy="148.5" fill="#F6C013" rx="68.5" ry="72.5"/></g><g filter="url(#gemini__e)"><path fill="#FA4340" d="M194 10.5C172 82.5 65.5 134.333 22.5 135L144-66l50 76.5Z"/></g><g filter="url(#gemini__f)"><path fill="#FA4340" d="M190.5-12.5C168.5 59.5 62 111.333 19 112L140.5-89l50 76.5Z"/></g><g filter="url(#gemini__g)"><path fill="#14BB69" d="M194.5 279.5C172.5 207.5 66 155.667 23 155l121.5 201 50-76.5Z"/></g><g filter="url(#gemini__h)"><path fill="#14BB69" d="M196.5 320.5C174.5 248.5 68 196.667 25 196l121.5 201 50-76.5Z"/></g></g><defs><filter id="gemini__b" width="464" height="390" x="-69" y="-46" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_69_17998" stdDeviation="18"/></filter><filter id="gemini__c" width="265" height="273" x="-99" y="6" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_69_17998" stdDeviation="32"/></filter><filter id="gemini__d" width="265" height="273" x="-113" y="12" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_69_17998" stdDeviation="32"/></filter><filter id="gemini__e" width="299.5" height="329" x="-41.5" y="-130" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_69_17998" stdDeviation="32"/></filter><filter id="gemini__f" width="299.5" height="329" x="-45" y="-153" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_69_17998" stdDeviation="32"/></filter><filter id="gemini__g" width="299.5" height="329" x="-41" y="91" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_69_17998" stdDeviation="32"/></filter><filter id="gemini__h" width="299.5" height="329" x="-39" y="132" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_69_17998" stdDeviation="32"/></filter></defs></svg>

After

Width:  |  Height:  |  Size: 3.7 KiB

215
public/assets/logos/glm.svg Normal file
View File

@@ -0,0 +1,215 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" x="0px" y="0px" viewBox="0.0 0.0 30.0 30.0" style="enable-background:new 0 0 30 30;" xml:space="preserve" width="316.22776601683796" height="316.22776601683796">
<style type="text/css">
.st0{opacity:0.3;fill:#E2E4E7;}
.st1{opacity:0.8;fill:#E2E4E7;stroke:#FFFFFF;stroke-width:5;stroke-miterlimit:10;}
.st2{fill:url(#SVGID_1_);}
.st3{fill:none;stroke:#E0E4E9;stroke-width:0.25;stroke-miterlimit:10;}
.st4{fill:none;}
.st5{fill:#9DA1A5;}
.st6{fill-rule:evenodd;clip-rule:evenodd;fill:none;}
.st7{fill-rule:evenodd;clip-rule:evenodd;fill:#DFE2E7;}
.st8{fill-rule:evenodd;clip-rule:evenodd;fill:#CDD4DA;}
.st9{fill-rule:evenodd;clip-rule:evenodd;fill:#B3BCC7;}
.st10{fill-rule:evenodd;clip-rule:evenodd;fill:#9DAAB7;}
.st11{fill-rule:evenodd;clip-rule:evenodd;fill:#8698A8;}
.st12{fill-rule:evenodd;clip-rule:evenodd;fill:url(#SVGID_2_);}
.st13{fill-rule:evenodd;clip-rule:evenodd;fill:url(#SVGID_3_);}
.st14{fill:#1F63EC;}
.st15{fill:#2D2D2D;}
.st16{fill:none;stroke:#E0E4E9;stroke-width:0.5;stroke-miterlimit:10;}
.st17{fill-rule:evenodd;clip-rule:evenodd;fill:url(#SVGID_4_);}
.st18{fill-rule:evenodd;clip-rule:evenodd;fill:url(#SVGID_5_);}
.st19{fill:none;stroke:#677380;stroke-width:0.5;stroke-miterlimit:10;}
.st20{fill:none;stroke:url(#SVGID_6_);stroke-width:2;stroke-miterlimit:10;}
.st21{fill-rule:evenodd;clip-rule:evenodd;fill:url(#SVGID_7_);}
.st22{fill-rule:evenodd;clip-rule:evenodd;fill:url(#SVGID_8_);}
.st23{fill:#FFFFFF;}
.st24{fill-rule:evenodd;clip-rule:evenodd;fill:#2D2D2D;}
.st25{clip-path:url(#SVGID_10_);}
.st26{clip-path:url(#SVGID_12_);}
.st27{fill:url(#SVGID_13_);}
.st28{fill-rule:evenodd;clip-rule:evenodd;fill:url(#SVGID_14_);}
.st29{fill-rule:evenodd;clip-rule:evenodd;fill:url(#SVGID_15_);}
.st30{clip-path:url(#SVGID_17_);}
.st31{clip-path:url(#SVGID_19_);}
.st32{fill:url(#SVGID_20_);}
.st33{fill:none;stroke:url(#SVGID_21_);stroke-width:2;stroke-miterlimit:10;}
.st34{fill-rule:evenodd;clip-rule:evenodd;fill:url(#SVGID_22_);}
.st35{fill-rule:evenodd;clip-rule:evenodd;fill:url(#SVGID_23_);}
.st36{clip-path:url(#SVGID_25_);}
.st37{clip-path:url(#SVGID_27_);}
.st38{fill:url(#SVGID_28_);}
.st39{clip-path:url(#SVGID_30_);}
.st40{clip-path:url(#SVGID_32_);}
.st41{fill:url(#SVGID_33_);}
.st42{fill-rule:evenodd;clip-rule:evenodd;fill:#126EF6;}
.st43{fill-rule:evenodd;clip-rule:evenodd;fill:#FFFFFF;}
.st44{clip-path:url(#SVGID_35_);}
.st45{clip-path:url(#SVGID_37_);}
.st46{fill:url(#SVGID_38_);}
.st47{fill-rule:evenodd;clip-rule:evenodd;fill:#9DA1A5;}
.st48{fill-rule:evenodd;clip-rule:evenodd;fill:url(#SVGID_39_);}
.st49{fill-rule:evenodd;clip-rule:evenodd;fill:url(#SVGID_40_);}
.st50{fill-rule:evenodd;clip-rule:evenodd;fill:url(#SVGID_41_);}
.st51{fill-rule:evenodd;clip-rule:evenodd;fill:url(#SVGID_42_);}
.st52{fill:none;stroke:url(#SVGID_43_);stroke-width:2;stroke-miterlimit:10;}
.st53{fill-rule:evenodd;clip-rule:evenodd;fill:none;stroke:#E0E4E9;stroke-width:0.5;stroke-miterlimit:10;}
.st54{fill-rule:evenodd;clip-rule:evenodd;fill:url(#SVGID_44_);}
.st55{fill-rule:evenodd;clip-rule:evenodd;fill:url(#SVGID_45_);}
.st56{fill-rule:evenodd;clip-rule:evenodd;fill:url(#SVGID_46_);}
.st57{fill-rule:evenodd;clip-rule:evenodd;fill:url(#SVGID_47_);}
.st58{fill-rule:evenodd;clip-rule:evenodd;fill:url(#SVGID_48_);}
.st59{fill-rule:evenodd;clip-rule:evenodd;fill:url(#SVGID_49_);}
.st60{fill-rule:evenodd;clip-rule:evenodd;fill:url(#SVGID_50_);}
.st61{fill-rule:evenodd;clip-rule:evenodd;fill:url(#SVGID_51_);}
.st62{fill-rule:evenodd;clip-rule:evenodd;fill:url(#SVGID_52_);}
.st63{fill-rule:evenodd;clip-rule:evenodd;fill:url(#SVGID_53_);}
.st64{clip-path:url(#SVGID_55_);}
.st65{clip-path:url(#SVGID_57_);}
.st66{fill:url(#SVGID_58_);}
.st67{clip-path:url(#SVGID_60_);}
.st68{clip-path:url(#SVGID_62_);}
.st69{fill:url(#SVGID_63_);}
.st70{fill:none;stroke:url(#SVGID_64_);stroke-width:2;stroke-miterlimit:10;}
.st71{clip-path:url(#SVGID_66_);}
.st72{clip-path:url(#SVGID_68_);}
.st73{fill:url(#SVGID_69_);}
.st74{clip-path:url(#SVGID_71_);}
.st75{clip-path:url(#SVGID_73_);}
.st76{fill:url(#SVGID_74_);}
.st77{clip-path:url(#SVGID_76_);}
.st78{clip-path:url(#SVGID_78_);}
.st79{fill:url(#SVGID_79_);}
.st80{fill-rule:evenodd;clip-rule:evenodd;fill:url(#SVGID_80_);}
.st81{fill-rule:evenodd;clip-rule:evenodd;fill:url(#SVGID_81_);}
.st82{clip-path:url(#SVGID_83_);}
.st83{clip-path:url(#SVGID_85_);}
.st84{fill:url(#SVGID_86_);}
.st85{fill-rule:evenodd;clip-rule:evenodd;fill:url(#SVGID_87_);}
.st86{fill-rule:evenodd;clip-rule:evenodd;fill:url(#SVGID_88_);}
.st87{clip-path:url(#SVGID_90_);}
.st88{clip-path:url(#SVGID_92_);}
.st89{fill:url(#SVGID_93_);}
.st90{fill:none;stroke:url(#SVGID_94_);stroke-width:2;stroke-miterlimit:10;}
.st91{fill-rule:evenodd;clip-rule:evenodd;fill:url(#SVGID_95_);}
.st92{fill-rule:evenodd;clip-rule:evenodd;fill:url(#SVGID_96_);}
.st93{clip-path:url(#SVGID_98_);}
.st94{clip-path:url(#SVGID_100_);}
.st95{fill:url(#SVGID_101_);}
.st96{clip-path:url(#SVGID_103_);}
.st97{clip-path:url(#SVGID_105_);}
.st98{fill:url(#SVGID_106_);}
.st99{clip-path:url(#SVGID_108_);}
.st100{clip-path:url(#SVGID_110_);}
.st101{fill:url(#SVGID_111_);}
.st102{fill:#FFFFFF;stroke:#B3BCC7;stroke-width:0.275;stroke-miterlimit:10;}
.st103{clip-path:url(#SVGID_113_);}
.st104{fill:#FDD138;}
.st105{fill:#FCA62F;}
.st106{fill:#FB7927;}
.st107{fill:#F44B22;}
.st108{fill:#D81915;}
.st109{fill:#2D2D2D;stroke:#FFFFFF;stroke-width:0.3354;stroke-miterlimit:10;}
.st110{fill:none;stroke:#65727F;stroke-width:2;stroke-miterlimit:10;}
.st111{fill:none;stroke:#65727F;stroke-width:0.75;stroke-miterlimit:10;}
.st112{fill:url(#SVGID_114_);}
.st113{fill:#D06C50;}
.st114{fill:#2D2D2D;stroke:#B3BCC7;stroke-width:0.275;stroke-miterlimit:10;}
.st115{opacity:0.2;}
.st116{fill:none;stroke:#677380;stroke-width:0.3564;stroke-miterlimit:10;}
.st117{fill:none;stroke:#677380;stroke-width:0.3564;stroke-miterlimit:10;stroke-dasharray:1.0212,1.0212;}
.st118{fill:none;stroke:#677380;stroke-width:0.3564;stroke-miterlimit:10;stroke-dasharray:1.0205,1.0205;}
.st119{opacity:0.2;fill:none;}
.st120{fill:none;stroke:#677380;stroke-width:0.3689;stroke-miterlimit:10;}
.st121{fill:none;stroke:#677380;stroke-width:0.3689;stroke-miterlimit:10;stroke-dasharray:1.0509,1.0509;}
.st122{opacity:0.3;fill:#1F63EC;}
.st123{fill:#2D2D2D;stroke:#FFFFFF;stroke-width:0.3162;stroke-miterlimit:10;}
.st124{fill:#FFFFFF;stroke:#B3BCC7;stroke-width:0.3162;stroke-miterlimit:10;}
.st125{clip-path:url(#SVGID_118_);}
.st126{fill:url(#SVGID_119_);}
.st127{fill:none;stroke:#DFE2E7;stroke-width:0.75;stroke-miterlimit:10;}
.st128{fill:#9DA1A5;stroke:#FFFFFF;stroke-miterlimit:10;}
.st129{fill:url(#SVGID_120_);}
.st130{fill:none;stroke:#677380;stroke-width:0.75;stroke-miterlimit:10;}
.st131{opacity:0.4;}
.st132{clip-path:url(#SVGID_122_);}
.st133{clip-path:url(#SVGID_124_);}
.st134{fill:url(#SVGID_125_);}
.st135{fill:none;stroke:#8392A3;stroke-width:0.35;stroke-miterlimit:10;}
.st136{fill:none;stroke:#8392A3;stroke-width:0.35;stroke-miterlimit:10;stroke-dasharray:0.9951,0.9951;}
.st137{fill:none;stroke:#8392A3;stroke-width:0.35;stroke-miterlimit:10;stroke-dasharray:1.004,1.004;}
.st138{fill:none;stroke:url(#SVGID_126_);stroke-width:1.5;stroke-miterlimit:10;}
.st139{fill:url(#SVGID_127_);}
.st140{fill:none;stroke:#DDE0E4;stroke-width:0.35;stroke-miterlimit:10;}
.st141{fill:#2D2D2D;stroke:#A9B3BE;stroke-width:0.275;stroke-miterlimit:10;}
.st142{fill-rule:evenodd;clip-rule:evenodd;fill:#126EF4;}
.st143{fill:#FFFFFF;stroke:#B1BAC4;stroke-width:0.275;stroke-miterlimit:10;}
.st144{fill:#CE6C50;}
.st145{fill:#5B5B5B;}
.st146{fill:#8392A3;}
.st147{fill:none;stroke:url(#SVGID_128_);stroke-width:1.5;stroke-miterlimit:10;}
.st148{fill:url(#SVGID_129_);}
.st149{fill:none;stroke:#B5BDC4;stroke-width:0.7;stroke-miterlimit:10;}
.st150{opacity:0.6;fill:none;stroke:#78838E;stroke-width:0.35;stroke-miterlimit:10;}
.st151{opacity:0.2;fill:none;stroke:#8392A3;stroke-width:0.35;stroke-miterlimit:10;stroke-dasharray:1,1;}
.st152{fill:none;stroke:#DDE0E4;stroke-width:0.75;stroke-miterlimit:10;}
.st153{fill:none;stroke:#8392A3;stroke-width:0.5;stroke-miterlimit:10;}
.st154{opacity:0.2;fill:none;stroke:#677380;stroke-width:0.3564;stroke-miterlimit:10;stroke-dasharray:1.0182,1.0182;}
.st155{fill:none;stroke:#DDE0E4;stroke-width:0.765;stroke-miterlimit:10;}
.st156{fill:url(#SVGID_130_);}
.st157{fill:url(#SVGID_131_);}
.st158{fill:#B1BAC4;}
.st159{fill:#CBD1D8;}
.st160{fill:#0B1B2B;}
.st161{fill:#91D119;}
.st162{opacity:0.7;}
.st163{fill:#FFFFFF;stroke:#000000;stroke-width:0.4418;stroke-miterlimit:10;}
.st164{fill:none;stroke:#939CAA;stroke-width:0.2209;stroke-miterlimit:10;}
.st165{fill:none;stroke:#FFFFFF;stroke-width:3.0924;stroke-miterlimit:10;}
.st166{fill:url(#SVGID_132_);}
.st167{fill:none;stroke:url(#SVGID_133_);stroke-width:1.714;stroke-miterlimit:10;}
.st168{fill:url(#SVGID_134_);}
.st169{fill:url(#SVGID_135_);}
.st170{fill:url(#SVGID_136_);}
.st171{fill:url(#SVGID_137_);}
.st172{fill:url(#SVGID_138_);}
.st173{fill:url(#SVGID_139_);}
.st174{fill:url(#SVGID_140_);}
.st175{fill:url(#SVGID_141_);}
.st176{fill:url(#SVGID_142_);}
.st177{fill:url(#SVGID_143_);}
.st178{fill:url(#SVGID_144_);}
.st179{fill:none;stroke:#1F63EC;stroke-width:4;stroke-miterlimit:10;}
.st180{fill:none;stroke:#0B1B2B;stroke-width:4;stroke-miterlimit:10;}
.st181{fill:none;stroke:#677380;stroke-width:0.3989;stroke-miterlimit:10;}
.st182{fill:none;stroke:#677380;stroke-width:0.3989;stroke-miterlimit:10;stroke-dasharray:1.14,1.14;}
.st183{fill:#257AF1;}
.st184{opacity:0.3;fill:#FFFFFF;}
.st185{fill:none;stroke:#98A5B2;stroke-width:4;stroke-miterlimit:10;}
.st186{fill:none;stroke:#65727F;stroke-width:0.3989;stroke-miterlimit:10;}
.st187{fill:none;stroke:#65727F;stroke-width:0.3989;stroke-miterlimit:10;stroke-dasharray:1.14,1.14;}
.st188{fill:none;stroke:#DDDFE4;stroke-width:0.75;stroke-miterlimit:10;}
.st189{fill:#9A9EA2;}
.st190{fill-rule:evenodd;clip-rule:evenodd;fill:#3267AC;}
.st191{fill:#FFFFFF;stroke:#AFB8C3;stroke-width:0.275;stroke-miterlimit:10;}
.st192{fill:#C5694E;}
.st193{fill:#8192A2;}
.st194{fill:#2D2D2D;stroke:#FFFFFF;stroke-width:0.6317;stroke-miterlimit:10;}
</style>
<g id="图层_2">
</g>
<g id="图层_1">
<path class="st194" d="M24.51,28.51H5.49c-2.21,0-4-1.79-4-4V5.49c0-2.21,1.79-4,4-4h19.03c2.21,0,4,1.79,4,4v19.03 C28.51,26.72,26.72,28.51,24.51,28.51z"/>
<g>
<g>
<g>
<g>
<path class="st23" d="M15.47,7.1l-1.3,1.85c-0.2,0.29-0.54,0.47-0.9,0.47h-7.1V7.09C6.16,7.1,15.47,7.1,15.47,7.1z"/>
<polygon class="st23" points="24.3,7.1 13.14,22.91 5.7,22.91 16.86,7.1 "/>
<path class="st23" d="M14.53,22.91l1.31-1.86c0.2-0.29,0.54-0.47,0.9-0.47h7.09v2.33H14.53z"/>
</g>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" fill="#ffff" viewBox="0 0 841.89 595.28"><path d="m557.09 211.99 8.31 326.37h66.56l8.32-445.18zM640.28 56.91H538.72L379.35 284.53l50.78 72.52zM201.61 538.36h101.56l50.79-72.52-50.79-72.53zM201.61 211.99l228.52 326.37h101.56L303.17 211.99z"/></svg>

After

Width:  |  Height:  |  Size: 308 B

View File

@@ -0,0 +1 @@
<svg viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2"><path d="M503 114.333v280c0 60.711-49.29 110-110 110H113c-60.711 0-110-49.289-110-110v-280c0-60.71 49.289-110 110-110h280c60.71 0 110 49.29 110 110z"/><path d="M342.065 189.759c1.886-2.42 3.541-4.63 5.289-6.77.81-1.007.74-1.771-.046-2.824-7.58-9.965-8.298-21.028-3.935-32.254 3.275-8.448 10.52-12.406 19.373-13.25 5.52-.521 10.936.046 15.959 2.73 6.596 3.53 10.438 8.912 11.688 16.341.995 5.926.81 11.712-.868 17.452-2.974 10.161-10.277 15.427-20.287 16.758-8.31 1.11-16.734 1.25-25.113 1.817-.648.046-1.308 0-2.06 0z" fill="#027aff"/><path d="M321.512 144.254h-50.064l-39.637 90.384h-56.036v-89.99H131v232.868h44.787v-98.103h78.973c13.598 0 26.015-7.927 31.744-20.252v118.355h44.787v-98.103c0-23.342-18.239-42.97-41.523-44.671v-.116h-24.593a45.577 45.577 0 0026.884-24.534l29.453-65.838z" fill="#fff"/></svg>

After

Width:  |  Height:  |  Size: 957 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="260" preserveAspectRatio="xMidYMid" viewBox="0 0 256 260"><path fill="#fff" d="M239.184 106.203a64.716 64.716 0 0 0-5.576-53.103C219.452 28.459 191 15.784 163.213 21.74A65.586 65.586 0 0 0 52.096 45.22a64.716 64.716 0 0 0-43.23 31.36c-14.31 24.602-11.061 55.634 8.033 76.74a64.665 64.665 0 0 0 5.525 53.102c14.174 24.65 42.644 37.324 70.446 31.36a64.72 64.72 0 0 0 48.754 21.744c28.481.025 53.714-18.361 62.414-45.481a64.767 64.767 0 0 0 43.229-31.36c14.137-24.558 10.875-55.423-8.083-76.483Zm-97.56 136.338a48.397 48.397 0 0 1-31.105-11.255l1.535-.87 51.67-29.825a8.595 8.595 0 0 0 4.247-7.367v-72.85l21.845 12.636c.218.111.37.32.409.563v60.367c-.056 26.818-21.783 48.545-48.601 48.601Zm-104.466-44.61a48.345 48.345 0 0 1-5.781-32.589l1.534.921 51.722 29.826a8.339 8.339 0 0 0 8.441 0l63.181-36.425v25.221a.87.87 0 0 1-.358.665l-52.335 30.184c-23.257 13.398-52.97 5.431-66.404-17.803ZM23.549 85.38a48.499 48.499 0 0 1 25.58-21.333v61.39a8.288 8.288 0 0 0 4.195 7.316l62.874 36.272-21.845 12.636a.819.819 0 0 1-.767 0L41.353 151.53c-23.211-13.454-31.171-43.144-17.804-66.405v.256Zm179.466 41.695-63.08-36.63L161.73 77.86a.819.819 0 0 1 .768 0l52.233 30.184a48.6 48.6 0 0 1-7.316 87.635v-61.391a8.544 8.544 0 0 0-4.4-7.213Zm21.742-32.69-1.535-.922-51.619-30.081a8.39 8.39 0 0 0-8.492 0L99.98 99.808V74.587a.716.716 0 0 1 .307-.665l52.233-30.133a48.652 48.652 0 0 1 72.236 50.391v.205ZM88.061 139.097l-21.845-12.585a.87.87 0 0 1-.41-.614V65.685a48.652 48.652 0 0 1 79.757-37.346l-1.535.87-51.67 29.825a8.595 8.595 0 0 0-4.246 7.367l-.051 72.697Zm11.868-25.58 28.138-16.217 28.188 16.218v32.434l-28.086 16.218-28.188-16.218-.052-32.434Z"/></svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -47,6 +47,11 @@ const server = Bun.serve({
}, },
fetch(req, server) { fetch(req, server) {
const url = new URL(req.url); const url = new URL(req.url);
if (url.pathname.startsWith("/assets/")) {
const path = `./public${url.pathname}`;
const file = Bun.file(path);
return new Response(file);
}
if (url.pathname === "/ws") { if (url.pathname === "/ws") {
const upgraded = server.upgrade(req); const upgraded = server.upgrade(req);
if (!upgraded) { if (!upgraded) {