feat(gui): Improved responsivity

This commit is contained in:
2026-07-12 13:23:12 +05:30
parent 126da35e88
commit 35ab4b4d40
3 changed files with 297 additions and 170 deletions

View File

@@ -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;

View File

@@ -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 ? (
<span className="italic opacity-80 text-muted-foreground ml-1">
({intent.modifiers.join(", ")})
</span>
) : null;
const modifiersStr =
intent.modifiers && intent.modifiers.length > 0 ? (
<span className="italic opacity-80 text-muted-foreground ml-1">
({intent.modifiers.join(", ")})
</span>
) : null;
return (
<span className="text-sm text-muted-foreground">
[{label}] &ldquo;{textToDisplay}&rdquo;{modifiersStr}{outcome}
[{label}] &ldquo;{textToDisplay}&rdquo;{modifiersStr}
{outcome}
{intent.minutesToAdvance ? ` [+${intent.minutesToAdvance}min]` : ""}
</span>
);
@@ -80,18 +91,21 @@ function LogEntryCard({
const showMenu = !!(entry.rawPrompt || entry.decoderPrompt);
return (
<div className={cn(
"border p-4 shadow-[2px_2px_0_0_var(--border)]",
isPlayerCard
? "border-primary bg-surface-container-low"
: "border-border/30 bg-card"
)}>
<div
className={cn(
"border p-4 shadow-[2px_2px_0_0_var(--border)]",
isPlayerCard
? "border-primary bg-surface-container-low"
: "border-border/30 bg-card",
)}
>
<div className="flex justify-between items-center mb-2 border-b border-dotted border-border/20 pb-2">
<div className="flex items-center gap-2">
<strong className="text-body-md font-bold text-foreground">{entry.entityName}</strong>
<strong className="text-body-md font-bold text-foreground">
{entry.entityName}
</strong>
<span className="text-xs text-muted-foreground font-mono">
Turn {entry.turn} &middot;{" "}
{formatSimTime(entry.timestamp)}
Turn {entry.turn} &middot; {formatSimTime(entry.timestamp)}
</span>
</div>
{showMenu && (
@@ -105,7 +119,9 @@ function LogEntryCard({
</Button>
)}
</div>
<div className="text-body-md leading-relaxed mb-3 text-foreground/90 whitespace-pre-wrap">{entry.narrativeProse}</div>
<div className="text-body-md leading-relaxed mb-3 text-foreground/90 whitespace-pre-wrap">
{entry.narrativeProse}
</div>
<div className="flex flex-col gap-1.5 mt-2 border-t border-dotted border-border/10 pt-2">
{entry.intents.map((intent, i) => (
<IntentTag key={i} intent={intent} isSelf={isPlayerCard} />
@@ -114,6 +130,25 @@ function LogEntryCard({
</div>
);
}
function MobileSidebarClose() {
const { isMobile, setOpenMobile } = useSidebar();
if (!isMobile) return null;
return (
<div className="flex justify-between items-center px-6 py-4 border-b border-border/10">
<span className="text-xs font-semibold uppercase tracking-wider text-muted-foreground font-mono">
Menu
</span>
<Button
variant="ghost"
size="icon"
onClick={() => setOpenMobile(false)}
className="h-8 w-8"
>
<ChevronLeft className="h-4 w-4" />
</Button>
</div>
);
}
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<SimSnapshot["log"][number] | null>(null);
const [selectedEntryForModal, setSelectedEntryForModal] = useState<
SimSnapshot["log"][number] | null
>(null);
const logEndRef = useRef<HTMLDivElement>(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 (
<div className="flex flex-col items-center justify-center min-h-[400px] gap-3">
<Spinner />
<span className="text-sm text-muted-foreground font-mono">Initializing simulation...</span>
<span className="text-sm text-muted-foreground font-mono">
Initializing simulation...
</span>
</div>
);
}
@@ -305,66 +340,79 @@ export function PlayView() {
if (!snapshot) return null;
return (
<div className="flex flex-1 min-h-0 overflow-hidden">
{/* Sidebar Navigation */}
<aside className="w-64 border-r border-border/30 bg-card/40 flex flex-col justify-between shrink-0">
<div className="p-6">
<h3 className="text-xs font-semibold uppercase tracking-wider text-muted-foreground font-mono mb-4">Simulation</h3>
<div className="flex flex-col gap-2">
<button
onClick={() => setActiveTab("interact")}
className={cn(
"w-full text-left px-4 py-2.5 text-sm font-medium border transition-all duration-100",
activeTab === "interact"
? "border-primary bg-primary/10 text-primary shadow-[2px_2px_0_0_var(--primary)]"
: "border-border/30 hover:bg-secondary text-foreground"
)}
>
💬 Interact
</button>
<button
onClick={() => setActiveTab("manage")}
className={cn(
"w-full text-left px-4 py-2.5 text-sm font-medium border transition-all duration-100",
activeTab === "manage"
? "border-primary bg-primary/10 text-primary shadow-[2px_2px_0_0_var(--primary)]"
: "border-border/30 hover:bg-secondary text-foreground"
)}
>
Manage
</button>
</div>
</div>
<div className="p-6 border-t border-dotted border-border/20">
<Button
variant="outline"
className="w-full text-xs font-mono"
onClick={() => {
pauseRequestedRef.current = true;
router.push("/");
}}
>
Back to Home
</Button>
</div>
</aside>
{/* Main Content Area */}
<div className="flex-1 flex flex-col h-full overflow-hidden bg-background">
{/* Sticky Header */}
<header className="sticky top-0 bg-background/95 backdrop-blur-xs border-b border-dotted border-border/20 px-8 py-5 z-10 flex flex-col gap-2 shrink-0">
<div className="flex justify-between items-start gap-4">
<div>
<h2 className="text-headline-md text-primary font-head tracking-wide">{snapshot.scenarioName}</h2>
<p className="text-sm text-muted-foreground/90 mt-1 max-w-[550px]">{snapshot.scenarioDescription}</p>
<SidebarProvider>
<div className="flex flex-1 min-h-0 overflow-hidden">
{/* Sidebar Navigation */}
<Sidebar className="border-r border-border/30 bg-card shrink-0">
<SidebarContent className="flex flex-col justify-between h-full bg-card">
<MobileSidebarClose />
<div className="p-6">
<h3 className="text-xs font-semibold uppercase tracking-wider text-muted-foreground font-mono mb-4">
Simulation
</h3>
<div className="flex flex-col gap-2">
<button
onClick={() => setActiveTab("interact")}
className={cn(
"w-full text-left px-4 py-2.5 text-sm font-medium border transition-all duration-100",
activeTab === "interact"
? "border-primary bg-primary/10 text-primary shadow-[2px_2px_0_0_var(--primary)]"
: "border-border/30 hover:bg-secondary text-foreground",
)}
>
Interact
</button>
<button
onClick={() => setActiveTab("manage")}
className={cn(
"w-full text-left px-4 py-2.5 text-sm font-medium border transition-all duration-100",
activeTab === "manage"
? "border-primary bg-primary/10 text-primary shadow-[2px_2px_0_0_var(--primary)]"
: "border-border/30 hover:bg-secondary text-foreground",
)}
>
Manage
</button>
</div>
</div>
<div className="p-6 border-t border-dotted border-border/20">
<Button
variant="outline"
className="w-full text-xs font-mono"
onClick={() => {
pauseRequestedRef.current = true;
router.push("/");
}}
>
Back to Home
</Button>
</div>
</SidebarContent>
</Sidebar>
{/* Main Content Area */}
<div className="flex-1 flex flex-col h-full overflow-hidden bg-background">
{/* Sticky Header */}
<header className="sticky top-0 bg-background/95 backdrop-blur-xs border-b border-dotted border-border/20 px-8 py-5 z-10 flex flex-col gap-2 shrink-0">
<div className="flex justify-between items-start gap-4">
<div className="flex items-center gap-3">
<SidebarTrigger className="md:hidden" />
<div>
<h2 className="text-headline-md text-primary font-head tracking-wide">
{snapshot.scenarioName}
</h2>
<p className="text-sm text-muted-foreground/90 mt-1 max-w-[550px]">
{snapshot.scenarioDescription}
</p>
</div>
</div>
{/* Simulation Global Controls */}
<div className="flex gap-2 shrink-0">
{snapshot.status !== "done" && snapshot.status !== "error" && (
<>
{snapshot.status === "running" && (
loading ? (
{snapshot.status === "running" &&
(loading ? (
<Button
variant="secondary"
size="sm"
@@ -382,8 +430,7 @@ export function PlayView() {
>
Resume
</Button>
)
)}
))}
<Button
variant="destructive"
size="sm"
@@ -399,8 +446,16 @@ export function PlayView() {
</div>
</div>
<div className="flex items-center justify-between text-xs font-mono mt-1 pt-1.5 border-t border-border/10">
<span className="text-muted-foreground">Status: <span className="text-primary font-bold">{snapshot.status.toUpperCase()}</span></span>
<span className="text-muted-foreground">Turn: <span className="text-foreground font-bold">{snapshot.turn}</span></span>
<span className="text-muted-foreground">
Status:{" "}
<span className="text-primary font-bold">
{snapshot.status.toUpperCase()}
</span>
</span>
<span className="text-muted-foreground">
Turn:{" "}
<span className="text-foreground font-bold">{snapshot.turn}</span>
</span>
</div>
<p className="text-xs font-medium text-primary mt-1 font-mono">
{loading && "⏳ "}
@@ -435,42 +490,73 @@ export function PlayView() {
<div className="max-w-[800px] mx-auto space-y-6 pb-12">
{/* Simulation Info */}
<div className="border border-border/30 bg-card p-6 shadow-[2px_2px_0_0_var(--border)]">
<h3 className="text-headline-sm text-primary mb-4 border-b border-dotted border-border/20 pb-2">Simulation Info</h3>
<h3 className="text-headline-sm text-primary mb-4 border-b border-dotted border-border/20 pb-2">
Simulation Info
</h3>
<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">
<span className="text-muted-foreground text-xs uppercase tracking-wider">Session ID</span>
<span className="text-foreground font-bold break-all">{snapshot.id}</span>
<span className="text-muted-foreground text-xs uppercase tracking-wider">
Session ID
</span>
<span className="text-foreground font-bold break-all">
{snapshot.id}
</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">Max Turns</span>
<span className="text-foreground font-bold">{snapshot.maxTurns}</span>
<span className="text-muted-foreground text-xs uppercase tracking-wider">
Max Turns
</span>
<span className="text-foreground font-bold">
{snapshot.maxTurns}
</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">Turn Count</span>
<span className="text-foreground font-bold">{snapshot.turn}</span>
<span className="text-muted-foreground text-xs uppercase tracking-wider">
Turn Count
</span>
<span className="text-foreground font-bold">
{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">{snapshot.entities.length}</span>
<span className="text-muted-foreground text-xs uppercase tracking-wider">
Entities Registered
</span>
<span className="text-foreground font-bold">
{snapshot.entities.length}
</span>
</div>
</div>
</div>
{/* Entities Involved */}
<div className="border border-border/30 bg-card p-6 shadow-[2px_2px_0_0_var(--border)]">
<h3 className="text-headline-sm text-primary mb-4 border-b border-dotted border-border/20 pb-2">Entities Involved</h3>
<h3 className="text-headline-sm text-primary mb-4 border-b border-dotted border-border/20 pb-2">
Entities Involved
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{snapshot.entities.map((ent) => (
<div key={ent.id} className="border border-border/20 bg-secondary/20 p-4 shadow-[1px_1px_0_0_var(--border)] flex justify-between items-center">
<div
key={ent.id}
className="border border-border/20 bg-secondary/20 p-4 shadow-[1px_1px_0_0_var(--border)] flex justify-between items-center"
>
<div>
<strong className="text-sm text-foreground block font-head tracking-wide">{ent.name}</strong>
<span className="text-xs text-muted-foreground font-mono block mt-1">ID: {ent.id}</span>
<strong className="text-sm text-foreground block font-head tracking-wide">
{ent.name}
</strong>
<span className="text-xs text-muted-foreground font-mono block mt-1">
ID: {ent.id}
</span>
</div>
<div className="flex items-center gap-2">
{ent.isPlayer ? (
<span className="bg-primary/20 text-primary border border-primary/30 px-2 py-0.5 text-xs font-mono">PLAYER</span>
<span className="bg-primary/20 text-primary border border-primary/30 px-2 py-0.5 text-xs font-mono">
PLAYER
</span>
) : (
<span className="bg-secondary/60 text-muted-foreground border border-border/20 px-2 py-0.5 text-xs font-mono">NPC</span>
<span className="bg-secondary/60 text-muted-foreground border border-border/20 px-2 py-0.5 text-xs font-mono">
NPC
</span>
)}
</div>
</div>
@@ -485,18 +571,24 @@ export function PlayView() {
{activeTab === "interact" && (
<footer className="sticky bottom-0 bg-background/95 backdrop-blur-xs border-t border-dotted border-border/20 px-8 py-4 z-10 shrink-0">
<div className="max-w-[800px] mx-auto">
{snapshot.status === "waiting_player" && snapshot.waitingEntity ? (
{snapshot.status === "waiting_player" &&
snapshot.waitingEntity ? (
<div className="border border-border/30 bg-card p-4 shadow-[2px_2px_0_0_var(--border)]">
<details className="mb-3">
<summary className="cursor-pointer text-sm font-medium font-head text-primary select-none outline-none">
<strong>Your context as {snapshot.waitingEntity.name}</strong>
<strong>
Your context as {snapshot.waitingEntity.name}
</strong>
</summary>
<pre className="text-xs whitespace-pre-wrap bg-input border border-border/20 p-2 max-h-[150px] overflow-y-auto mt-2 font-mono">
{snapshot.waitingEntity.userContext}
</pre>
</details>
<form onSubmit={handleSubmitAction} className="flex flex-col gap-2">
<form
onSubmit={handleSubmitAction}
className="flex flex-col gap-2"
>
<Textarea
value={playerInput}
onChange={(e) => setPlayerInput(e.target.value)}
@@ -504,15 +596,20 @@ export function PlayView() {
rows={3}
disabled={loading}
/>
<Button type="submit" disabled={loading || !playerInput.trim()}>
<Button
type="submit"
disabled={loading || !playerInput.trim()}
>
{loading ? "Processing..." : "Submit Action"}
</Button>
</form>
</div>
) : (snapshot.status === "done" || snapshot.status === "error") ? (
) : snapshot.status === "done" || snapshot.status === "error" ? (
<div className="flex justify-between items-center bg-card border border-border/30 p-4 shadow-[2px_2px_0_0_var(--border)]">
<span className="text-sm font-mono text-muted-foreground">
{snapshot.status === "error" ? "Simulation finished with an error." : "Simulation complete."}
{snapshot.status === "error"
? "Simulation finished with an error."
: "Simulation complete."}
</span>
<Button
onClick={() => {
@@ -520,7 +617,9 @@ export function PlayView() {
}}
size="sm"
>
{snapshot.status === "error" ? "Back to Dashboard" : "New Simulation"}
{snapshot.status === "error"
? "Back to Dashboard"
: "New Simulation"}
</Button>
</div>
) : null}
@@ -542,5 +641,6 @@ export function PlayView() {
/>
)}
</div>
</SidebarProvider>
);
}

View File

@@ -168,7 +168,7 @@ function Sidebar({
<div
data-slot="sidebar"
className={cn(
"flex h-full w-(--sidebar-width) flex-col bg-sidebar text-sidebar-foreground",
"flex h-full w-(--sidebar-width) flex-col bg-card text-sidebar-foreground",
className
)}
{...props}
@@ -186,7 +186,7 @@ function Sidebar({
data-sidebar="sidebar"
data-slot="sidebar"
data-mobile="true"
className="w-(--sidebar-width) bg-sidebar p-0 text-sidebar-foreground [&>button]:hidden"
className="w-(--sidebar-width) bg-card p-0 text-sidebar-foreground [&>button]:hidden"
style={
{
"--sidebar-width": SIDEBAR_WIDTH_MOBILE,
@@ -241,7 +241,7 @@ function Sidebar({
<div
data-sidebar="sidebar"
data-slot="sidebar-inner"
className="flex size-full flex-col bg-sidebar group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:shadow-sm group-data-[variant=floating]:ring-1 group-data-[variant=floating]:ring-sidebar-border"
className="flex size-full flex-col bg-card group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:shadow-sm group-data-[variant=floating]:ring-1 group-data-[variant=floating]:ring-sidebar-border"
>
{children}
</div>