diff --git a/db.ts b/db.ts index ca17de9..01300b0 100644 --- a/db.ts +++ b/db.ts @@ -189,6 +189,13 @@ export function getCreditByOrder(orderId: string): { } | null; } +/** Insert a user answer directly, bypassing credit checks (admin use). */ +export function insertAdminAnswer(roundNum: number, text: string, username: string): void { + db.prepare( + "INSERT INTO user_answers (round_num, text, username, token) VALUES ($roundNum, $text, $username, 'admin')" + ).run({ $roundNum: roundNum, $text: text, $username: username }); +} + /** * Atomically validates a credit token, records a user answer for the given * round, and decrements the answer budget. Returns null if the token is diff --git a/frontend.tsx b/frontend.tsx index a40bad3..00f3b23 100644 --- a/frontend.tsx +++ b/frontend.tsx @@ -621,6 +621,7 @@ function ProposeAnswer({ activeRound }: { activeRound: RoundState | null }) { const [credit, setCredit] = useState(null); const [loaded, setLoaded] = useState(false); + const [isAdmin, setIsAdmin] = useState(false); const [verifying, setVerifying] = useState(false); const [verifyError, setVerifyError] = useState(false); const [selectedTier, setSelectedTier] = useState(null); @@ -637,6 +638,10 @@ function ProposeAnswer({ activeRound }: { activeRound: RoundState | null }) { useEffect(() => { setCredit(loadCredit()); setLoaded(true); + // Check if admin is logged in (cookie-based, no token needed) + fetch("/api/admin/status") + .then(r => { if (r.ok) setIsAdmin(true); }) + .catch(() => {}); }, []); // Clear submission state when a new round starts @@ -704,28 +709,33 @@ function ProposeAnswer({ activeRound }: { activeRound: RoundState | null }) { async function handleSubmitAnswer(e: React.FormEvent) { e.preventDefault(); - if (!credit || !activeRound) return; + if (!credit && !isAdmin) return; + if (!activeRound) return; setSubmitError(null); setSubmitting(true); try { const res = await fetch("/api/respuesta/enviar", { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ text: text.trim(), token: credit.token }), + body: JSON.stringify({ text: text.trim(), token: credit?.token ?? "" }), }); if (!res.ok) { if (res.status === 401) { - localStorage.removeItem(CREDIT_STORAGE_KEY); - setCredit(null); + if (!isAdmin) { + localStorage.removeItem(CREDIT_STORAGE_KEY); + setCredit(null); + } throw new Error("Crédito agotado o no válido."); } if (res.status === 409) throw new Error("La ronda aún no tiene pregunta activa."); throw new Error(await res.text() || `Error ${res.status}`); } const data = await res.json() as { ok: boolean; answersLeft: number }; - const updated: CreditInfo = { ...credit, answersLeft: data.answersLeft }; - localStorage.setItem(CREDIT_STORAGE_KEY, JSON.stringify(updated)); - setCredit(updated); + if (credit) { + const updated: CreditInfo = { ...credit, answersLeft: data.answersLeft }; + localStorage.setItem(CREDIT_STORAGE_KEY, JSON.stringify(updated)); + setCredit(updated); + } setSubmittedFor(activeRound.num); setSubmittedText(text.trim()); setText(""); @@ -742,8 +752,8 @@ function ProposeAnswer({ activeRound }: { activeRound: RoundState | null }) { const exhausted = credit !== null && credit.answersLeft <= 0; const hasPrompt = !!(activeRound?.prompt); const alreadySubmitted = submittedFor === activeRound?.num; - const canAnswer = credit && !exhausted && hasPrompt && !alreadySubmitted && - (activeRound?.phase === "answering" || activeRound?.phase === "prompting"); + const phaseOk = activeRound?.phase === "answering" || activeRound?.phase === "prompting"; + const canAnswer = (isAdmin || (credit && !exhausted)) && hasPrompt && !alreadySubmitted && phaseOk; // Verifying payment if (verifying || (creditOkOrder && !credit)) { @@ -828,6 +838,46 @@ function ProposeAnswer({ activeRound }: { activeRound: RoundState | null }) { ); } + // Admin: show answer form without requiring credit + if (isAdmin) { + return ( +
+
+ Responde junto a las IAs + Admin +
+ {alreadySubmitted && submittedText && ( +

✓ Tu respuesta: “{submittedText}”

+ )} + {canAnswer && ( +
+
+