diff --git a/frontend.css b/frontend.css
index c46bc83..9ec4827 100644
--- a/frontend.css
+++ b/frontend.css
@@ -335,6 +335,24 @@ body {
background: rgba(255, 255, 255, 0.06);
}
+/* ── My Vote Highlight ──────────────────────────────────────── */
+
+.contestant--my-vote {
+ background: rgba(255, 255, 255, 0.03);
+ box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.08);
+}
+
+.my-vote-tag {
+ font-family: var(--mono);
+ font-size: 10px;
+ font-weight: 700;
+ letter-spacing: 1px;
+ padding: 3px 8px;
+ border: 1px solid var(--text-muted);
+ color: var(--text-dim);
+ border-radius: 3px;
+}
+
/* ── Viewer Votes ────────────────────────────────────────────── */
.viewer-vote-bar {
@@ -358,25 +376,6 @@ body {
font-size: 14px;
}
-/* ── Vote CTA ────────────────────────────────────────────────── */
-
-.vote-cta {
- text-align: center;
- font-family: var(--mono);
- font-size: 13px;
- font-weight: 600;
- letter-spacing: 0.5px;
- text-transform: uppercase;
- color: var(--text-dim);
- padding: 10px 0;
- margin-bottom: 8px;
- animation: vote-cta-pulse 2s ease-in-out infinite;
-}
-
-@keyframes vote-cta-pulse {
- 0%, 100% { opacity: 0.7; }
- 50% { opacity: 1; }
-}
.vote-countdown {
color: var(--text);
diff --git a/frontend.tsx b/frontend.tsx
index 01eb14b..b7686a1 100644
--- a/frontend.tsx
+++ b/frontend.tsx
@@ -53,7 +53,7 @@ type ViewerCountMessage = {
type: "viewerCount";
viewerCount: number;
};
-type VotedAckMessage = { type: "votedAck" };
+type VotedAckMessage = { type: "votedAck"; votedFor: "A" | "B" };
type ServerMessage = StateMessage | ViewerCountMessage | VotedAckMessage;
// ── Model colors & logos ─────────────────────────────────────────────────────
@@ -163,6 +163,7 @@ function ContestantCard({
totalViewerVotes,
votable,
onVote,
+ isMyVote,
}: {
task: TaskInfo;
voteCount: number;
@@ -174,6 +175,7 @@ function ContestantCard({
totalViewerVotes?: number;
votable?: boolean;
onVote?: () => void;
+ isMyVote?: boolean;
}) {
const color = getColor(task.model.name);
const pct = totalVotes > 0 ? Math.round((voteCount / totalVotes) * 100) : 0;
@@ -184,7 +186,7 @@ function ContestantCard({
return (
+ {isMyVote && YOUR PICK}
{isWinner && WIN}
@@ -277,13 +280,13 @@ function ContestantCard({
function Arena({
round,
total,
- hasVoted,
+ myVote,
onVote,
viewerVotingSecondsLeft,
}: {
round: RoundState;
total: number | null;
- hasVoted: boolean;
+ myVote: "A" | "B" | null;
onVote: (side: "A" | "B") => void;
viewerVotingSecondsLeft: number;
}) {
@@ -304,7 +307,6 @@ function Arena({
const canVote =
round.phase === "voting" &&
- !hasVoted &&
viewerVotingSecondsLeft > 0 &&
round.answerTasks[0].finishedAt &&
round.answerTasks[1].finishedAt;
@@ -337,12 +339,6 @@ function Arena({
- {canVote && (
-
- Pick the funnier answer!
-
- )}
-
{round.phase !== "prompting" && (
onVote("A")}
+ isMyVote={myVote === "A"}
/>
onVote("B")}
+ isMyVote={myVote === "B"}
/>
)}
@@ -492,16 +490,16 @@ function App() {
const [totalRounds, setTotalRounds] = useState
(null);
const [viewerCount, setViewerCount] = useState(0);
const [connected, setConnected] = useState(false);
- const [hasVoted, setHasVoted] = useState(false);
+ const [myVote, setMyVote] = useState<"A" | "B" | null>(null);
const [votedRound, setVotedRound] = useState(null);
const [viewerVotingSecondsLeft, setViewerVotingSecondsLeft] = useState(0);
const wsRef = React.useRef(null);
- // Reset hasVoted when round changes
+ // Reset vote when round changes
useEffect(() => {
const currentRound = state?.active?.num ?? null;
if (currentRound !== null && currentRound !== votedRound) {
- setHasVoted(false);
+ setMyVote(null);
setVotedRound(null);
}
}, [state?.active?.num, votedRound]);
@@ -552,7 +550,7 @@ function App() {
} else if (msg.type === "viewerCount") {
setViewerCount(msg.viewerCount);
} else if (msg.type === "votedAck") {
- setHasVoted(true);
+ setMyVote(msg.votedFor);
}
};
}
@@ -565,8 +563,9 @@ function App() {
}, []);
const handleVote = (side: "A" | "B") => {
- if (hasVoted || !wsRef.current) return;
+ if (myVote === side || !wsRef.current) return;
wsRef.current.send(JSON.stringify({ type: "vote", votedFor: side }));
+ setMyVote(side);
setVotedRound(state?.active?.num ?? null);
};
@@ -607,7 +606,7 @@ function App() {
diff --git a/server.ts b/server.ts
index c207c86..f4f0095 100644
--- a/server.ts
+++ b/server.ts
@@ -218,7 +218,7 @@ function setHistoryCache(key: string, body: string, expiresAt: number) {
// ── WebSocket clients ───────────────────────────────────────────────────────
const clients = new Set>();
-const viewerVoters = new Set>();
+const viewerVoters = new Map, "A" | "B">();
let viewerVoteBroadcastTimer: ReturnType | null = null;
function scheduleViewerVoteBroadcast() {
@@ -611,14 +611,20 @@ const server = Bun.serve({
const round = gameState.active;
if (!round || round.phase !== "voting") return;
if (!round.viewerVotingEndsAt || Date.now() > round.viewerVotingEndsAt) return;
- if (viewerVoters.has(ws)) return;
if (msg.votedFor !== "A" && msg.votedFor !== "B") return;
- viewerVoters.add(ws);
+ const previousVote = viewerVoters.get(ws);
+ if (previousVote === msg.votedFor) return; // same vote, ignore
+
+ // Undo previous vote if changing
+ if (previousVote === "A") round.viewerVotesA = Math.max(0, (round.viewerVotesA ?? 0) - 1);
+ else if (previousVote === "B") round.viewerVotesB = Math.max(0, (round.viewerVotesB ?? 0) - 1);
+
+ viewerVoters.set(ws, msg.votedFor);
if (msg.votedFor === "A") round.viewerVotesA = (round.viewerVotesA ?? 0) + 1;
else round.viewerVotesB = (round.viewerVotesB ?? 0) + 1;
- ws.send(JSON.stringify({ type: "votedAck" }));
+ ws.send(JSON.stringify({ type: "votedAck", votedFor: msg.votedFor }));
scheduleViewerVoteBroadcast();
} catch {}
},