import React, { useCallback, useEffect, useRef, useState } from 'react'; import { Client } from '@gradio/client'; interface Me { username: string | null; member: boolean; } interface Poll { id: string; question: string; options: string[]; counts: number[]; total: number; created_by: string; created_at: string; my_vote: number | null; voters: Record; } type Notice = { kind: 'ok' | 'err'; text: string } | null; type Tab = 'polls' | 'create'; async function callApi(client: unknown, name: string, args: unknown[]): Promise { const c = client as { predict: (n: string, a: unknown[]) => Promise<{ data: unknown[] }> }; const res = await c.predict(name, args); return res.data[0] as T; } const PollApp: React.FC = () => { const clientRef = useRef(null); const [ready, setReady] = useState(false); const [backendError, setBackendError] = useState(null); const [me, setMe] = useState({ username: null, member: false }); const [polls, setPolls] = useState([]); const [busy, setBusy] = useState(false); const [tab, setTab] = useState('polls'); const [editingId, setEditingId] = useState(null); const [question, setQuestion] = useState(''); const [options, setOptions] = useState(['', '']); const [notice, setNotice] = useState(null); const MAX_OPTIONS = 20; const setOption = (idx: number, value: string) => { setOptions((prev) => prev.map((o, i) => (i === idx ? value : o))); }; const addOption = () => { setOptions((prev) => (prev.length >= MAX_OPTIONS ? prev : [...prev, ''])); }; const removeOption = (idx: number) => { setOptions((prev) => (prev.length <= 1 ? prev : prev.filter((_, i) => i !== idx))); }; const refresh = useCallback(async () => { const client = clientRef.current; if (!client) return; try { const data = await callApi<{ me: Me; polls: Poll[] }>(client, '/polls', []); setMe(data.me); setPolls(data.polls); setBackendError(null); } catch { setBackendError('Cannot reach the voting backend.'); } }, []); useEffect(() => { let cancelled = false; (async () => { try { clientRef.current = await Client.connect(window.location.origin + '/gradio'); if (!cancelled) { setReady(true); await refresh(); } } catch { if (!cancelled) setBackendError('Cannot reach the voting backend.'); } })(); return () => { cancelled = true; }; }, [refresh]); const doVote = async (pollId: string, idx: number) => { if (busy || !clientRef.current) return; setBusy(true); try { const data = await callApi<{ ok: boolean; message: string; poll: Poll | null }>( clientRef.current, '/vote', [pollId, idx] ); setNotice({ kind: data.ok ? 'ok' : 'err', text: data.message }); if (data.ok && data.poll) { setPolls((prev) => prev.map((p) => (p.id === data.poll!.id ? data.poll! : p))); } else { await refresh(); } } catch { setNotice({ kind: 'err', text: 'Vote failed — please retry.' }); } finally { setBusy(false); } }; const startEdit = (poll: Poll) => { setEditingId(poll.id); setQuestion(poll.question); setOptions(poll.options.length > 0 ? [...poll.options] : ['', '']); setNotice(null); setTab('create'); }; const cancelEdit = () => { setEditingId(null); setQuestion(''); setOptions(['', '']); setNotice(null); }; const doSubmit = async () => { if (busy || !clientRef.current) return; setBusy(true); const optsArg = options.filter((o) => o.trim()).join('\n'); const api = editingId ? '/edit_poll' : '/create_poll'; const args = editingId ? [editingId, question, optsArg] : [question, optsArg]; try { const data = await callApi<{ ok: boolean; message: string; poll: Poll | null }>( clientRef.current, api, args ); setNotice({ kind: data.ok ? 'ok' : 'err', text: data.message }); if (data.ok) { setEditingId(null); setQuestion(''); setOptions(['', '']); await refresh(); setTab('polls'); } } catch { setNotice({ kind: 'err', text: 'Could not save poll — please retry.' }); } finally { setBusy(false); } }; const doDelete = async (poll: Poll) => { if (busy || !clientRef.current) return; if (!window.confirm(`Delete poll “${poll.question}” and its ${poll.total} vote(s)?`)) return; setBusy(true); try { const data = await callApi<{ ok: boolean; message: string }>( clientRef.current, '/delete_poll', [poll.id] ); setNotice({ kind: data.ok ? 'ok' : 'err', text: data.message }); await refresh(); } catch { setNotice({ kind: 'err', text: 'Could not delete poll — please retry.' }); } finally { setBusy(false); } }; return (