Better frontend kind of
842
frontend.css
@@ -1,262 +1,706 @@
|
||||
/* ── Reset & Base ─────────────────────────────────────────────── */
|
||||
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
|
||||
:root {
|
||||
--bg: #0d1117;
|
||||
--surface: #161b22;
|
||||
--border: #30363d;
|
||||
--text: #e6edf3;
|
||||
--text-dim: #7d8590;
|
||||
--accent: #58a6ff;
|
||||
|
||||
--cyan: #56d4dd;
|
||||
--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;
|
||||
--bg: #050505;
|
||||
--surface: #0a0a0a;
|
||||
--surface-2: #141414;
|
||||
--border: #222222;
|
||||
--border-light: #333333;
|
||||
--text: #ffffff;
|
||||
--text-dim: #a1a1a1;
|
||||
--text-muted: #555555;
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
font-family: "SF Mono", "Fira Code", "JetBrains Mono", "Cascadia Code", monospace;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
padding: 24px;
|
||||
font-family: 'Inter', -apple-system, sans-serif;
|
||||
font-size: 15px;
|
||||
line-height: 1.5;
|
||||
min-height: 100vh;
|
||||
overflow: hidden;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
#root {
|
||||
max-width: 720px;
|
||||
margin: 0 auto;
|
||||
/* ── App Layout ───────────────────────────────────────────────── */
|
||||
|
||||
.app {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* ── 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 {
|
||||
display: inline-block;
|
||||
background: #8b5cf6;
|
||||
color: #000;
|
||||
.header__logo {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
font-size: 18px;
|
||||
padding: 4px 12px;
|
||||
letter-spacing: -1px;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.header__tagline {
|
||||
color: var(--text-muted);
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.header-sub {
|
||||
color: var(--text-dim);
|
||||
margin-top: 4px;
|
||||
font-size: 13px;
|
||||
}
|
||||
/* ── Layout Grid ──────────────────────────────────────────────── */
|
||||
|
||||
/* ── Round ──────────────────────────────────────────────────────── */
|
||||
|
||||
.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 {
|
||||
.layout {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 8px;
|
||||
padding: 2px 0 2px 16px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.prompt-text {
|
||||
color: var(--yellow);
|
||||
font-weight: 700;
|
||||
padding: 4px 0 4px 16px;
|
||||
font-size: 15px;
|
||||
.main {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 48px 64px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
.dim { color: var(--text-dim); }
|
||||
.bold { font-weight: 700; }
|
||||
.error { color: var(--red); }
|
||||
/* ── Sidebar ──────────────────────────────────────────────────── */
|
||||
|
||||
.answer-text { font-weight: 700; }
|
||||
|
||||
.vote-arrow { color: var(--text-dim); }
|
||||
|
||||
/* ── Result ────────────────────────────────────────────────────── */
|
||||
|
||||
.round-result {
|
||||
margin-top: 8px;
|
||||
padding: 8px 16px;
|
||||
border-top: 1px solid var(--border);
|
||||
border-bottom: 1px solid var(--border);
|
||||
.sidebar {
|
||||
width: 320px;
|
||||
border-left: 1px solid var(--border);
|
||||
background: var(--surface);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.result-winner { font-weight: 700; }
|
||||
.result-detail { color: var(--text-dim); font-size: 13px; padding-left: 16px; }
|
||||
|
||||
/* ── 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;
|
||||
.sidebar__header {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
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;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 3px 0 3px 16px;
|
||||
gap: 16px;
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.score-rank {
|
||||
color: var(--text-dim);
|
||||
min-width: 24px;
|
||||
text-align: right;
|
||||
.standing--active {
|
||||
background: var(--surface-2);
|
||||
}
|
||||
|
||||
.score-name {
|
||||
.standing__rank {
|
||||
width: 20px;
|
||||
text-align: center;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-weight: 700;
|
||||
min-width: 160px;
|
||||
color: var(--text-muted);
|
||||
font-size: 13px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.score-bar-track {
|
||||
width: 200px;
|
||||
height: 14px;
|
||||
background: var(--surface);
|
||||
.standing__info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.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;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.score-bar-fill {
|
||||
.standing__bar-fill {
|
||||
height: 100%;
|
||||
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;
|
||||
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);
|
||||
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;
|
||||
}
|
||||
|
||||
.connecting-dot {
|
||||
animation: blink 1s infinite;
|
||||
.game-over__title {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 4px;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 48px;
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.3; }
|
||||
.game-over__champion {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
/* ── Spinner ───────────────────────────────────────────────────── */
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
.game-over__crown {
|
||||
font-size: 48px;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
display: inline-block;
|
||||
.game-over__name {
|
||||
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);
|
||||
animation: pulse 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.4; }
|
||||
/* ── Connecting ───────────────────────────────────────────────── */
|
||||
|
||||
.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; }
|
||||
}
|
||||
531
frontend.tsx
@@ -5,23 +5,8 @@ import "./frontend.css";
|
||||
// ── Types (mirrors game.ts) ─────────────────────────────────────────────────
|
||||
|
||||
type Model = { id: string; name: string };
|
||||
|
||||
type TaskInfo = {
|
||||
model: Model;
|
||||
startedAt: number;
|
||||
finishedAt?: number;
|
||||
result?: string;
|
||||
error?: string;
|
||||
};
|
||||
|
||||
type VoteInfo = {
|
||||
voter: Model;
|
||||
startedAt: number;
|
||||
finishedAt?: number;
|
||||
votedFor?: Model;
|
||||
error?: boolean;
|
||||
};
|
||||
|
||||
type TaskInfo = { model: Model; startedAt: number; finishedAt?: number; result?: string; error?: string };
|
||||
type VoteInfo = { voter: Model; startedAt: number; finishedAt?: number; votedFor?: Model; error?: boolean };
|
||||
type RoundState = {
|
||||
num: number;
|
||||
phase: "prompting" | "answering" | "voting" | "done";
|
||||
@@ -34,28 +19,35 @@ type RoundState = {
|
||||
scoreA?: 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 = {
|
||||
completed: RoundState[];
|
||||
active: RoundState | null;
|
||||
scores: Record<string, number>;
|
||||
done: boolean;
|
||||
// ── Model Assets & Colors ───────────────────────────────────────────────────
|
||||
|
||||
const MODEL_COLORS: Record<string, string> = {
|
||||
"Gemini 3.1 Pro": "#4285F4",
|
||||
"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 = {
|
||||
type: "state";
|
||||
data: GameState;
|
||||
totalRounds: number;
|
||||
};
|
||||
|
||||
// ── Helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
function modelClass(name: string): string {
|
||||
return "model-" + name.toLowerCase().replace(/[\s.]+/g, "-");
|
||||
function getColor(name: string): string {
|
||||
return MODEL_COLORS[name] ?? "#A1A1A1";
|
||||
}
|
||||
|
||||
function barClass(name: string): string {
|
||||
return "bar-" + name.toLowerCase().replace(/[\s.]+/g, "-");
|
||||
function getLogo(name: string): string | null {
|
||||
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 ──────────────────────────────────────────────────────────────
|
||||
@@ -68,101 +60,97 @@ function Timer({ startedAt, finishedAt }: { startedAt: number; finishedAt?: numb
|
||||
return () => clearInterval(id);
|
||||
}, [finishedAt]);
|
||||
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 }) {
|
||||
return <span className={`bold ${modelClass(model.name)}`}>{model.name}</span>;
|
||||
function ModelName({ model, className = "", showLogo = true }: { model: Model; className?: string, showLogo?: boolean }) {
|
||||
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 }) {
|
||||
const [contA, contB] = round.contestants;
|
||||
function PromptCard({ round }: { round: RoundState }) {
|
||||
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 (
|
||||
<div className="round">
|
||||
<span className="round-header">ROUND {round.num}/{total}</span>
|
||||
<div className="divider">{"─".repeat(50)}</div>
|
||||
<div className="prompt-card">
|
||||
<div className="prompt-card__by">
|
||||
Prompted by <ModelName model={round.prompter} />
|
||||
</div>
|
||||
<div className="prompt-card__text">{round.prompt}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
{/* Prompt */}
|
||||
<div className="phase">
|
||||
<div className="phase-row">
|
||||
<span className="badge badge-prompt">PROMPT</span>
|
||||
<MName model={round.prompter} />
|
||||
{!round.prompt && !round.promptTask.error && (
|
||||
<span className="spinner">writing a prompt...</span>
|
||||
)}
|
||||
<Timer startedAt={round.promptTask.startedAt} finishedAt={round.promptTask.finishedAt} />
|
||||
function ContestantPanel({
|
||||
task,
|
||||
voteCount,
|
||||
totalVotes,
|
||||
isWinner,
|
||||
showVotes,
|
||||
}: {
|
||||
task: TaskInfo;
|
||||
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>
|
||||
{round.promptTask.error && (
|
||||
<div className="phase-row"><span className="error">✗ {round.promptTask.error}</span></div>
|
||||
)}
|
||||
{round.prompt && (
|
||||
<div className="prompt-text">"{round.prompt}"</div>
|
||||
{isWinner && <div className="contestant__winner-badge">WINNER</div>}
|
||||
</div>
|
||||
|
||||
<div className="contestant__answer">
|
||||
{!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">“{task.result}”</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Answers */}
|
||||
{round.phase !== "prompting" && (
|
||||
<div className="phase">
|
||||
<span className="badge badge-answers">ANSWERS</span>
|
||||
{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>
|
||||
)}
|
||||
{showVotes && (
|
||||
<div className="contestant__votes">
|
||||
<div className="vote-bar">
|
||||
<div className="vote-bar__fill" style={{ width: `${pct}%`, background: color }} />
|
||||
</div>
|
||||
<div className="result-detail">
|
||||
<MName model={contA} /> <span className="dim">+{round.scoreA}</span>
|
||||
{" | "}
|
||||
<MName model={contB} /> <span className="dim">+{round.scoreB}</span>
|
||||
<div className="vote-bar__label">
|
||||
<span className="vote-bar__count" style={{ color }}>{voteCount}</span>
|
||||
<span className="vote-bar__pct">{pct}%</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -170,43 +158,241 @@ function RoundView({ round, total }: { round: RoundState; total: number }) {
|
||||
);
|
||||
}
|
||||
|
||||
function Scoreboard({ scores }: { scores: Record<string, number> }) {
|
||||
const sorted = Object.entries(scores).sort((a, b) => b[1] - a[1]);
|
||||
const maxScore = sorted[0]?.[1] || 1;
|
||||
const medals = ["👑", "🥈", "🥉"];
|
||||
function VoteTicker({ votes }: { votes: VoteInfo[] }) {
|
||||
const finishedVotes = votes.filter((v) => v.finishedAt);
|
||||
const pendingVotes = votes.filter((v) => !v.finishedAt);
|
||||
|
||||
return (
|
||||
<div className="scoreboard">
|
||||
<span className="scoreboard-title">FINAL SCORES</span>
|
||||
{sorted.map(([name, score], i) => {
|
||||
const pct = Math.round((score / maxScore) * 100);
|
||||
return (
|
||||
<div key={name} className="score-row">
|
||||
<span className="score-rank">{i + 1}.</span>
|
||||
<span className={`score-name ${modelClass(name)}`}>{name}</span>
|
||||
<div className="score-bar-track">
|
||||
<div className={`score-bar-fill ${barClass(name)}`} style={{ width: `${pct}%` }} />
|
||||
</div>
|
||||
<span className="score-value">{score}</span>
|
||||
{i < 3 && <span className="score-medal">{medals[i]}</span>}
|
||||
<div className="vote-ticker">
|
||||
<div className="vote-ticker__header">
|
||||
<span className="vote-ticker__title">JUDGES</span>
|
||||
<span className="vote-ticker__status">
|
||||
{finishedVotes.length} / {votes.length}
|
||||
</span>
|
||||
</div>
|
||||
<div className="vote-ticker__list">
|
||||
{finishedVotes.map((vote, i) => (
|
||||
<div key={`f-${i}`} className="vote-entry vote-entry--in">
|
||||
<ModelName model={vote.voter} showLogo={false} />
|
||||
<span className="vote-entry__arrow">→</span>
|
||||
{vote.error || !vote.votedFor ? (
|
||||
<span className="vote-entry__error">abstained</span>
|
||||
) : (
|
||||
<ModelName model={vote.votedFor} showLogo={false} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{sorted[0] && sorted[0][1] > 0 && (
|
||||
<div className="winner-banner">
|
||||
🏆 <span className={`bold ${modelClass(sorted[0][0])}`}>{sorted[0][0]}</span>
|
||||
<span className="bold"> is the funniest AI!</span>
|
||||
))}
|
||||
{pendingVotes.map((vote, i) => (
|
||||
<div key={`p-${i}`} className="vote-entry vote-entry--pending">
|
||||
<ModelName model={vote.voter} showLogo={false} />
|
||||
<span className="vote-entry__pending">deliberating…</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’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">“{round.answerTasks[0].result}”</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">“{round.answerTasks[1].result}”</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>
|
||||
);
|
||||
}
|
||||
|
||||
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() {
|
||||
const [state, setState] = useState<GameState | null>(null);
|
||||
const [totalRounds, setTotalRounds] = useState(5);
|
||||
const [connected, setConnected] = useState(false);
|
||||
const bottomRef = useRef<HTMLDivElement>(null);
|
||||
const mainRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const wsUrl = `ws://${window.location.host}/ws`;
|
||||
@@ -237,33 +423,54 @@ function App() {
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
bottomRef.current?.scrollIntoView({ behavior: "smooth" });
|
||||
}, [state]);
|
||||
if (mainRef.current) {
|
||||
// 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) {
|
||||
return (
|
||||
<div className="connecting">
|
||||
<span className="connecting-dot">●</span> Connecting to Quipslop...
|
||||
</div>
|
||||
);
|
||||
return <ConnectingScreen />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="header">
|
||||
<span className="header-title">QUIPSLOP</span>
|
||||
<div className="header-sub">AI vs AI comedy showdown — {totalRounds} rounds</div>
|
||||
<div className="app">
|
||||
<header className="header">
|
||||
<h1 className="header__logo">QUIPSLOP</h1>
|
||||
<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>
|
||||
|
||||
{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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<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" />
|
||||
</head>
|
||||
<body>
|
||||
|
||||
1
public/assets/logos/claude.svg
Normal 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 |
1
public/assets/logos/deepseek.svg
Normal 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 |
1
public/assets/logos/gemini.svg
Normal 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
@@ -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 |
1
public/assets/logos/grok.svg
Normal 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 |
1
public/assets/logos/kimi.svg
Normal 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 |
1
public/assets/logos/openai.svg
Normal 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 |
@@ -47,6 +47,11 @@ const server = Bun.serve({
|
||||
},
|
||||
fetch(req, server) {
|
||||
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") {
|
||||
const upgraded = server.upgrade(req);
|
||||
if (!upgraded) {
|
||||
|
||||