feat(scenario): Custom names for simulations

This commit is contained in:
2026-07-18 14:14:57 +05:30
parent 9223ae3750
commit 896ff6f210
5 changed files with 140 additions and 6 deletions

View File

@@ -38,6 +38,7 @@ export async function startSimulation(input: {
scenario?: string;
playEntity?: string;
providerInstanceId?: string;
customName?: string;
}): Promise<ActionResult> {
try {
const scenarioFile =
@@ -52,6 +53,7 @@ export async function startSimulation(input: {
resolved,
input.playEntity || undefined,
input.providerInstanceId,
input.customName,
);
if (snapshot.status === "error") {
@@ -348,3 +350,21 @@ export async function fetchAvailableModelsForInstance(
inst.endpointUrl,
);
}
export async function renameSimulation(
simId: string,
newName: string,
): Promise<{ ok: true; snapshot: SimSnapshot } | { ok: false; error: string }> {
try {
const snapshot = await simulationManager.rename(simId, newName);
if (!snapshot) {
return { ok: false, error: "Simulation session not found" };
}
return { ok: true, snapshot };
} catch (err) {
return {
ok: false,
error: err instanceof Error ? err.message : "Failed to rename simulation",
};
}
}

View File

@@ -33,6 +33,7 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Input } from "@/components/ui/input";
export function HomeView() {
const router = useRouter();
@@ -59,6 +60,7 @@ export function HomeView() {
const [loadingEntities, setLoadingEntities] = useState(false);
const [selectedEntityForModal, setSelectedEntityForModal] =
useState<string>("");
const [customName, setCustomName] = useState<string>("");
const loadSavedSessions = useCallback(async () => {
try {
@@ -144,6 +146,7 @@ export function HomeView() {
setScenarioForModal(scenario);
setLoadingEntities(true);
setSelectedEntityForModal(""); // Reset selection to Spectator
setCustomName(scenario.name); // Set custom simulation name default
try {
const res = await getScenarioEntities(scenario.path);
if (res.ok) {
@@ -168,6 +171,7 @@ export function HomeView() {
const result = await startSimulation({
scenario: targetScenario.path,
playEntity: selectedEntityForModal || undefined,
customName: customName.trim() || undefined,
});
if (!result.ok) {
@@ -394,6 +398,16 @@ export function HomeView() {
</div>
) : (
<div className="flex flex-col gap-4 py-4">
<div className="flex flex-col gap-2">
<label className="text-xs font-semibold uppercase tracking-wider text-muted-foreground font-mono">
Custom Simulation Name
</label>
<Input
value={customName}
onChange={(e) => setCustomName(e.target.value)}
placeholder="Enter custom name..."
/>
</div>
<div className="flex flex-col gap-2">
<label className="text-xs font-semibold uppercase tracking-wider text-muted-foreground font-mono">
Simulation Mode / Play as

View File

@@ -1,13 +1,48 @@
"use client";
import * as React from "react";
import { useState } from "react";
import type { SimSnapshot } from "@/lib/simulation-types";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { renameSimulation } from "@/app/actions";
interface ManageViewProps {
snapshot: SimSnapshot;
onRename: (updated: SimSnapshot) => void;
}
export function ManageView({ snapshot }: ManageViewProps) {
export function ManageView({ snapshot, onRename }: ManageViewProps) {
const [isEditingName, setIsEditingName] = useState(false);
const [editedName, setEditedName] = useState(snapshot.scenarioName);
const [saving, setSaving] = useState(false);
const [error, setError] = useState("");
React.useEffect(() => {
setEditedName(snapshot.scenarioName);
}, [snapshot.scenarioName]);
const handleSaveName = async () => {
if (!editedName.trim()) return;
setSaving(true);
setError("");
try {
const res = await renameSimulation(snapshot.id, editedName.trim());
if (res.ok) {
onRename(res.snapshot);
setIsEditingName(false);
} else {
setError(res.error);
}
} catch (err) {
setError(
err instanceof Error ? err.message : "Failed to rename simulation",
);
} finally {
setSaving(false);
}
};
return (
<main className="flex-1 overflow-y-auto px-8 py-6">
<div className="max-w-[800px] mx-auto space-y-6 pb-12">
@@ -16,7 +51,56 @@ export function ManageView({ snapshot }: ManageViewProps) {
<h3 className="text-headline-sm text-primary mb-4 border-b border-dotted border-border/20 pb-2">
Simulation Info
</h3>
{error && (
<div className="mb-4 border border-destructive bg-destructive/10 px-3 py-2 text-xs text-destructive">
{error}
</div>
)}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 text-sm font-mono">
<div className="flex flex-col gap-1 border-b border-border/10 pb-2 md:col-span-2">
<span className="text-muted-foreground text-xs uppercase tracking-wider">
Simulation Name
</span>
{isEditingName ? (
<div className="flex items-center gap-2 mt-1">
<Input
value={editedName}
onChange={(e) => setEditedName(e.target.value)}
className="h-8 max-w-sm font-sans"
disabled={saving}
/>
<Button size="sm" onClick={handleSaveName} disabled={saving}>
{saving ? "Saving..." : "Save"}
</Button>
<Button
size="sm"
variant="outline"
onClick={() => setIsEditingName(false)}
disabled={saving}
>
Cancel
</Button>
</div>
) : (
<div className="flex items-center gap-3">
<span className="text-foreground font-bold text-base font-head">
{snapshot.scenarioName}
</span>
<Button
size="sm"
variant="outline"
className="h-6 text-[10px]"
onClick={() => {
setEditedName(snapshot.scenarioName);
setIsEditingName(true);
}}
>
Rename
</Button>
</div>
)}
</div>
<div className="flex flex-col gap-1 border-b border-border/10 pb-2">
<span className="text-muted-foreground text-xs uppercase tracking-wider">
Session ID
@@ -29,7 +113,7 @@ export function ManageView({ snapshot }: ManageViewProps) {
<span className="text-muted-foreground text-xs uppercase tracking-wider">
Max Turns
</span>
<span className="text-foreground font-bold">
<span className="text-foreground font-bold font-mono">
{snapshot.maxTurns}
</span>
</div>
@@ -37,13 +121,15 @@ export function ManageView({ snapshot }: ManageViewProps) {
<span className="text-muted-foreground text-xs uppercase tracking-wider">
Turn Count
</span>
<span className="text-foreground font-bold">{snapshot.turn}</span>
<span className="text-foreground font-bold font-mono">
{snapshot.turn}
</span>
</div>
<div className="flex flex-col gap-1 border-b border-border/10 pb-2">
<span className="text-muted-foreground text-xs uppercase tracking-wider">
Entities Registered
</span>
<span className="text-foreground font-bold">
<span className="text-foreground font-bold font-mono">
{snapshot.entities.length}
</span>
</div>

View File

@@ -387,7 +387,7 @@ export function PlayView() {
logEndRef={logEndRef}
/>
) : (
<ManageView snapshot={snapshot} />
<ManageView snapshot={snapshot} onRename={setSnapshot} />
)}
</div>

View File

@@ -36,6 +36,7 @@ export class SimulationManager {
scenarioPath: string,
playEntityName?: string,
providerInstanceId?: string,
customName?: string,
): Promise<SimSnapshot> {
// Resolve or validate the active generative provider upfront so we can
// return a clean error snapshot before touching the filesystem.
@@ -168,7 +169,7 @@ export class SimulationManager {
bufferRepo,
ledgerRepo,
worldInstanceId,
scenarioName: scenarioJson.name,
scenarioName: customName || scenarioJson.name,
scenarioDescription: scenarioJson.description || "",
turn: 1,
maxTurns: 20,
@@ -291,6 +292,19 @@ export class SimulationManager {
return session ? this.snapshot(session) : null;
}
async rename(id: string, newName: string): Promise<SimSnapshot | null> {
let session = this.sessions.get(id);
if (!session) {
await this.load(id);
session = this.sessions.get(id);
}
if (!session) return null;
session.scenarioName = newName;
saveSession(session);
return this.snapshot(session);
}
// ---------------------------------------------------------------------------
// Simulation stepping
// ---------------------------------------------------------------------------