diff --git a/apps/gui/src/components/builder/EntitiesTab.tsx b/apps/gui/src/components/builder/EntitiesTab.tsx
index fa4cafb..0818bcf 100644
--- a/apps/gui/src/components/builder/EntitiesTab.tsx
+++ b/apps/gui/src/components/builder/EntitiesTab.tsx
@@ -4,6 +4,14 @@ import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Checkbox } from "@/components/ui/checkbox";
+import {
+ Combobox,
+ ComboboxContent,
+ ComboboxEmpty,
+ ComboboxInput,
+ ComboboxItem,
+ ComboboxList,
+} from "@/components/ui/combobox";
import { AttributeEditor } from "./AttributeEditor";
import { Plus, Trash2, ChevronRight } from "lucide-react";
import type { EntityData, MemoryData, LocationData } from "./types";
@@ -104,9 +112,9 @@ export function EntitiesTab({
{/* Right panel: Edit selected entity details */}
{selectedEnt ? (
-
- {/* Column 1: ID, Location, Attributes */}
-
+
+ {/* Entity Configuration Card */}
+
Entity Configuration
@@ -122,23 +130,27 @@ export function EntitiesTab({
-
+
+
+ No locations found.
+
+ {(id: string) => (
+
+ {getLocationDisplayNameById(id, locations)}
+
+ )}
+
+
+
@@ -191,360 +203,354 @@ export function EntitiesTab({
- {/* Column 2: Aliases and Initial Memories */}
-
- {/* Aliases Section */}
-
-
-
- Aliases (Perceptions)
-
-
-
-
- {Object.keys(selectedEnt.aliases || {}).length === 0 ? (
-
- No descriptive aliases configured. Defaults to actual entity
- ID.
-
- ) : (
-
- {Object.entries(selectedEnt.aliases).map(
- ([targetId, aliasText]) => (
-
-
- {getEntityDisplayNameById(targetId, entities)}
-
-
- {
- const copy = [...entities];
- copy[selectedEntIndex].aliases = {
- ...selectedEnt.aliases,
- [targetId]: e.target.value,
- };
- setEntities(copy);
- }}
- className="h-7 text-xs flex-1"
- />
-
-
- ),
- )}
-
- )}
+ {/* Aliases Card */}
+
+
+
+ Aliases (Perceptions)
+
+
- {/* Initial Memories Section */}
-
-
-
- Initial Memories
-
-
-
-
- {!selectedEnt.initialMemories ||
- selectedEnt.initialMemories.length === 0 ? (
-
- No initial memories loaded. Entities will start blank.
-
- ) : (
-
- {selectedEnt.initialMemories.map((mem, memIdx) => (
+ {Object.keys(selectedEnt.aliases || {}).length === 0 ? (
+
+ No descriptive aliases configured. Defaults to actual entity ID.
+
+ ) : (
+
+ {Object.entries(selectedEnt.aliases).map(
+ ([targetId, aliasText]) => (
+
+ {getEntityDisplayNameById(targetId, entities)}
+
+
+
{
+ const copy = [...entities];
+ copy[selectedEntIndex].aliases = {
+ ...selectedEnt.aliases,
+ [targetId]: e.target.value,
+ };
+ setEntities(copy);
+ }}
+ className="h-7 text-xs flex-1"
+ />
-
-
-
-
- Type
-
-
-
-
-
-
- Location
-
-
-
-
-
-
-
- Verbatim Text (originalText)
-
- {
- const copy = [...entities];
- copy[selectedEntIndex].initialMemories[
- memIdx
- ].intent.originalText = e.target.value;
- setEntities(copy);
- }}
- className="h-7 text-xs"
- />
-
-
-
-
- Objective Description
-
- {
- const copy = [...entities];
- copy[selectedEntIndex].initialMemories[
- memIdx
- ].intent.description = e.target.value;
- setEntities(copy);
- }}
- className="h-7 text-xs"
- />
-
-
- {/* Targets multi-select */}
-
-
- Involved Targets
-
-
- {entityIds
- .filter((id) => id !== selectedEnt.id)
- .map((entId) => {
- const isSelected =
- mem.intent.targetIds?.includes(entId);
- return (
-
- );
- })}
-
-
-
- {/* Action Validation outcome */}
- {mem.intent.type === "action" && (
-
-
- {mem.outcome && (
-
-
-
- {
- const copy = [...entities];
- if (
- copy[selectedEntIndex].initialMemories[
- memIdx
- ].outcome
- ) {
- copy[selectedEntIndex].initialMemories[
- memIdx
- ].outcome!.isValid = !!checked;
- setEntities(copy);
- }
- }}
- />
-
-
-
- {
- const copy = [...entities];
- if (
- copy[selectedEntIndex].initialMemories[
- memIdx
- ].outcome
- ) {
- copy[selectedEntIndex].initialMemories[
- memIdx
- ].outcome!.reason = e.target.value;
- setEntities(copy);
- }
- }}
- className="h-6 text-[10px]"
- />
-
-
- )}
-
- )}
- ))}
-
- )}
+ ),
+ )}
+
+ )}
+
+
+ {/* Initial Memories Card */}
+
+
+
+ Initial Memories
+
+
+
+ {!selectedEnt.initialMemories ||
+ selectedEnt.initialMemories.length === 0 ? (
+
+ No initial memories loaded. Entities will start blank.
+
+ ) : (
+
+ {selectedEnt.initialMemories.map((mem, memIdx) => (
+
+
+
+
+
+
+ Type
+
+
+
+
+
+
+ Location
+
+
+
+
+
+
+
+ Verbatim Text (originalText)
+
+ {
+ const copy = [...entities];
+ copy[selectedEntIndex].initialMemories[
+ memIdx
+ ].intent.originalText = e.target.value;
+ setEntities(copy);
+ }}
+ className="h-7 text-xs"
+ />
+
+
+
+
+ Objective Description
+
+ {
+ const copy = [...entities];
+ copy[selectedEntIndex].initialMemories[
+ memIdx
+ ].intent.description = e.target.value;
+ setEntities(copy);
+ }}
+ className="h-7 text-xs"
+ />
+
+
+ {/* Targets multi-select */}
+
+
+ Involved Targets
+
+
+ {entityIds
+ .filter((id) => id !== selectedEnt.id)
+ .map((entId) => {
+ const isSelected =
+ mem.intent.targetIds?.includes(entId);
+ return (
+
+ );
+ })}
+
+
+
+ {/* Action Validation outcome */}
+ {mem.intent.type === "action" && (
+
+
+ {mem.outcome && (
+
+
+
+ {
+ const copy = [...entities];
+ if (
+ copy[selectedEntIndex].initialMemories[
+ memIdx
+ ].outcome
+ ) {
+ copy[selectedEntIndex].initialMemories[
+ memIdx
+ ].outcome!.isValid = !!checked;
+ setEntities(copy);
+ }
+ }}
+ />
+
+
+
+ {
+ const copy = [...entities];
+ if (
+ copy[selectedEntIndex].initialMemories[
+ memIdx
+ ].outcome
+ ) {
+ copy[selectedEntIndex].initialMemories[
+ memIdx
+ ].outcome!.reason = e.target.value;
+ setEntities(copy);
+ }
+ }}
+ className="h-6 text-[10px]"
+ />
+
+
+ )}
+
+ )}
+
+ ))}
+
+ )}
) : (
diff --git a/apps/gui/src/components/builder/JsonTab.tsx b/apps/gui/src/components/builder/JsonTab.tsx
index 3ee31f3..983a0f6 100644
--- a/apps/gui/src/components/builder/JsonTab.tsx
+++ b/apps/gui/src/components/builder/JsonTab.tsx
@@ -30,7 +30,7 @@ export function JsonTab({ compiledScenario, onCopySuccess }: JsonTabProps) {
-
+
{JSON.stringify(compiledScenario, null, 2)}
diff --git a/apps/gui/src/components/builder/LocationsTab.tsx b/apps/gui/src/components/builder/LocationsTab.tsx
index f81ef5d..596d65c 100644
--- a/apps/gui/src/components/builder/LocationsTab.tsx
+++ b/apps/gui/src/components/builder/LocationsTab.tsx
@@ -4,6 +4,14 @@ import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Checkbox } from "@/components/ui/checkbox";
+import {
+ Combobox,
+ ComboboxContent,
+ ComboboxEmpty,
+ ComboboxInput,
+ ComboboxItem,
+ ComboboxList,
+} from "@/components/ui/combobox";
import { AttributeEditor } from "./AttributeEditor";
import { Plus, Trash2 } from "lucide-react";
import type { LocationData, ConnectionData, EntityData } from "./types";
@@ -162,25 +170,30 @@ export function LocationsTab({
-
+
+
+ No locations found.
+
+ {(id: string) => (
+
+ {getLocationDisplayNameById(id, locations)}
+
+ )}
+
+
+
{/* Attributes for Location */}
@@ -257,27 +270,35 @@ export function LocationsTab({
Target Location
-
+
+
+ No locations found.
+
+ {(id: string) => (
+
+ {getLocationDisplayNameById(id, locations)}
+
+ )}
+
+
+
diff --git a/apps/gui/src/components/builder/MetadataTab.tsx b/apps/gui/src/components/builder/MetadataTab.tsx
index 87f39cd..6f64e75 100644
--- a/apps/gui/src/components/builder/MetadataTab.tsx
+++ b/apps/gui/src/components/builder/MetadataTab.tsx
@@ -1,5 +1,6 @@
"use client";
+import { useMemo } from "react";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
@@ -42,6 +43,39 @@ export function MetadataTab({
]);
};
+ // Parse initial state from ISO string (or fallback to now)
+ const parsedDate = useMemo(() => {
+ try {
+ const d = new Date(startTime);
+ if (isNaN(d.getTime())) return new Date();
+ return d;
+ } catch {
+ return new Date();
+ }
+ }, [startTime]);
+
+ const dateValue = useMemo(() => {
+ return parsedDate.toISOString().split("T")[0];
+ }, [parsedDate]);
+
+ const timeValue = useMemo(() => {
+ return parsedDate.toISOString().split("T")[1].slice(0, 8);
+ }, [parsedDate]);
+
+ const handleDateChange = (newDateStr: string) => {
+ if (!newDateStr) return;
+ const combined = `${newDateStr}T${timeValue}.000Z`;
+ setStartTime(combined);
+ };
+
+ const handleTimeChange = (newTimeStr: string) => {
+ if (!newTimeStr) return;
+ const formattedTime =
+ newTimeStr.split(":").length === 2 ? `${newTimeStr}:00` : newTimeStr;
+ const combined = `${dateValue}T${formattedTime}.000Z`;
+ setStartTime(combined);
+ };
+
return (
{/* Basic Fields */}
@@ -55,14 +89,9 @@ export function MetadataTab({
- setScenarioId(
- e.target.value.toLowerCase().replace(/[^a-z0-9-_]/g, ""),
- )
- }
- className="font-mono text-xs"
+ readOnly
+ className="font-mono text-xs bg-muted cursor-not-allowed text-muted-foreground"
/>
Unique filename ID. Alphanumeric, hyphens and underscores only.
@@ -70,16 +99,28 @@ export function MetadataTab({
-
-
setStartTime(e.target.value)}
- className="font-mono text-xs"
- />
+
+
- Global clock starting timestamp.
+ Global clock starting date and time (stored in ISO UTC).
diff --git a/apps/gui/src/components/ui/combobox.tsx b/apps/gui/src/components/ui/combobox.tsx
new file mode 100644
index 0000000..1e3c45e
--- /dev/null
+++ b/apps/gui/src/components/ui/combobox.tsx
@@ -0,0 +1,300 @@
+"use client";
+
+import * as React from "react";
+import { Combobox as ComboboxPrimitive } from "@base-ui/react";
+import { CheckIcon, ChevronDownIcon, XIcon } from "lucide-react";
+
+import { cn } from "@/lib/utils";
+import { Button } from "@/components/ui/button";
+import {
+ InputGroup,
+ InputGroupAddon,
+ InputGroupButton,
+ InputGroupInput,
+} from "@/components/ui/input-group";
+
+const Combobox = ComboboxPrimitive.Root;
+
+function ComboboxValue({ ...props }: ComboboxPrimitive.Value.Props) {
+ return
;
+}
+
+function ComboboxTrigger({
+ className,
+ children,
+ ...props
+}: ComboboxPrimitive.Trigger.Props) {
+ return (
+
+ {children}
+
+
+ );
+}
+
+function ComboboxClear({ className, ...props }: ComboboxPrimitive.Clear.Props) {
+ return (
+
}
+ className={cn(className)}
+ {...props}
+ >
+
+
+ );
+}
+
+function ComboboxInput({
+ className,
+ children,
+ disabled = false,
+ showTrigger = true,
+ showClear = false,
+ ...props
+}: ComboboxPrimitive.Input.Props & {
+ showTrigger?: boolean;
+ showClear?: boolean;
+}) {
+ return (
+
+ }
+ {...props}
+ />
+
+ {showTrigger && (
+ }
+ data-slot="input-group-button"
+ className="group-has-data-[slot=combobox-clear]/input-group:hidden data-pressed:bg-transparent"
+ disabled={disabled}
+ />
+ )}
+ {showClear && }
+
+ {children}
+
+ );
+}
+
+function ComboboxContent({
+ className,
+ side = "bottom",
+ sideOffset = 6,
+ align = "start",
+ alignOffset = 0,
+ anchor,
+ ...props
+}: ComboboxPrimitive.Popup.Props &
+ Pick<
+ ComboboxPrimitive.Positioner.Props,
+ "side" | "align" | "sideOffset" | "alignOffset" | "anchor"
+ >) {
+ return (
+
+
+
+
+
+ );
+}
+
+function ComboboxList({ className, ...props }: ComboboxPrimitive.List.Props) {
+ return (
+
+ );
+}
+
+function ComboboxItem({
+ className,
+ children,
+ ...props
+}: ComboboxPrimitive.Item.Props) {
+ return (
+
+ {children}
+
+ }
+ >
+
+
+
+ );
+}
+
+function ComboboxGroup({ className, ...props }: ComboboxPrimitive.Group.Props) {
+ return (
+
+ );
+}
+
+function ComboboxLabel({
+ className,
+ ...props
+}: ComboboxPrimitive.GroupLabel.Props) {
+ return (
+
+ );
+}
+
+function ComboboxCollection({ ...props }: ComboboxPrimitive.Collection.Props) {
+ return (
+
+ );
+}
+
+function ComboboxEmpty({ className, ...props }: ComboboxPrimitive.Empty.Props) {
+ return (
+
+ );
+}
+
+function ComboboxSeparator({
+ className,
+ ...props
+}: ComboboxPrimitive.Separator.Props) {
+ return (
+
+ );
+}
+
+function ComboboxChips({
+ className,
+ ...props
+}: React.ComponentPropsWithRef
&
+ ComboboxPrimitive.Chips.Props) {
+ return (
+
+ );
+}
+
+function ComboboxChip({
+ className,
+ children,
+ showRemove = true,
+ ...props
+}: ComboboxPrimitive.Chip.Props & {
+ showRemove?: boolean;
+}) {
+ return (
+
+ {children}
+ {showRemove && (
+ }
+ className="-ml-1 opacity-50 hover:opacity-100"
+ data-slot="combobox-chip-remove"
+ >
+
+
+ )}
+
+ );
+}
+
+function ComboboxChipsInput({
+ className,
+ ...props
+}: ComboboxPrimitive.Input.Props) {
+ return (
+
+ );
+}
+
+function useComboboxAnchor() {
+ return React.useRef(null);
+}
+
+export {
+ Combobox,
+ ComboboxInput,
+ ComboboxContent,
+ ComboboxList,
+ ComboboxItem,
+ ComboboxGroup,
+ ComboboxLabel,
+ ComboboxCollection,
+ ComboboxEmpty,
+ ComboboxSeparator,
+ ComboboxChips,
+ ComboboxChip,
+ ComboboxChipsInput,
+ ComboboxTrigger,
+ ComboboxValue,
+ useComboboxAnchor,
+};
diff --git a/apps/gui/src/components/ui/input-group.tsx b/apps/gui/src/components/ui/input-group.tsx
new file mode 100644
index 0000000..093a312
--- /dev/null
+++ b/apps/gui/src/components/ui/input-group.tsx
@@ -0,0 +1,170 @@
+"use client";
+
+import * as React from "react";
+import { cva, type VariantProps } from "class-variance-authority";
+
+import { cn } from "@/lib/utils";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Textarea } from "@/components/ui/textarea";
+
+function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+ textarea]:h-auto",
+
+ // Variants based on alignment.
+ "has-[>[data-align=inline-start]]:[&>input]:pl-2",
+ "has-[>[data-align=inline-end]]:[&>input]:pr-2",
+ "has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>[data-align=block-start]]:[&>input]:pb-3",
+ "has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-end]]:[&>input]:pt-3",
+
+ // Focus state.
+ "has-[[data-slot=input-group-control]:focus-visible]:ring-ring has-[[data-slot=input-group-control]:focus-visible]:ring-1",
+
+ // Error state.
+ "has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[[data-slot][aria-invalid=true]]:border-destructive dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40",
+
+ className,
+ )}
+ {...props}
+ />
+ );
+}
+
+const inputGroupAddonVariants = cva(
+ "text-muted-foreground flex h-auto cursor-text select-none items-center justify-center gap-2 py-1.5 text-sm font-medium group-data-[disabled=true]/input-group:opacity-50 [&>kbd]:rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-4",
+ {
+ variants: {
+ align: {
+ "inline-start":
+ "order-first pl-3 has-[>button]:ml-[-0.45rem] has-[>kbd]:ml-[-0.35rem]",
+ "inline-end":
+ "order-last pr-3 has-[>button]:mr-[-0.4rem] has-[>kbd]:mr-[-0.35rem]",
+ "block-start":
+ "[.border-b]:pb-3 order-first w-full justify-start px-3 pt-3 group-has-[>input]/input-group:pt-2.5",
+ "block-end":
+ "[.border-t]:pt-3 order-last w-full justify-start px-3 pb-3 group-has-[>input]/input-group:pb-2.5",
+ },
+ },
+ defaultVariants: {
+ align: "inline-start",
+ },
+ },
+);
+
+function InputGroupAddon({
+ className,
+ align = "inline-start",
+ ...props
+}: React.ComponentProps<"div"> & VariantProps
) {
+ return (
+ {
+ if ((e.target as HTMLElement).closest("button")) {
+ return;
+ }
+ e.currentTarget.parentElement?.querySelector("input")?.focus();
+ }}
+ {...props}
+ />
+ );
+}
+
+const inputGroupButtonVariants = cva(
+ "flex items-center gap-2 text-sm shadow-none",
+ {
+ variants: {
+ size: {
+ xs: "h-6 gap-1 rounded-[calc(var(--radius)-5px)] px-2 has-[>svg]:px-2 [&>svg:not([class*='size-'])]:size-3.5",
+ sm: "h-8 gap-1.5 rounded-md px-2.5 has-[>svg]:px-2.5",
+ "icon-xs":
+ "size-6 rounded-[calc(var(--radius)-5px)] p-0 has-[>svg]:p-0",
+ "icon-sm": "size-8 p-0 has-[>svg]:p-0",
+ },
+ },
+ defaultVariants: {
+ size: "xs",
+ },
+ },
+);
+
+function InputGroupButton({
+ className,
+ type = "button",
+ variant = "ghost",
+ size = "xs",
+ ...props
+}: Omit, "size"> &
+ VariantProps) {
+ return (
+
+ );
+}
+
+function InputGroupText({ className, ...props }: React.ComponentProps<"span">) {
+ return (
+
+ );
+}
+
+function InputGroupInput({
+ className,
+ ...props
+}: React.ComponentProps<"input">) {
+ return (
+
+ );
+}
+
+function InputGroupTextarea({
+ className,
+ ...props
+}: React.ComponentProps<"textarea">) {
+ return (
+
+ );
+}
+
+export {
+ InputGroup,
+ InputGroupAddon,
+ InputGroupButton,
+ InputGroupText,
+ InputGroupInput,
+ InputGroupTextarea,
+};