diff --git a/apps/gui/src/app/globals.css b/apps/gui/src/app/globals.css index 4521d47..2cb32dc 100644 --- a/apps/gui/src/app/globals.css +++ b/apps/gui/src/app/globals.css @@ -52,6 +52,15 @@ --color-input: var(--input); --color-ring: var(--ring); + --color-sidebar: var(--sidebar); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-ring: var(--sidebar-ring); + --color-surface: var(--surface); --color-surface-dim: var(--surface-dim); --color-surface-bright: var(--surface-bright); @@ -85,6 +94,15 @@ --input: #fff8f5; --ring: #e58e58; + --sidebar: #fcf3e8; + --sidebar-foreground: #201a16; + --sidebar-primary: #e58e58; + --sidebar-primary-foreground: #201a16; + --sidebar-accent: #ffd966; + --sidebar-accent-foreground: #201a16; + --sidebar-border: #5c544e; + --sidebar-ring: #e58e58; + --surface: #fff8f5; --surface-dim: #e4d8d0; --surface-bright: #fff8f5; @@ -117,6 +135,15 @@ --input: #362f2a; --ring: #e58e58; + --sidebar: #201a16; + --sidebar-foreground: #fff8f5; + --sidebar-primary: #e58e58; + --sidebar-primary-foreground: #201a16; + --sidebar-accent: #c1a032; + --sidebar-accent-foreground: #201a16; + --sidebar-border: #877369; + --sidebar-ring: #e58e58; + --surface: #201a16; --surface-dim: #362f2a; --surface-bright: #201a16; diff --git a/apps/gui/src/components/play/PlayView.tsx b/apps/gui/src/components/play/PlayView.tsx index 2d232e4..ee971a9 100644 --- a/apps/gui/src/components/play/PlayView.tsx +++ b/apps/gui/src/components/play/PlayView.tsx @@ -13,6 +13,14 @@ import { Textarea } from "@/components/ui/textarea"; import { Spinner } from "@/components/ui/spinner"; import { PromptModal } from "./PromptModal"; import { cn } from "@/lib/utils"; +import { ChevronLeft } from "lucide-react"; +import { + SidebarProvider, + Sidebar, + SidebarContent, + SidebarTrigger, + useSidebar, +} from "@/components/ui/sidebar"; function IntentTag({ intent, @@ -34,19 +42,22 @@ function IntentTag({ outcome = intent.isValid ? " ✅" : ` ❌ (${intent.reason})`; } - const textToDisplay = (isSelf && intent.selfDescription) - ? intent.selfDescription - : intent.description; + const textToDisplay = + isSelf && intent.selfDescription + ? intent.selfDescription + : intent.description; - const modifiersStr = intent.modifiers && intent.modifiers.length > 0 ? ( - - ({intent.modifiers.join(", ")}) - - ) : null; + const modifiersStr = + intent.modifiers && intent.modifiers.length > 0 ? ( + + ({intent.modifiers.join(", ")}) + + ) : null; return ( - [{label}] “{textToDisplay}”{modifiersStr}{outcome} + [{label}] “{textToDisplay}”{modifiersStr} + {outcome} {intent.minutesToAdvance ? ` [+${intent.minutesToAdvance}min]` : ""} ); @@ -80,18 +91,21 @@ function LogEntryCard({ const showMenu = !!(entry.rawPrompt || entry.decoderPrompt); return ( -
+
- {entry.entityName} + + {entry.entityName} + - Turn {entry.turn} ·{" "} - {formatSimTime(entry.timestamp)} + Turn {entry.turn} · {formatSimTime(entry.timestamp)}
{showMenu && ( @@ -105,7 +119,9 @@ function LogEntryCard({ )}
-
{entry.narrativeProse}
+
+ {entry.narrativeProse} +
{entry.intents.map((intent, i) => ( @@ -114,6 +130,25 @@ function LogEntryCard({
); } +function MobileSidebarClose() { + const { isMobile, setOpenMobile } = useSidebar(); + if (!isMobile) return null; + return ( +
+ + Menu + + +
+ ); +} export function PlayView() { const router = useRouter(); @@ -125,7 +160,9 @@ export function PlayView() { const [playerInput, setPlayerInput] = useState(""); const [error, setError] = useState(""); const [statusText, setStatusText] = useState(""); - const [selectedEntryForModal, setSelectedEntryForModal] = useState(null); + const [selectedEntryForModal, setSelectedEntryForModal] = useState< + SimSnapshot["log"][number] | null + >(null); const logEndRef = useRef(null); const steppingRef = useRef(false); @@ -147,79 +184,79 @@ export function PlayView() { scrollToBottom(); }, [snapshot, scrollToBottom]); - const runSteps = useCallback( + const runSteps = useCallback(async (id: string) => { + if (steppingRef.current) return; + steppingRef.current = true; + setLoading(true); + setError(""); + pauseRequestedRef.current = false; + + try { + let current = snapshotRef.current; + while (true) { + if (pauseRequestedRef.current) { + break; + } + const result = await stepSimulation({ simId: id }); + if (!result.ok) { + setError(result.error); + break; + } + current = result.snapshot; + setSnapshot(current); + + if ( + current.status === "waiting_player" || + current.status === "done" || + current.status === "error" + ) { + break; + } + + const entityName = + current.entities[current.entityIndex ?? 0]?.name || ""; + setStatusText( + `Turn ${current.turn} — processing ${entityName || "next step"}...`, + ); + } + } catch (err) { + setError( + err instanceof Error ? err.message : "Failed during simulation step.", + ); + } finally { + steppingRef.current = false; + setLoading(false); + setStatusText(""); + } + }, []); + + const handleResume = useCallback( async (id: string) => { - if (steppingRef.current) return; - steppingRef.current = true; setLoading(true); setError(""); - pauseRequestedRef.current = false; - try { - let current = snapshotRef.current; - while (true) { - if (pauseRequestedRef.current) { - break; - } - const result = await stepSimulation({ simId: id }); - if (!result.ok) { - setError(result.error); - break; - } - current = result.snapshot; - setSnapshot(current); - - if ( - current.status === "waiting_player" || - current.status === "done" || - current.status === "error" - ) { - break; - } - - const entityName = - current.entities[current.entityIndex ?? 0]?.name || ""; - setStatusText( - `Turn ${current.turn} — processing ${entityName || "next step"}...`, - ); + const res = await resumeSimulation(id); + if (!res.ok) { + setError(res.error); + setLoading(false); + return; + } + setSnapshot(res.snapshot); + if (res.snapshot.status === "running") { + await runSteps(res.snapshot.id); + } else { + setLoading(false); } } catch (err) { setError( - err instanceof Error - ? err.message - : "Failed during simulation step.", + err instanceof Error ? err.message : "Failed to resume session.", ); - } finally { - steppingRef.current = false; setLoading(false); - setStatusText(""); } }, - [], + [runSteps], ); - const handleResume = useCallback(async (id: string) => { - setLoading(true); - setError(""); - try { - const res = await resumeSimulation(id); - if (!res.ok) { - setError(res.error); - setLoading(false); - return; - } - setSnapshot(res.snapshot); - if (res.snapshot.status === "running") { - await runSteps(res.snapshot.id); - } else { - setLoading(false); - } - } catch (err) { - setError(err instanceof Error ? err.message : "Failed to resume session."); - setLoading(false); - } - }, [runSteps]); - // Load simulation on mount useEffect(() => { if (!simId) { @@ -256,11 +293,7 @@ export function PlayView() { await runSteps(result.snapshot.id); } } catch (err) { - setError( - err instanceof Error - ? err.message - : "Failed to submit action.", - ); + setError(err instanceof Error ? err.message : "Failed to submit action."); setLoading(false); } }; @@ -286,7 +319,9 @@ export function PlayView() { return (
- Initializing simulation... + + Initializing simulation... +
); } @@ -305,66 +340,79 @@ export function PlayView() { if (!snapshot) return null; return ( -
- {/* Sidebar Navigation */} - - - {/* Main Content Area */} -
- {/* Sticky Header */} -
-
-
-

{snapshot.scenarioName}

-

{snapshot.scenarioDescription}

+ +
+ {/* Sidebar Navigation */} + + + +
+

+ Simulation +

+
+ + +
+ +
+ +
+
+
+ + {/* Main Content Area */} +
+ {/* Sticky Header */} +
+
+
+ +
+

+ {snapshot.scenarioName} +

+

+ {snapshot.scenarioDescription} +

+
+
{/* Simulation Global Controls */}
{snapshot.status !== "done" && snapshot.status !== "error" && ( <> - {snapshot.status === "running" && ( - loading ? ( + {snapshot.status === "running" && + (loading ? ( - ) - )} + ))}
- Status: {snapshot.status.toUpperCase()} - Turn: {snapshot.turn} + + Status:{" "} + + {snapshot.status.toUpperCase()} + + + + Turn:{" "} + {snapshot.turn} +

{loading && "⏳ "} @@ -435,42 +490,73 @@ export function PlayView() {

{/* Simulation Info */}
-

Simulation Info

+

+ Simulation Info +

- Session ID - {snapshot.id} + + Session ID + + + {snapshot.id} +
- Max Turns - {snapshot.maxTurns} + + Max Turns + + + {snapshot.maxTurns} +
- Turn Count - {snapshot.turn} + + Turn Count + + + {snapshot.turn} +
- Entities Registered - {snapshot.entities.length} + + Entities Registered + + + {snapshot.entities.length} +
{/* Entities Involved */}
-

Entities Involved

+

+ Entities Involved +

{snapshot.entities.map((ent) => ( -
+
- {ent.name} - ID: {ent.id} + + {ent.name} + + + ID: {ent.id} +
{ent.isPlayer ? ( - PLAYER + + PLAYER + ) : ( - NPC + + NPC + )}
@@ -485,18 +571,24 @@ export function PlayView() { {activeTab === "interact" && (