'use client'; import { randomId } from '@/lib/random-id'; import { useEffect, useId, useRef, useState } from 'react'; import type { GameView } from '@/game/engine'; import { DIFFICULTIES, type Difficulty } from '@/game/bot-profiles'; import { Button } from './ui/button'; import { requestJson, requestMayHaveCompleted } from '@/lib/client-request'; import { parseSeatAiDelegateRequest, seatAiDelegateStorageKey, storeSeatAiDelegateRequest, type SeatAiDelegateRequest, } from '@/lib/seat-ai-delegation'; /** Mounted separately for each authenticated room/seat. No recipient gets a seat key. */ export function SeatAiDelegation({ game, disabled, onPending, onUncertain, onRestored, }: { game: GameView; disabled: boolean; onPending: (value: boolean) => void; onUncertain: (value: boolean) => void; onRestored: (view: GameView) => void; }) { const id = useId(); const pending = useRef(false); const [loaded, setLoaded] = useState(false); const [expanded, setExpanded] = useState(false); const [now, setNow] = useState(Date.now); const [record, setRecord] = useState(null); const [storageProblem, setStorageProblem] = useState(false); const [busy, setBusy] = useState(false); const [message, setMessage] = useState(''); const [abandon, setAbandon] = useState(false); const [delegateId, setDelegateId] = useState(''); const [difficulty, setDifficulty] = useState('Medium'); const key = seatAiDelegateStorageKey(game.code, game.me); useEffect(() => { try { const saved = sessionStorage.getItem(key); if (saved !== null) { // oxlint-disable-next-line react/react-compiler -- Restore this seat's exact pending request after SSR. setRecord(parseSeatAiDelegateRequest(saved)); setExpanded(true); } } catch { setStorageProblem(true); setExpanded(true); setMessage( 'The saved AI permission request could not be read. Restore access to tab storage and reload, or discard the record below.', ); } setLoaded(true); }, [key]); useEffect(() => { const timer = setInterval(() => setNow(Date.now()), 1000); return () => clearInterval(timer); }, []); useEffect(() => { onUncertain(!loaded || !!record || storageProblem); }, [loaded, record, storageProblem, onUncertain]); const own = game.players.find((player) => player.id === game.me); const peers = game.players.filter( (player) => player.id !== game.me && !player.bot, ); const grants = game.seatAiDelegations ?? []; const ownGrant = grants.find((grant) => grant.ownerId === game.me); const received = grants.filter((grant) => grant.delegateId === game.me); const started = ['setup', 'playing'].includes(game.status); const locked = disabled || busy || !loaded; if (!own || own.bot) return null; function store(next: SeatAiDelegateRequest | null) { storeSeatAiDelegateRequest(sessionStorage, key, next); setRecord(next); setStorageProblem(false); } async function submit(request: SeatAiDelegateRequest) { if (locked || pending.current) return; pending.current = true; setBusy(true); setExpanded(true); setAbandon(false); onPending(true); let dispatched = false; try { store(request); dispatched = true; const result = await requestJson<{ view: GameView }>( `/api/rooms/${game.code}/control`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(request), }, ); if ( result.view?.code !== game.code || result.view?.me !== game.me || !Number.isSafeInteger(result.view.version) || result.view.version < request.version || !Array.isArray(result.view.players) ) throw new Error( 'The server did not confirm the AI permission request.', ); onRestored(result.view); store(null); const confirmedGrant = result.view.seatAiDelegations?.find( (grant) => grant.grantId === request.grantId, ); setMessage( request.type === 'setSeatAiDelegate' ? confirmedGrant && confirmedGrant.usedAt === null && confirmedGrant.expiresAt > Date.now() ? 'Permission saved. The named player can start the chosen AI once before its expiry.' : 'The original permission request is confirmed. That permission has already been used, expired or invalidated; it was not renewed.' : request.type === 'revokeSeatAiDelegate' ? 'This permission can no longer start AI. If AI already started, use Take back control. Completed decisions remain.' : 'The permission was used. Check the current AI state at the table; the owner keeps the seat and can take back control.', ); } catch (error) { const uncertain = dispatched && requestMayHaveCompleted(error); if (!dispatched) setStorageProblem(true); if (dispatched && !uncertain) { try { store(null); } catch { setStorageProblem(true); } } setMessage( `${(error as Error).message}${ uncertain ? ' The outcome is uncertain. Retry the exact saved request; it cannot start AI a second time.' : dispatched ? ' No new attempt will be sent automatically. The current table shows any earlier completed change.' : ' No request was sent.' }`, ); } finally { pending.current = false; setBusy(false); onPending(false); } } const name = (playerId: string) => game.players.find((player) => player.id === playerId)?.name ?? 'Player'; return (
setExpanded(event.currentTarget.open)} > Let a player start AI for your seat

Choose another human player who may start your chosen AI once, within 24 hours. They can use this permission even while you are online. They get no access to your hand or seat credentials. You keep your seat, recovery kit and Take back control button.

AI may spend spice, move forces and commit battle plans. Completed decisions cannot be undone. Replacing or revoking permission, changing your own AI control, or recovering or transferring either seat invalidates unused permission.

{record || storageProblem ? (

Confirm the saved request before granting another permission. Refresh keeps the same request in this tab.

{record && ( )}
) : (
{ownGrant && (

{name(ownGrant.delegateId)} · {ownGrant.difficulty} ·{' '} {ownGrant.usedAt !== null ? 'Already used' : ownGrant.expiresAt <= now ? 'Expired' : 'Available once'} .

Expires {new Date(ownGrant.expiresAt).toLocaleString()}.

{ownGrant.usedAt === null && ( )}
)} {started && !own.autopilot && peers.length > 0 && (
)} {!started &&

Permission can be granted after the game starts.

} {own.autopilot && (

Your AI is already running. Use Take back control at the table before granting another permission.

)} {received.map((grant) => (

{name(grant.ownerId)} authorized you to start {grant.difficulty}{' '} AI for their seat once.

Expires {new Date(grant.expiresAt).toLocaleString()}.

{grant.usedAt !== null ? (

Permission already used.

) : grant.expiresAt <= now ? (

Permission expired.

) : ( )}
))}
)} {message && {message}}
); }