another fix for human choices
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
@@ -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) => {
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user