diff --git a/apps/gui/package.json b/apps/gui/package.json index bfe39ce..f4bac81 100644 --- a/apps/gui/package.json +++ b/apps/gui/package.json @@ -20,6 +20,7 @@ "@omnia/scenario": "workspace:*", "@omnia/spatial": "workspace:*", "@radix-ui/react-dialog": "^1.1.19", + "@radix-ui/react-dropdown-menu": "^2.1.20", "@radix-ui/react-separator": "^1.1.11", "@radix-ui/react-slot": "^1.3.0", "@radix-ui/react-tooltip": "^1.2.12", diff --git a/apps/gui/src/app/builder/page.tsx b/apps/gui/src/app/builder/page.tsx index de80736..cf2f37d 100644 --- a/apps/gui/src/app/builder/page.tsx +++ b/apps/gui/src/app/builder/page.tsx @@ -2,12 +2,24 @@ import { useEffect, useState, useMemo } from "react"; import { useRouter } from "next/navigation"; -import { Button } from "@/components/ui/button"; import { SidebarProvider, Sidebar, SidebarContent, } from "@/components/ui/sidebar"; +import { + Menubar, + MenubarMenu, + MenubarTrigger, + MenubarContent, + MenubarItem, + MenubarSeparator, + MenubarSub, + MenubarSubTrigger, + MenubarSubContent, + MenubarRadioGroup, + MenubarRadioItem, +} from "@/components/ui/menubar"; import { getConfigStatus, loadScenarioJson, saveScenario } from "@/app/actions"; import type { Scenario } from "@omnia/scenario"; import { @@ -16,9 +28,9 @@ import { Globe, MapPin, Users, - Sparkles, Info, Eye, + Pencil, } from "lucide-react"; // Import refactored builder components @@ -54,7 +66,6 @@ export default function BuilderPage() { const [availableScenarios, setAvailableScenarios] = useState< { path: string; name: string; description: string }[] >([]); - const [selectedTemplatePath, setSelectedTemplatePath] = useState(""); // Tabs: "metadata", "locations", "entities", "json" const [activeTab, setActiveTab] = useState< @@ -430,229 +441,326 @@ export default function BuilderPage() { } }; + const handleResetScenario = () => { + setScenarioId(""); + setName("My Custom Scenario"); + setDescription("A custom scenario template created via builder."); + setStartTime("2026-07-06T12:00:00.000Z"); + setWorldAttributes([]); + setSelectedLocIndex(0); + setSelectedEntIndex(0); + setStatusMessage({ text: "Scenario reset successfully.", type: "success" }); + }; + + const handleAddLocation = () => { + const newId = generateUUID(); + setLocations([ + ...locations, + { id: newId, attributes: [], connections: [] }, + ]); + setSelectedLocIndex(locations.length); + setActiveTab("locations"); + }; + + const handleAddEntity = () => { + const newId = generateUUID(); + setEntities([ + ...entities, + { + id: newId, + locationId: locationIds[0] || "", + attributes: [], + aliases: {}, + initialMemories: [], + isAgent: true, + }, + ]); + setSelectedEntIndex(entities.length); + setActiveTab("entities"); + }; + return ( - -
- {/* Save Status Banner */} - {statusMessage && ( -
-
- -
{statusMessage.text}
-
-
- )} - - {/* Viewport-level Vertical Sidebar on the Left Side */} - + {/* Save Status Banner */} + {statusMessage && ( +
- -
- - Configuration - - - - - - - - - -
- - {/* Sidebar Footer link */} -
- -
-
- - - {/* Main Centered Content Pane on the Right */} -
-
- {/* Header block with Page Name and Load/Save Actions */} -
-
-

- Scenario Builder -

-

- Define entities, spatial layouts, and initial metadata for - custom Omnia scenarios. -

-
- -
- {/* Load Template dropdown */} -
- - Load: - - -
- - - - -
-
- - {/* Active configuration tab form */} -
- {/* TAB 1: World Metadata & Attributes */} - {activeTab === "metadata" && ( - - )} - - {/* TAB 2: Locations & Spatial connections */} - {activeTab === "locations" && ( - - )} - - {/* TAB 3: Entities */} - {activeTab === "entities" && ( - - )} - - {/* TAB 4: Live JSON Preview */} - {activeTab === "json" && ( - - setStatusMessage({ - text: "JSON copied to clipboard!", - type: "success", - }) - } - /> - )} -
+
+ +
{statusMessage.text}
-
+
+ )} + + {/* Menubar spanning full page width right below navbar */} +
+
+ + + + + + File + + + + Load Template + + + {availableScenarios.length === 0 ? ( + No templates + ) : ( + availableScenarios.map((sc) => ( + { + handleLoadTemplate(sc.path); + }} + > + {sc.name} + + )) + )} + + + + + Save to Server + + + Export JSON + + + + + + Edit + + + Reset Scenario + + + + Add New Location + + + Add New Entity + + + + + + View + + setActiveTab(val as typeof activeTab)} + > + + World Metadata + + + Locations + + + Entities + + + Live JSON Preview + + + + + +
+
+ {scenarioId ? `ID: ${scenarioId}` : "Unsaved Scenario"} +
- + + +
+ {/* Viewport-level Vertical Sidebar on the Left Side */} + + +
+ + Configuration + + + + + + + + + +
+ + {/* Sidebar Footer link */} +
+ +
+
+
+ + {/* Main Centered Content Pane on the Right */} +
+
+ {/* Header block with Page Name */} +
+

+ Scenario Builder +

+
+ + {/* Active configuration tab form */} +
+ {/* TAB 1: World Metadata & Attributes */} + {activeTab === "metadata" && ( + + )} + + {/* TAB 2: Locations & Spatial connections */} + {activeTab === "locations" && ( + + )} + + {/* TAB 3: Entities */} + {activeTab === "entities" && ( + + )} + + {/* TAB 4: Live JSON Preview */} + {activeTab === "json" && ( + + setStatusMessage({ + text: "JSON copied to clipboard!", + type: "success", + }) + } + /> + )} +
+
+
+
+
+
); } diff --git a/apps/gui/src/app/globals.css b/apps/gui/src/app/globals.css index 2cb32dc..8a9f557 100644 --- a/apps/gui/src/app/globals.css +++ b/apps/gui/src/app/globals.css @@ -8,6 +8,7 @@ @custom-variant data-horizontal (&[data-orientation="horizontal"]); @custom-variant data-vertical (&[data-orientation="vertical"]); @custom-variant data-popup-open (&[data-state="open"]); +@custom-variant data-highlighted (&[data-highlighted]); @theme inline { --font-head: var(--font-head); diff --git a/apps/gui/src/components/builder/MetadataTab.tsx b/apps/gui/src/components/builder/MetadataTab.tsx index 6f64e75..fee8ca5 100644 --- a/apps/gui/src/components/builder/MetadataTab.tsx +++ b/apps/gui/src/components/builder/MetadataTab.tsx @@ -24,7 +24,6 @@ interface MetadataTabProps { export function MetadataTab({ scenarioId, - setScenarioId, name, setName, description, diff --git a/apps/gui/src/components/builder/WorldMap.tsx b/apps/gui/src/components/builder/WorldMap.tsx index e31c6c9..9a4a35b 100644 --- a/apps/gui/src/components/builder/WorldMap.tsx +++ b/apps/gui/src/components/builder/WorldMap.tsx @@ -2,7 +2,7 @@ import { useState, useMemo, useRef } from "react"; import { Button } from "@/components/ui/button"; -import { ZoomIn, ZoomOut, RotateCcw, Map as MapIcon } from "lucide-react"; +import { ZoomIn, ZoomOut, RotateCcw } from "lucide-react"; import type { LocationData } from "./types"; interface BoxNode { @@ -203,7 +203,20 @@ export function WorldMap({ // Connection lines computation const connectionLines = useMemo(() => { - const lines: any[] = []; + const lines: { + id: string; + x1: number; + y1: number; + x2: number; + y2: number; + portalName?: string; + portalState?: string; + vision?: number; + sound?: number; + bidirectional?: boolean; + sourceId: string; + targetId: string; + }[] = []; const seen = new Set(); locations.forEach((loc) => { diff --git a/apps/gui/src/components/ui/combobox.tsx b/apps/gui/src/components/ui/combobox.tsx index 1e3c45e..9b62879 100644 --- a/apps/gui/src/components/ui/combobox.tsx +++ b/apps/gui/src/components/ui/combobox.tsx @@ -5,7 +5,6 @@ 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, @@ -68,13 +67,16 @@ function ComboboxInput({ /> {showTrigger && ( - } - data-slot="input-group-button" - className="group-has-data-[slot=combobox-clear]/input-group:hidden data-pressed:bg-transparent" - disabled={disabled} + + } /> )} {showClear && } @@ -252,7 +254,7 @@ function ComboboxChip({ {children} {showRemove && ( } + render={} className="-ml-1 opacity-50 hover:opacity-100" data-slot="combobox-chip-remove" > diff --git a/apps/gui/src/components/ui/dropdown-menu.tsx b/apps/gui/src/components/ui/dropdown-menu.tsx new file mode 100644 index 0000000..99cba12 --- /dev/null +++ b/apps/gui/src/components/ui/dropdown-menu.tsx @@ -0,0 +1,200 @@ +"use client"; + +import * as React from "react"; +import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"; +import { Check, ChevronRight, Circle } from "lucide-react"; + +import { cn } from "@/lib/utils"; + +const DropdownMenu = DropdownMenuPrimitive.Root; + +const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger; + +const DropdownMenuGroup = DropdownMenuPrimitive.Group; + +const DropdownMenuPortal = DropdownMenuPrimitive.Portal; + +const DropdownMenuSub = DropdownMenuPrimitive.Sub; + +const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup; + +const DropdownMenuSubTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean; + } +>(({ className, inset, children, ...props }, ref) => ( + + {children} + + +)); +DropdownMenuSubTrigger.displayName = + DropdownMenuPrimitive.SubTrigger.displayName; + +const DropdownMenuSubContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DropdownMenuSubContent.displayName = + DropdownMenuPrimitive.SubContent.displayName; + +const DropdownMenuContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, sideOffset = 4, ...props }, ref) => ( + + + +)); +DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName; + +const DropdownMenuItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean; + } +>(({ className, inset, ...props }, ref) => ( + +)); +DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName; + +const DropdownMenuCheckboxItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, checked, ...props }, ref) => ( + + + + + + + {children} + +)); +DropdownMenuCheckboxItem.displayName = + DropdownMenuPrimitive.CheckboxItem.displayName; + +const DropdownMenuRadioItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + {children} + +)); +DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName; + +const DropdownMenuLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean; + } +>(({ className, inset, ...props }, ref) => ( + +)); +DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName; + +const DropdownMenuSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName; + +const DropdownMenuShortcut = ({ + className, + ...props +}: React.HTMLAttributes) => { + return ( + + ); +}; +DropdownMenuShortcut.displayName = "DropdownMenuShortcut"; + +export { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuCheckboxItem, + DropdownMenuRadioItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuGroup, + DropdownMenuPortal, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuRadioGroup, +}; diff --git a/apps/gui/src/components/ui/menubar.tsx b/apps/gui/src/components/ui/menubar.tsx new file mode 100644 index 0000000..57ed7f6 --- /dev/null +++ b/apps/gui/src/components/ui/menubar.tsx @@ -0,0 +1,292 @@ +"use client"; + +import * as React from "react"; +import { Menu as MenuPrimitive } from "@base-ui/react/menu"; +import { Menubar as MenubarPrimitive } from "@base-ui/react/menubar"; +import { CheckIcon, ChevronRight } from "lucide-react"; + +import { cn } from "@/lib/utils"; + +function Menubar({ className, ...props }: MenubarPrimitive.Props) { + return ( + + ); +} + +function MenubarMenu({ ...props }: MenuPrimitive.Root.Props) { + return ; +} + +function MenubarGroup({ ...props }: MenuPrimitive.Group.Props) { + return ; +} + +function MenubarPortal({ ...props }: MenuPrimitive.Portal.Props) { + return ; +} + +function MenubarTrigger({ className, ...props }: MenuPrimitive.Trigger.Props) { + return ( + + ); +} + +function MenubarContent({ + className, + align = "start", + alignOffset = -4, + sideOffset = 8, + ...props +}: MenuPrimitive.Popup.Props & { + align?: MenuPrimitive.Positioner.Props["align"]; + alignOffset?: MenuPrimitive.Positioner.Props["alignOffset"]; + sideOffset?: MenuPrimitive.Positioner.Props["sideOffset"]; +}) { + return ( + + + + + + ); +} + +function MenubarItem({ + className, + inset, + variant = "default", + ...props +}: MenuPrimitive.Item.Props & { + inset?: boolean; + variant?: "default" | "destructive"; +}) { + return ( + + ); +} + +function MenubarCheckboxItem({ + className, + children, + checked, + inset, + ...props +}: MenuPrimitive.CheckboxItem.Props & { + inset?: boolean; +}) { + return ( + + + + + + + {children} + + ); +} + +function MenubarRadioGroup({ ...props }: MenuPrimitive.RadioGroup.Props) { + return ( + + ); +} + +function MenubarRadioItem({ + className, + children, + inset, + ...props +}: MenuPrimitive.RadioItem.Props & { + inset?: boolean; +}) { + return ( + + + + + + + {children} + + ); +} + +function MenubarLabel({ + className, + inset, + ...props +}: MenuPrimitive.GroupLabel.Props & { + inset?: boolean; +}) { + return ( + + ); +} + +function MenubarSeparator({ + className, + ...props +}: MenuPrimitive.Separator.Props) { + return ( + + ); +} + +function MenubarShortcut({ + className, + ...props +}: React.ComponentProps<"span">) { + return ( + + ); +} + +// Submenu components using SubmenuRoot and SubmenuTrigger +function MenubarSub({ ...props }: MenuPrimitive.SubmenuRoot.Props) { + return ; +} + +function MenubarSubTrigger({ + className, + inset, + children, + ...props +}: MenuPrimitive.SubmenuTrigger.Props & { + inset?: boolean; +}) { + return ( + + {children} + + + ); +} + +function MenubarSubContent({ + className, + align = "start", + alignOffset = -4, + sideOffset = 8, + ...props +}: MenuPrimitive.Popup.Props & { + align?: MenuPrimitive.Positioner.Props["align"]; + alignOffset?: MenuPrimitive.Positioner.Props["alignOffset"]; + sideOffset?: MenuPrimitive.Positioner.Props["sideOffset"]; +}) { + return ( + + + + + + ); +} + +export { + Menubar, + MenubarPortal, + MenubarMenu, + MenubarTrigger, + MenubarContent, + MenubarGroup, + MenubarSeparator, + MenubarLabel, + MenubarItem, + MenubarShortcut, + MenubarCheckboxItem, + MenubarRadioGroup, + MenubarRadioItem, + MenubarSub, + MenubarSubTrigger, + MenubarSubContent, +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 058feeb..eb055c7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -342,6 +342,9 @@ importers: "@radix-ui/react-dialog": specifier: ^1.1.19 version: 1.1.19(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + "@radix-ui/react-dropdown-menu": + specifier: ^2.1.20 + version: 2.1.20(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) "@radix-ui/react-separator": specifier: ^1.1.11 version: 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)