import { FormEvent, useEffect, useMemo, useState } from "react"; type Entity = { id: string; type: string; name: string; attributes: Record; }; type Action = { actorId: string; type: string; targetId?: string; locationId?: string; }; type ValidationResult = { actionIndex: number; success: boolean; reason?: string; message?: string; }; type Turn = { id: string; rawText: string; actions: Action[]; validation: ValidationResult[]; createdAt: number; interpreter?: { interpreterVersion: string; resolutionSource: "deterministic" | "llm" | "hybrid"; minConfidence: number; status: "resolved" | "needs_clarification" | "rejected"; selectedConfidence?: number; candidates?: Array<{ action: Action; confidence: number; rationale?: string; }>; diagnostics: string[]; clarification?: { reasonCode: string; question: string; field?: string; options?: Array<{ id: string; label: string; value: string; }>; }; }; }; type WorldState = { id: string; entities: Record; metadata: Record; createdAt: number; }; type AppSnapshot = { worldState: WorldState; turns: Turn[]; }; type ProcessTurnResponse = { rawText: string; actions: Action[]; validation: ValidationResult[]; worldState: WorldState; interpreter: { interpreterVersion: string; resolutionSource: "deterministic" | "llm" | "hybrid"; minConfidence: number; status: "resolved" | "needs_clarification" | "rejected"; selectedConfidence?: number; candidates: Array<{ action: Action; confidence: number; rationale?: string; }>; diagnostics: string[]; clarification?: { reasonCode: string; question: string; field?: string; options?: Array<{ id: string; label: string; value: string; }>; }; }; }; type RuleCheck = { id: string; description: string; condition: unknown; failReason: string; failMessage: string; }; type ActionRuleSet = { actionType: string; enabled: boolean; checks: RuleCheck[]; }; type SceneRulebook = { id: string; worldId: string; version: number; name: string; description?: string; rules: ActionRuleSet[]; createdAt: number; updatedAt: number; }; type RulebookListItem = { id: string; name: string; version: number; updatedAt: number; }; const starterPrompts = [ "look around", "take key", "take lantern", "give key to groundskeeper", "introduce jeff", "describe groundskeeper as patient", "open door", "move to exit", ]; function formatConfidence(value: number | undefined): string { if (typeof value !== "number") return "n/a"; return `${Math.round(value * 100)}%`; } function formatTurnTime(epochMs: number): string { try { return new Date(epochMs).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" }); } catch { return String(epochMs); } } async function fetchJson(input: RequestInfo, init?: RequestInit): Promise { const response = await fetch(input, init); if (!response.ok) { throw new Error(`Request failed: ${response.status}`); } return response.json() as Promise; } // --------------------------------------------------------------------------- // Rulebook editor component // --------------------------------------------------------------------------- function RulebookEditor(props: { onSaved?: (rulebook: SceneRulebook) => void }) { const [rulebook, setRulebook] = useState(null); const [drafts, setDrafts] = useState>({}); const [parseErrors, setParseErrors] = useState>({}); const [saving, setSaving] = useState(false); const [saveError, setSaveError] = useState(null); const [saved, setSaved] = useState(false); useEffect(() => { void fetchJson("/api/rulebook").then((rb) => { setRulebook(rb); const initial: Record = {}; for (const ruleSet of rb.rules) { initial[ruleSet.actionType] = JSON.stringify(ruleSet.checks, null, 2); } setDrafts(initial); }); }, []); function toggleEnabled(actionType: string) { if (!rulebook) return; setRulebook({ ...rulebook, rules: rulebook.rules.map((r) => r.actionType === actionType ? { ...r, enabled: !r.enabled } : r ), }); } function updateDraft(actionType: string, value: string) { setDrafts((prev) => ({ ...prev, [actionType]: value })); setParseErrors((prev) => { const next = { ...prev }; delete next[actionType]; return next; }); } async function saveRulebook() { if (!rulebook) return; setSaveError(null); setSaved(false); // Validate and apply all drafts. const updatedRules: ActionRuleSet[] = []; const newErrors: Record = {}; for (const ruleSet of rulebook.rules) { const draft = drafts[ruleSet.actionType] ?? JSON.stringify(ruleSet.checks, null, 2); try { const parsedChecks = JSON.parse(draft) as RuleCheck[]; updatedRules.push({ ...ruleSet, checks: parsedChecks }); } catch { newErrors[ruleSet.actionType] = "Invalid JSON"; updatedRules.push(ruleSet); } } if (Object.keys(newErrors).length > 0) { setParseErrors(newErrors); return; } setSaving(true); try { const updated = await fetchJson("/api/rulebook", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ ...rulebook, rules: updatedRules }), }); setRulebook(updated); props.onSaved?.(updated); setSaved(true); setTimeout(() => setSaved(false), 2500); } catch (err) { setSaveError(err instanceof Error ? err.message : "Save failed"); } finally { setSaving(false); } } if (!rulebook) return

Loading rulebook…

; return (

{rulebook.name}

Version {rulebook.version} | Updated {formatTurnTime(rulebook.updatedAt)}

{rulebook.description ? (

{rulebook.description}

) : null}
{saveError ?

{saveError}

: null}
{rulebook.rules.map((ruleSet) => (
{ruleSet.actionType} {ruleSet.checks.length} check{ruleSet.checks.length !== 1 ? "s" : ""}

Edit the checks array below. Each check has: id, description, condition, failReason, failMessage.

For character permissions, use {`{"op":"actorIdIn","allowedIds":["player"]}`} or {`{"op":"actorNameIn","allowedNames":["Player"]}`}.

For conditional creation, use {`{"op":"actionMetadataEq","key":"createIfMissing","value":true}`} together with actor checks.

For transfer ownership checks, use {`{"op":"itemInInventory","itemMetadataKey":"itemId","holderRole":"actor"}`}.

{ruleSet.checks.length > 0 ? (
    {ruleSet.checks.map((check) => (
  • {check.description}
  • ))}
) : (

No checks — action always passes.

)}