-
- Omnia
-
+
- {links.map((link) => (
-
-
-
- {link.label}
-
-
-
- ))}
+ {links.map((link) => {
+ const isActive = pathname === link.href || (link.href !== "/" && pathname?.startsWith(link.href));
+ return (
+
+
+
+ {link.label}
+
+
+
+ );
+ })}
diff --git a/apps/gui/src/app/page.tsx b/apps/gui/src/app/page.tsx
index cc1306c..b267fae 100644
--- a/apps/gui/src/app/page.tsx
+++ b/apps/gui/src/app/page.tsx
@@ -1,35 +1,361 @@
+"use client";
+
+import { useCallback, useEffect, useState } from "react";
import Link from "next/link";
-import { Card, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
+import { useRouter } from "next/navigation";
+import {
+ startSimulation,
+ listSavedSimulations,
+ getConfigStatus,
+ getScenarioEntities,
+ deleteSimulation,
+ listProviderInstances,
+} from "@/app/play/actions";
+import type { SimSnapshot } from "@/lib/simulation-types";
+import type { ModelProviderInstance } from "@omnia/llm";
+import { Button } from "@/components/ui/button";
+import { Spinner } from "@/components/ui/spinner";
+import { Skeleton } from "@/components/ui/skeleton";
+import {
+ Dialog,
+ DialogContent,
+ DialogHeader,
+ DialogTitle,
+ DialogDescription,
+ DialogFooter,
+} from "@/components/ui/dialog";
+import {
+ Select,
+ SelectContent,
+ SelectGroup,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select";
export default function Home() {
+ const router = useRouter();
+ const [loading, setLoading] = useState(false);
+ const [loadingData, setLoadingData] = useState(true);
+ const [error, setError] = useState("");
+
+ const [savedSessions, setSavedSessions] = useState
([]);
+ const [scenarios, setScenarios] = useState<{ path: string; name: string }[]>([]);
+ const [providerInstances, setProviderInstances] = useState([]);
+
+ // Modal State
+ const [scenarioForModal, setScenarioForModal] = useState<{ path: string; name: string } | null>(null);
+ const [modalEntities, setModalEntities] = useState<{ id: string; name: string }[]>([]);
+ const [loadingEntities, setLoadingEntities] = useState(false);
+ const [selectedEntityForModal, setSelectedEntityForModal] = useState("");
+
+ const loadSavedSessions = useCallback(async () => {
+ try {
+ const res = await listSavedSimulations();
+ if (res.ok) {
+ setSavedSessions(res.sessions);
+ }
+ } catch {
+ // ignore
+ }
+ }, []);
+
+ useEffect(() => {
+ let active = true;
+ async function loadAll() {
+ setLoadingData(true);
+ try {
+ const sessionsRes = await listSavedSimulations();
+ if (active && sessionsRes.ok) {
+ setSavedSessions(sessionsRes.sessions);
+ }
+ } catch {}
+
+ try {
+ const configStatus = await getConfigStatus();
+ if (active) {
+ setScenarios(configStatus.availableScenarios);
+ }
+ } catch {}
+
+ try {
+ const providersList = await listProviderInstances();
+ if (active) {
+ setProviderInstances(providersList);
+ }
+ } catch {}
+
+ if (active) {
+ setLoadingData(false);
+ }
+ }
+ loadAll();
+ return () => {
+ active = false;
+ };
+ }, []);
+
+ const handleResume = (id: string) => {
+ router.push(`/play?simId=${id}`);
+ };
+
+ const handleDelete = async (id: string, e: React.MouseEvent) => {
+ e.stopPropagation();
+ if (!confirm("Are you sure you want to delete this simulation session?")) return;
+ setLoading(true);
+ try {
+ const res = await deleteSimulation(id);
+ if (!res.ok) {
+ setError(res.error);
+ } else {
+ await loadSavedSessions();
+ }
+ } catch (err) {
+ setError(err instanceof Error ? err.message : "Failed to delete session.");
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleScenarioClick = async (scenario: { path: string; name: string }) => {
+ setScenarioForModal(scenario);
+ setLoadingEntities(true);
+ setSelectedEntityForModal(""); // Reset selection to Spectator
+ try {
+ const res = await getScenarioEntities(scenario.path);
+ if (res.ok) {
+ setModalEntities(res.entities);
+ } else {
+ setModalEntities([]);
+ }
+ } catch {
+ setModalEntities([]);
+ } finally {
+ setLoadingEntities(false);
+ }
+ };
+
+ const handleStartFromModal = async () => {
+ if (!scenarioForModal) return;
+ setLoading(true);
+ setError("");
+ const targetScenario = scenarioForModal;
+ setScenarioForModal(null); // close modal
+ try {
+ const result = await startSimulation({
+ scenario: targetScenario.path,
+ playEntity: selectedEntityForModal || undefined,
+ });
+
+ if (!result.ok) {
+ setError(result.error);
+ setLoading(false);
+ return;
+ }
+
+ // Navigate to the separate play page with the newly created simulation ID
+ router.push(`/play?simId=${result.snapshot.id}`);
+ } catch (err) {
+ setError(
+ err instanceof Error
+ ? err.message
+ : "Failed to start simulation.",
+ );
+ setLoading(false);
+ }
+ };
+
return (
-
- Omnia GUI
-
- Configuration and gameplay interface for the Omnia simulation engine.
-
-
-
-
-
- Play
-
- Start a simulation and interact with NPCs
-
-
-
-
-
-
-
- Config
-
- Check environment, API keys, and available scenarios
-
-
-
-
+
+
+ {/* Centered Big Logo */}
+
+

+
Simulation Engine
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+ {/* Simulations Section */}
+
+ Simulations
+ {loadingData ? (
+
+ {Array.from({ length: 3 }).map((_, index) => (
+
+ ))}
+
+ ) : savedSessions.length === 0 ? (
+ No saved simulations found. Start a new one below!
+ ) : (
+
+ {savedSessions.map((s) => (
+
handleResume(s.id)}
+ className="flex-shrink-0 w-72 border border-border/30 bg-card p-5 cursor-pointer shadow-sm hover:-translate-y-0.5 hover:shadow-md active:translate-y-0 active:shadow-sm transition-all relative group"
+ >
+
+ {s.scenarioName}
+
+ Turn {s.turn} · {s.entities.length} entities · {s.status}
+
+
+ ID: {s.id.substring(0, 8)}...
+
+
+
+ Resume
+
+
+
+ ))}
+
+ )}
+
+
+ {/* Scenarios Section */}
+
+ Scenarios
+ {loadingData ? (
+
+
+
+
+ Build a scenario
+ Create a custom simulation starting point
+
+
+ +
+ Create New
+
+
+
+ {Array.from({ length: 3 }).map((_, index) => (
+
+ ))}
+
+ ) : (
+
+
+
+
+ Build a scenario
+ Create a custom simulation starting point
+
+
+ +
+ Create New
+
+
+
+ {scenarios.map((s) => (
+
handleScenarioClick(s)}
+ className="flex-shrink-0 w-64 border border-border/30 bg-card p-5 cursor-pointer shadow-sm hover:-translate-y-0.5 hover:shadow-md active:translate-y-0 active:shadow-sm transition-all"
+ >
+ {s.name}
+ {s.path}
+ Click to Start
+
+ ))}
+
+ )}
+
+
+ {/* Start Scenario Selection Modal */}
+ {scenarioForModal && (
+
+ )}
-
+
);
}
diff --git a/apps/gui/src/app/play/page.tsx b/apps/gui/src/app/play/page.tsx
index 9e2dab7..b7d5487 100644
--- a/apps/gui/src/app/play/page.tsx
+++ b/apps/gui/src/app/play/page.tsx
@@ -1,5 +1,16 @@
+"use client";
+
import { PlayView } from "@/components/play/PlayView";
+import { Suspense } from "react";
export default function PlayPage() {
- return
;
+ return (
+
+ Loading...
+
+ }>
+
+
+ );
}
diff --git a/apps/gui/src/components/play/PlayView.tsx b/apps/gui/src/components/play/PlayView.tsx
index 52eee02..a1c9538 100644
--- a/apps/gui/src/components/play/PlayView.tsx
+++ b/apps/gui/src/components/play/PlayView.tsx
@@ -1,32 +1,18 @@
"use client";
import { useCallback, useEffect, useRef, useState } from "react";
+import { useRouter, useSearchParams } from "next/navigation";
import {
- startSimulation,
stepSimulation,
submitPlayerAction,
- listSavedSimulations,
resumeSimulation,
- getConfigStatus,
- getScenarioEntities,
- deleteSimulation,
- listProviderInstances,
} from "@/app/play/actions";
import type { SimSnapshot } from "@/lib/simulation-types";
-import type { ModelProviderInstance } from "@omnia/llm";
import { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/textarea";
import { Spinner } from "@/components/ui/spinner";
import { PromptModal } from "./PromptModal";
import { cn } from "@/lib/utils";
-import {
- Select,
- SelectContent,
- SelectGroup,
- SelectItem,
- SelectTrigger,
- SelectValue,
-} from "@/components/ui/select";
function IntentTag({
intent,
@@ -66,7 +52,6 @@ function IntentTag({
);
}
-
function formatSimTime(isoString: string) {
try {
const d = new Date(isoString);
@@ -131,12 +116,17 @@ function LogEntryCard({
}
export function PlayView() {
+ const router = useRouter();
+ const searchParams = useSearchParams();
+ const simId = searchParams.get("simId");
+
const [snapshot, setSnapshot] = useState(null);
- const [loading, setLoading] = useState(false);
+ const [loading, setLoading] = useState(true);
const [playerInput, setPlayerInput] = useState("");
const [error, setError] = useState("");
const [statusText, setStatusText] = useState("");
const [selectedEntryForModal, setSelectedEntryForModal] = useState(null);
+
const logEndRef = useRef(null);
const steppingRef = useRef(false);
const pauseRequestedRef = useRef(false);
@@ -203,26 +193,7 @@ export function PlayView() {
[snapshot],
);
- const [savedSessions, setSavedSessions] = useState([]);
-
- const loadSavedSessions = useCallback(async () => {
- try {
- const res = await listSavedSimulations();
- if (res.ok) {
- setSavedSessions(res.sessions);
- }
- } catch {
- // ignore
- }
- }, []);
-
- useEffect(() => {
- if (!snapshot) {
- loadSavedSessions();
- }
- }, [snapshot, loadSavedSessions]);
-
- const handleResume = async (id: string) => {
+ const handleResume = useCallback(async (id: string) => {
setLoading(true);
setError("");
try {
@@ -242,114 +213,16 @@ export function PlayView() {
setError(err instanceof Error ? err.message : "Failed to resume session.");
setLoading(false);
}
- };
+ }, [runSteps]);
- const handleDelete = async (id: string, e: React.MouseEvent) => {
- e.stopPropagation();
- if (!confirm("Are you sure you want to delete this simulation session?")) return;
- setLoading(true);
- try {
- const res = await deleteSimulation(id);
- if (!res.ok) {
- setError(res.error);
- } else {
- await loadSavedSessions();
- }
- } catch (err) {
- setError(err instanceof Error ? err.message : "Failed to delete session.");
- } finally {
- setLoading(false);
- }
- };
-
- const [scenarios, setScenarios] = useState<{ path: string; name: string }[]>([]);
- const [selectedScenario, setSelectedScenario] = useState("");
- const [availableEntities, setAvailableEntities] = useState<{ id: string; name: string }[]>([]);
- const [selectedEntity, setSelectedEntity] = useState("");
-
- const [providerInstances, setProviderInstances] = useState([]);
-
- // Load scenarios and provider instances on mount
+ // Load simulation on mount
useEffect(() => {
- async function loadScenariosAndProviders() {
- try {
- const configStatus = await getConfigStatus();
- setScenarios(configStatus.availableScenarios);
- if (configStatus.availableScenarios.length > 0) {
- setSelectedScenario(configStatus.availableScenarios[0].path);
- }
- } catch {
- // ignore
- }
- try {
- const providersList = await listProviderInstances();
- setProviderInstances(providersList);
- } catch {
- // ignore
- }
- }
- loadScenariosAndProviders();
- }, [snapshot]);
-
- // Fetch entities when selectedScenario changes
- useEffect(() => {
- if (!selectedScenario) {
- setAvailableEntities([]);
- setSelectedEntity("");
+ if (!simId) {
+ router.replace("/");
return;
}
- async function loadEntities() {
- try {
- const res = await getScenarioEntities(selectedScenario);
- if (res.ok) {
- setAvailableEntities(res.entities);
- if (res.entities.length > 0) {
- setSelectedEntity(res.entities[0].id);
- } else {
- setSelectedEntity("");
- }
- }
- } catch {
- // ignore
- }
- }
- loadEntities();
- }, [selectedScenario]);
-
- const handleStart = async (e: React.FormEvent) => {
- e.preventDefault();
- setLoading(true);
- setError("");
-
- try {
- const form = new FormData(e.currentTarget);
- const result = await startSimulation({
- scenario: (form.get("scenario") as string) || undefined,
- playEntity: (form.get("playEntity") as string) || undefined,
- });
-
- if (!result.ok) {
- setError(result.error);
- setLoading(false);
- return;
- }
-
- setSnapshot(result.snapshot);
-
- if (result.snapshot.status === "running") {
- await runSteps(result.snapshot.id);
- } else {
- setLoading(false);
- }
- } catch (err) {
- setError(
- err instanceof Error
- ? err.message
- : "Failed to start simulation.",
- );
- setLoading(false);
- }
- };
+ handleResume(simId);
+ }, [simId, handleResume, router]);
const handleSubmitAction = async (e: React.FormEvent) => {
e.preventDefault();
@@ -402,235 +275,162 @@ export function PlayView() {
}
};
+ if (!snapshot && loading) {
+ return (
+
+
+ Initializing simulation...
+
+ );
+ }
+
+ if (!snapshot && error) {
+ return (
+
+
+ {error}
+
+
+
+ );
+ }
+
+ if (!snapshot) return null;
+
return (
-
-
Omnia Play
-
- {!snapshot && (
-