From 607a68fdb5f7d3a2fc27e03659ab89a87e538c78 Mon Sep 17 00:00:00 2001 From: Aditya Gupta Date: Sun, 12 Jul 2026 11:01:46 +0530 Subject: [PATCH] refactor(gui): ScenarioCard as a component --- apps/gui/next-env.d.ts | 2 +- apps/gui/src/app/{play => }/actions.ts | 5 +- apps/gui/src/app/config/page.tsx | 211 +--------- apps/gui/src/app/page.tsx | 360 +----------------- apps/gui/src/components/config/ConfigView.tsx | 212 +++++++++++ .../config/ProviderInstancesConfig.tsx | 2 +- .../gui/src/components/play/DashboardView.tsx | 359 +++++++++++++++++ apps/gui/src/components/play/PlayView.tsx | 2 +- apps/gui/src/components/play/ScenarioCard.tsx | 25 ++ 9 files changed, 606 insertions(+), 572 deletions(-) rename apps/gui/src/app/{play => }/actions.ts (97%) create mode 100644 apps/gui/src/components/config/ConfigView.tsx create mode 100644 apps/gui/src/components/play/DashboardView.tsx create mode 100644 apps/gui/src/components/play/ScenarioCard.tsx diff --git a/apps/gui/next-env.d.ts b/apps/gui/next-env.d.ts index 9edff1c..c4b7818 100644 --- a/apps/gui/next-env.d.ts +++ b/apps/gui/next-env.d.ts @@ -1,6 +1,6 @@ /// /// -import "./.next/types/routes.d.ts"; +import "./.next/dev/types/routes.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/apps/gui/src/app/play/actions.ts b/apps/gui/src/app/actions.ts similarity index 97% rename from apps/gui/src/app/play/actions.ts rename to apps/gui/src/app/actions.ts index 29cfccc..4503301 100644 --- a/apps/gui/src/app/play/actions.ts +++ b/apps/gui/src/app/actions.ts @@ -123,10 +123,10 @@ export async function getConfigStatus(): Promise<{ apiKeySet: boolean; apiKeyPreview: string; model: string; - availableScenarios: { path: string; name: string }[]; + availableScenarios: { path: string; name: string; description: string }[]; }> { const apiKey = process.env.GOOGLE_API_KEY; - const scenarios: { path: string; name: string }[] = []; + const scenarios: { path: string; name: string; description: string }[] = []; const cwd = process.cwd(); const candidates = [ @@ -150,6 +150,7 @@ export async function getConfigStatus(): Promise<{ scenarios.push({ path: `content/demo/scenarios/${file}`, name: content.name || file, + description: content.description || "", }); } catch { /* skip invalid */ diff --git a/apps/gui/src/app/config/page.tsx b/apps/gui/src/app/config/page.tsx index f60f0ca..535cbd3 100644 --- a/apps/gui/src/app/config/page.tsx +++ b/apps/gui/src/app/config/page.tsx @@ -1,212 +1,5 @@ -"use client"; - -import { useEffect, useState, useCallback } from "react"; -import { - getConfigStatus, - listProviderInstances, - getProviderMappings, - setProviderMapping, - getAvailableProviders, - regenerateEmbeddings, -} from "@/app/play/actions"; -import type { ModelProviderInstance, ModelProviderMeta } from "@omnia/llm"; -import { ProviderInstancesConfig } from "@/components/config/ProviderInstancesConfig"; -import { - Select, - SelectContent, - SelectGroup, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "@/components/ui/table"; - -interface ConfigStatus { - apiKeySet: boolean; - apiKeyPreview: string; - model: string; - availableScenarios: { path: string; name: string }[]; -} +import { ConfigView } from "@/components/config/ConfigView"; export default function ConfigPage() { - const [config, setConfig] = useState(null); - const [instances, setInstances] = useState([]); - const [mappings, setMappings] = useState>({}); - const [availableProviders, setAvailableProviders] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(""); - - const loadInstances = useCallback(async () => { - const list = await listProviderInstances(); - setInstances(list); - }, []); - - const loadMappings = useCallback(async () => { - const maps = await getProviderMappings(); - setMappings(maps); - }, []); - - const loadAll = useCallback(async () => { - try { - setLoading(true); - setError(""); - const status = await getConfigStatus(); - setConfig(status); - await loadInstances(); - await loadMappings(); - const provs = await getAvailableProviders(); - setAvailableProviders(provs); - } catch (err) { - setError(err instanceof Error ? err.message : String(err)); - } finally { - setLoading(false); - } - }, [loadInstances, loadMappings]); - - useEffect(() => { - loadAll(); - }, [loadAll]); - - const handleUpdateMapping = async (task: string, providerInstanceId: string) => { - if (task === "embeddings" && mappings[task] !== providerInstanceId) { - const confirmChange = window.confirm( - "Changing the embeddings provider will delete all existing embeddings and regenerate them from scratch. Are you sure you want to do this?" - ); - if (!confirmChange) return; - } - - try { - setLoading(true); - await setProviderMapping(task, providerInstanceId); - if (task === "embeddings") { - await regenerateEmbeddings(providerInstanceId); - } - await loadMappings(); - } catch (err) { - setError(err instanceof Error ? err.message : String(err)); - } finally { - setLoading(false); - } - }; - - return ( -
-

Configuration

- - {config === null && loading &&

Loading configuration...

} - {error && ( -
- {error} -
- )} - - {config && ( -
- { - await loadInstances(); - await loadMappings(); - }} - /> - -
-

Task Provider Routing

-

- Configure which LLM Provider Key Instance should handle each - specific simulation task. Mappings default to the currently{" "} - Active instance if not specified. -

-
- {[ - { key: "actor-prose", label: "Actor Prose Generation", desc: "Generates roleplay/narrative prose for Non-Player Characters.", type: "generative" }, - { key: "llm-validator", label: "LLM Validator", desc: "Arbitrates and validates proposed actions against the world state rules.", type: "generative" }, - { key: "intent-decoder", label: "Intent Decoder", desc: "Splits raw prose actions into structured intents (Player and NPC).", type: "generative" }, - { key: "timedelta", label: "TimeDelta Generator", desc: "Calculates the duration of character actions to advance the game clock.", type: "generative" }, - { key: "handoff", label: "Memory Handoff Engine", desc: "Promotes entities' working memories to the long-term Ledger via LLM summarization and pruning.", type: "generative" }, - { key: "embeddings", label: "Text Embeddings Generator", desc: "Generates vector embeddings for long-term memory retrieval.", type: "embedding" }, - ].map((task) => ( -
-
- - {task.label} - - {task.desc} -
- -
- ))} -
-
- -
-

Available Scenarios

- {config.availableScenarios.length === 0 ? ( -

- No scenarios found in{" "} - - content/demo/scenarios/ - - . -

- ) : ( - - - - Name - Path - - - - {config.availableScenarios.map((s) => ( - - {s.name} - - - {s.path} - - - - ))} - -
- )} -
-
- )} -
- ); + return ; } diff --git a/apps/gui/src/app/page.tsx b/apps/gui/src/app/page.tsx index b267fae..475e4cd 100644 --- a/apps/gui/src/app/page.tsx +++ b/apps/gui/src/app/page.tsx @@ -1,361 +1,5 @@ -"use client"; - -import { useCallback, useEffect, useState } from "react"; -import Link from "next/link"; -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"; +import { DashboardView } from "@/components/play/DashboardView"; 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 ( -
-
- {/* Centered Big Logo */} -
- Omnia 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 && ( - !open && setScenarioForModal(null)}> - - - Start Scenario - - Choose how you want to interact with {scenarioForModal.name}. - - - - {loadingEntities ? ( -
- - Loading scenario entities... -
- ) : ( -
-
- - -
-
- )} - - - - - -
-
- )} -
-
- ); + return ; } diff --git a/apps/gui/src/components/config/ConfigView.tsx b/apps/gui/src/components/config/ConfigView.tsx new file mode 100644 index 0000000..3153137 --- /dev/null +++ b/apps/gui/src/components/config/ConfigView.tsx @@ -0,0 +1,212 @@ +"use client"; + +import { useEffect, useState, useCallback } from "react"; +import { + getConfigStatus, + listProviderInstances, + getProviderMappings, + setProviderMapping, + getAvailableProviders, + regenerateEmbeddings, +} from "@/app/actions"; +import type { ModelProviderInstance, ModelProviderMeta } from "@omnia/llm"; +import { ProviderInstancesConfig } from "@/components/config/ProviderInstancesConfig"; +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; + +interface ConfigStatus { + apiKeySet: boolean; + apiKeyPreview: string; + model: string; + availableScenarios: { path: string; name: string }[]; +} + +export function ConfigView() { + const [config, setConfig] = useState(null); + const [instances, setInstances] = useState([]); + const [mappings, setMappings] = useState>({}); + const [availableProviders, setAvailableProviders] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(""); + + const loadInstances = useCallback(async () => { + const list = await listProviderInstances(); + setInstances(list); + }, []); + + const loadMappings = useCallback(async () => { + const maps = await getProviderMappings(); + setMappings(maps); + }, []); + + const loadAll = useCallback(async () => { + try { + setLoading(true); + setError(""); + const status = await getConfigStatus(); + setConfig(status); + await loadInstances(); + await loadMappings(); + const provs = await getAvailableProviders(); + setAvailableProviders(provs); + } catch (err) { + setError(err instanceof Error ? err.message : String(err)); + } finally { + setLoading(false); + } + }, [loadInstances, loadMappings]); + + useEffect(() => { + loadAll(); + }, [loadAll]); + + const handleUpdateMapping = async (task: string, providerInstanceId: string) => { + if (task === "embeddings" && mappings[task] !== providerInstanceId) { + const confirmChange = window.confirm( + "Changing the embeddings provider will delete all existing embeddings and regenerate them from scratch. Are you sure you want to do this?" + ); + if (!confirmChange) return; + } + + try { + setLoading(true); + await setProviderMapping(task, providerInstanceId); + if (task === "embeddings") { + await regenerateEmbeddings(providerInstanceId); + } + await loadMappings(); + } catch (err) { + setError(err instanceof Error ? err.message : String(err)); + } finally { + setLoading(false); + } + }; + + return ( +
+

Configuration

+ + {config === null && loading &&

Loading configuration...

} + {error && ( +
+ {error} +
+ )} + + {config && ( +
+ { + await loadInstances(); + await loadMappings(); + }} + /> + +
+

Task Provider Routing

+

+ Configure which LLM Provider Key Instance should handle each + specific simulation task. Mappings default to the currently{" "} + Active instance if not specified. +

+
+ {[ + { key: "actor-prose", label: "Actor Prose Generation", desc: "Generates roleplay/narrative prose for Non-Player Characters.", type: "generative" }, + { key: "llm-validator", label: "LLM Validator", desc: "Arbitrates and validates proposed actions against the world state rules.", type: "generative" }, + { key: "intent-decoder", label: "Intent Decoder", desc: "Splits raw prose actions into structured intents (Player and NPC).", type: "generative" }, + { key: "timedelta", label: "TimeDelta Generator", desc: "Calculates the duration of character actions to advance the game clock.", type: "generative" }, + { key: "handoff", label: "Memory Handoff Engine", desc: "Promotes entities' working memories to the long-term Ledger via LLM summarization and pruning.", type: "generative" }, + { key: "embeddings", label: "Text Embeddings Generator", desc: "Generates vector embeddings for long-term memory retrieval.", type: "embedding" }, + ].map((task) => ( +
+
+ + {task.label} + + {task.desc} +
+ +
+ ))} +
+
+ +
+

Available Scenarios

+ {config.availableScenarios.length === 0 ? ( +

+ No scenarios found in{" "} + + content/demo/scenarios/ + + . +

+ ) : ( + + + + Name + Path + + + + {config.availableScenarios.map((s) => ( + + {s.name} + + + {s.path} + + + + ))} + +
+ )} +
+
+ )} +
+ ); +} diff --git a/apps/gui/src/components/config/ProviderInstancesConfig.tsx b/apps/gui/src/components/config/ProviderInstancesConfig.tsx index 6b65810..874aae3 100644 --- a/apps/gui/src/components/config/ProviderInstancesConfig.tsx +++ b/apps/gui/src/components/config/ProviderInstancesConfig.tsx @@ -7,7 +7,7 @@ import { setActiveProviderInstance, regenerateEmbeddings, deleteProviderInstance, -} from "@/app/play/actions"; +} from "@/app/actions"; import type { ModelProviderInstance, ModelProviderMeta } from "@omnia/llm"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; diff --git a/apps/gui/src/components/play/DashboardView.tsx b/apps/gui/src/components/play/DashboardView.tsx new file mode 100644 index 0000000..76a6e79 --- /dev/null +++ b/apps/gui/src/components/play/DashboardView.tsx @@ -0,0 +1,359 @@ +"use client"; + +import { useCallback, useEffect, useState } from "react"; +import Link from "next/link"; +import { useRouter } from "next/navigation"; +import { + startSimulation, + listSavedSimulations, + getConfigStatus, + getScenarioEntities, + deleteSimulation, + listProviderInstances, +} from "@/app/actions"; +import type { SimSnapshot } from "@/lib/simulation-types"; +import type { ModelProviderInstance } from "@omnia/llm"; +import { Button } from "@/components/ui/button"; +import { ScenarioCard } from "@/components/play/ScenarioCard"; +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 function DashboardView() { + 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; description: 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 ( +
+
+ {/* Centered Big Logo */} +
+ Omnia 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)} + /> + ))} +
+ )} +
+ + {/* Start Scenario Selection Modal */} + {scenarioForModal && ( + !open && setScenarioForModal(null)}> + + + Start Scenario + + Choose how you want to interact with {scenarioForModal.name}. + + + + {loadingEntities ? ( +
+ + Loading scenario entities... +
+ ) : ( +
+
+ + +
+
+ )} + + + + + +
+
+ )} +
+
+ ); +} diff --git a/apps/gui/src/components/play/PlayView.tsx b/apps/gui/src/components/play/PlayView.tsx index a1c9538..8221657 100644 --- a/apps/gui/src/components/play/PlayView.tsx +++ b/apps/gui/src/components/play/PlayView.tsx @@ -6,7 +6,7 @@ import { stepSimulation, submitPlayerAction, resumeSimulation, -} from "@/app/play/actions"; +} from "@/app/actions"; import type { SimSnapshot } from "@/lib/simulation-types"; import { Button } from "@/components/ui/button"; import { Textarea } from "@/components/ui/textarea"; diff --git a/apps/gui/src/components/play/ScenarioCard.tsx b/apps/gui/src/components/play/ScenarioCard.tsx new file mode 100644 index 0000000..eeea23b --- /dev/null +++ b/apps/gui/src/components/play/ScenarioCard.tsx @@ -0,0 +1,25 @@ +"use client"; + +function truncate(text: string, maxLen: number): string { + if (text.length <= maxLen) return text; + return text.slice(0, maxLen).trimEnd() + "..."; +} + +interface ScenarioCardProps { + name: string; + description: string; + onClick: () => void; +} + +export function ScenarioCard({ name, description, onClick }: ScenarioCardProps) { + return ( +
+ {name} +

{truncate(description, 80)}

+ {'>'} +
+ ); +}