another fix for human choices

This commit is contained in:
Dorian
2026-03-11 00:13:31 +00:00
parent 112bcde515
commit bbe656929c
7 changed files with 42 additions and 22 deletions
@@ -42,6 +42,7 @@ export function useFightPolling(fightId: Ref<string>) {
async function loadFight(): Promise<string | null> { async function loadFight(): Promise<string | null> {
try { try {
const res = await fetch(`/api/fights/${fightId.value}`) const res = await fetch(`/api/fights/${fightId.value}`)
if (res.status === 429) return null // Rate limited, skip this poll
if (res.ok) { if (res.ok) {
const data = await res.json() const data = await res.json()
liveRounds.value = data.rounds?.length || 0 liveRounds.value = data.rounds?.length || 0
+13 -1
View File
@@ -97,11 +97,19 @@ export function useHumanChallenge(
} }
} }
let pollBackoff = 0
async function pollForChallenge() { async function pollForChallenge() {
if (!myBotId.value) return if (!myBotId.value) return
try { try {
const res = await fetch(`/api/fights/${fightId.value}/challenge/${myBotId.value}`) const res = await fetch(`/api/fights/${fightId.value}/challenge/${myBotId.value}`)
if (res.status === 429) {
// Back off on rate limit — skip next few polls
pollBackoff = Math.min(pollBackoff + 2, 8)
return
}
if (!res.ok) return if (!res.ok) return
pollBackoff = Math.max(0, pollBackoff - 1) // Recover gradually
const data = await res.json() const data = await res.json()
if (data.pending) { if (data.pending) {
@@ -130,8 +138,12 @@ export function useHumanChallenge(
function startHumanPolling() { function startHumanPolling() {
if (!myBotId.value) return if (!myBotId.value) return
pollBackoff = 0
void pollForChallenge() void pollForChallenge()
humanPollHandle = setInterval(() => { void pollForChallenge() }, 400) humanPollHandle = setInterval(() => {
if (pollBackoff > 0) { pollBackoff--; return }
void pollForChallenge()
}, 800)
} }
function stopHumanPolling() { function stopHumanPolling() {
+5 -13
View File
@@ -197,19 +197,8 @@ function wireSSE() {
}) })
scrollLiveLog() scrollLiveLog()
}, },
async onHumanChallenge(sseData) { onHumanChallenge(sseData) {
if (myBot.value) { // SSE now includes choices directly — no secondary fetch needed
try {
const res = await fetch(`/api/fights/${fightId.value}/challenge/${myBot.value.id}`)
if (res.ok) {
const fullData = await res.json()
if (fullData.pending) {
handleSSEChallenge(fullData)
return
}
}
} catch { /* fall through */ }
}
handleSSEChallenge(sseData) handleSSEChallenge(sseData)
}, },
onRoundEnd(data) { onRoundEnd(data) {
@@ -539,6 +528,9 @@ async function fightAgain(botId: string) {
liveHpA.value = 100 liveHpA.value = 100
liveHpB.value = 100 liveHpB.value = 100
liveCurrentRound.value = 0 liveCurrentRound.value = 0
// Stop ALL old polling/connections before starting new fight
stopHumanPolling()
stopPolling()
disconnectSSE() disconnectSSE()
try { try {
+9 -1
View File
@@ -123,9 +123,15 @@ function handleKeyboard(e: KeyboardEvent) {
} }
} }
let pollBackoff = 0
function startPolling() { function startPolling() {
pollBackoff = 0
pollForChallenge() pollForChallenge()
pollHandle = setInterval(pollForChallenge, 600) pollHandle = setInterval(() => {
if (pollBackoff > 0) { pollBackoff--; return }
pollForChallenge()
}, 800)
} }
function stopPolling() { function stopPolling() {
@@ -172,7 +178,9 @@ async function pollForChallenge() {
try { try {
const res = await fetch(`/api/fights/${fightId.value}/challenge/${myBotId.value}`) const res = await fetch(`/api/fights/${fightId.value}/challenge/${myBotId.value}`)
if (res.status === 429) { pollBackoff = Math.min(pollBackoff + 2, 8); return }
if (!res.ok) return if (!res.ok) return
pollBackoff = Math.max(0, pollBackoff - 1)
const data = await res.json() const data = await res.json()
// Reset error tracking on success // Reset error tracking on success
+2 -2
View File
@@ -57,7 +57,7 @@ app.use('*', secureHeaders({
scriptSrc: ["'self'", 'blob:'], scriptSrc: ["'self'", 'blob:'],
styleSrc: ["'self'", "'unsafe-inline'", 'https://fonts.googleapis.com'], styleSrc: ["'self'", "'unsafe-inline'", 'https://fonts.googleapis.com'],
imgSrc: ["'self'", 'data:', 'blob:'], imgSrc: ["'self'", 'data:', 'blob:'],
connectSrc: ["'self'", 'https://huggingface.co', 'https://*.huggingface.co'], connectSrc: ["'self'", 'https://huggingface.co', 'https://*.huggingface.co', 'https://*.hf.co'],
fontSrc: ["'self'", 'https://fonts.gstatic.com'], fontSrc: ["'self'", 'https://fonts.gstatic.com'],
workerSrc: ["'self'", 'blob:'], workerSrc: ["'self'", 'blob:'],
} : undefined, } : undefined,
@@ -67,7 +67,7 @@ app.use('*', secureHeaders({
app.use('/api/*', bodyLimit({ maxSize: 256 * 1024 })) app.use('/api/*', bodyLimit({ maxSize: 256 * 1024 }))
// Global rate limit (120/min per IP — generous for polling + signup flows) // Global rate limit (120/min per IP — generous for polling + signup flows)
app.use('/api/*', rateLimit(60_000, 120)) app.use('/api/*', rateLimit(60_000, 300))
// API cache headers // API cache headers
app.use('/api/*', async (c, next) => { app.use('/api/*', async (c, next) => {
+9 -4
View File
@@ -190,6 +190,11 @@ const CREATIVE_POOLS: Record<string, { good: string[]; medium: string[]; bad: st
} }
function generateChoices(challenge: Challenge): string[] { function generateChoices(challenge: Challenge): string[] {
// Use pre-built choices from templateToChallenge if available (hand-picked distractors)
if (challenge.choices && challenge.choices.length >= 2) {
return shuffle([...challenge.choices])
}
if (challenge.scoring === 'factual' && challenge.answers && challenge.answers.length > 0) { if (challenge.scoring === 'factual' && challenge.answers && challenge.answers.length > 0) {
const correct = challenge.answers[0] const correct = challenge.answers[0]
const num = Number(correct) const num = Number(correct)
@@ -237,8 +242,9 @@ export function waitForHumanResponse(
botId: string, botId: string,
challenge: Challenge, challenge: Challenge,
roundNumber: number, roundNumber: number,
): Promise<{ answer: string | null; trashTalk?: string; timedOut: boolean }> { ): { promise: Promise<{ answer: string | null; trashTalk?: string; timedOut: boolean }>; choices: string[] } {
return new Promise((resolve) => { const choices = generateChoices(challenge)
const promise = new Promise<{ answer: string | null; trashTalk?: string; timedOut: boolean }>((resolve) => {
const key = `${fightId}:${botId}` const key = `${fightId}:${botId}`
const timeoutMs = challenge.timeout_ms || DEFAULT_HUMAN_TIMEOUT_MS const timeoutMs = challenge.timeout_ms || DEFAULT_HUMAN_TIMEOUT_MS
@@ -248,8 +254,6 @@ export function waitForHumanResponse(
resolve({ answer: null, timedOut: true }) resolve({ answer: null, timedOut: true })
}, timeoutMs) }, timeoutMs)
const choices = generateChoices(challenge)
pending.set(key, { pending.set(key, {
fightId, fightId,
botId, botId,
@@ -267,6 +271,7 @@ export function waitForHumanResponse(
logger.info('human', `waiting: ${key} round=${roundNumber} type=${challenge.type} choices=${choices.length}`) logger.info('human', `waiting: ${key} round=${roundNumber} type=${challenge.type} choices=${choices.length}`)
}) })
return { promise, choices }
} }
export function submitHumanResponse( export function submitHumanResponse(
+3 -1
View File
@@ -269,7 +269,8 @@ async function getBotResponse(
// before the SSE event tells the frontend to fetch it (fixes race condition // before the SSE event tells the frontend to fetch it (fixes race condition
// where frontend fetched before pending was set → no choices shown) // where frontend fetched before pending was set → no choices shown)
const start = Date.now() const start = Date.now()
const resultPromise = waitForHumanResponse(fightId, bot.id, challenge, roundNumber) const { promise: resultPromise, choices } = waitForHumanResponse(fightId, bot.id, challenge, roundNumber)
// Include choices directly in SSE so frontend never depends on a secondary fetch
emit(fightId, 'human_challenge', { emit(fightId, 'human_challenge', {
botId: bot.id, botId: bot.id,
round: roundNumber, round: roundNumber,
@@ -278,6 +279,7 @@ async function getBotResponse(
prompt: challenge.prompt, prompt: challenge.prompt,
timeoutMs: challenge.timeout_ms, timeoutMs: challenge.timeout_ms,
scoring: challenge.scoring, scoring: challenge.scoring,
choices,
}) })
const result = await resultPromise const result = await resultPromise
const elapsed = Date.now() - start const elapsed = Date.now() - start