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" - /> + +
+
+ handleDateChange(e.target.value)} + className="text-xs font-mono" + /> +
+
+ handleTimeChange(e.target.value)} + className="text-xs font-mono" + /> +
+
- 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 ( +