mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-04-26 00:46:23 +02:00
Add agent selection and artifact management to RowboatX UI
- Implemented agent selection dropdown in the input area. - Enhanced artifact management with loading, saving, and error handling. - Added new API routes for fetching agent summaries and run details. - Updated sidebar to display agents, configurations, and runs dynamically. - Introduced theme selection options in the user navigation menu.
This commit is contained in:
parent
b1f6e64244
commit
023a65de45
8 changed files with 965 additions and 251 deletions
|
|
@ -1,117 +1,55 @@
|
|||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import {
|
||||
AudioWaveform,
|
||||
Bot,
|
||||
Calendar,
|
||||
Command,
|
||||
GalleryVerticalEnd,
|
||||
Play,
|
||||
Plug,
|
||||
Users,
|
||||
Zap,
|
||||
} from "lucide-react"
|
||||
import { ChevronRight, Clock3, FileText, Folder, Play, Plug, Rocket, Users } from "lucide-react"
|
||||
|
||||
import { NavMain } from "@/components/nav-main"
|
||||
import { NavProjects } from "@/components/nav-projects"
|
||||
import { NavUser } from "@/components/nav-user"
|
||||
import { TeamSwitcher } from "@/components/team-switcher"
|
||||
import { NavMain } from "@/components/nav-main"
|
||||
import { NavProjects } from "@/components/nav-projects"
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
SidebarFooter,
|
||||
SidebarHeader,
|
||||
SidebarRail,
|
||||
SidebarGroup,
|
||||
SidebarGroupLabel,
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
useSidebar,
|
||||
} from "@/components/ui/sidebar"
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"
|
||||
|
||||
// This is sample data.
|
||||
const data = {
|
||||
user: {
|
||||
name: "shadcn",
|
||||
email: "m@example.com",
|
||||
avatar: "/avatars/shadcn.jpg",
|
||||
name: "user",
|
||||
email: "user@example.com",
|
||||
avatar: "/avatars/user.jpg",
|
||||
},
|
||||
teams: [
|
||||
{
|
||||
name: "Acme Inc",
|
||||
logo: GalleryVerticalEnd,
|
||||
plan: "Enterprise",
|
||||
},
|
||||
{
|
||||
name: "Acme Corp.",
|
||||
logo: AudioWaveform,
|
||||
plan: "Startup",
|
||||
},
|
||||
{
|
||||
name: "Evil Corp.",
|
||||
logo: Command,
|
||||
plan: "Free",
|
||||
name: "RowboatX",
|
||||
logo: Users,
|
||||
plan: "Workspace",
|
||||
},
|
||||
],
|
||||
chatHistory: [
|
||||
{ name: "Building a React Dashboard", url: "#" },
|
||||
{ name: "API Integration Best Practices", url: "#" },
|
||||
{ name: "TypeScript Migration Guide", url: "#" },
|
||||
{ name: "Database Optimization Tips", url: "#" },
|
||||
{ name: "Docker Container Setup", url: "#" },
|
||||
{ name: "GraphQL vs REST API", url: "#" },
|
||||
],
|
||||
navMain: [
|
||||
{
|
||||
title: "Agents",
|
||||
url: "#",
|
||||
icon: Users,
|
||||
isActive: true,
|
||||
items: [
|
||||
{
|
||||
title: "View All Agents",
|
||||
url: "#",
|
||||
},
|
||||
{
|
||||
title: "Create Agent",
|
||||
url: "#",
|
||||
},
|
||||
{
|
||||
title: "Agent Templates",
|
||||
url: "#",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "MCP",
|
||||
url: "#",
|
||||
icon: Plug,
|
||||
items: [
|
||||
{
|
||||
title: "Servers",
|
||||
url: "#",
|
||||
},
|
||||
{
|
||||
title: "Tools",
|
||||
url: "#",
|
||||
},
|
||||
{
|
||||
title: "Configuration",
|
||||
url: "#",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Runs",
|
||||
url: "#",
|
||||
icon: Play,
|
||||
items: [
|
||||
{
|
||||
title: "Active Runs",
|
||||
url: "#",
|
||||
},
|
||||
{
|
||||
title: "History",
|
||||
url: "#",
|
||||
},
|
||||
{
|
||||
title: "Failed Runs",
|
||||
url: "#",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Scheduled",
|
||||
url: "#",
|
||||
icon: Calendar,
|
||||
icon: Clock3,
|
||||
isActive: false,
|
||||
items: [
|
||||
{
|
||||
title: "View Schedule",
|
||||
|
|
@ -130,7 +68,7 @@ const data = {
|
|||
{
|
||||
title: "Applets",
|
||||
url: "#",
|
||||
icon: Zap,
|
||||
icon: Rocket,
|
||||
items: [
|
||||
{
|
||||
title: "Browse Applets",
|
||||
|
|
@ -147,42 +85,259 @@ const data = {
|
|||
],
|
||||
},
|
||||
],
|
||||
chatHistory: [
|
||||
{
|
||||
name: "Building a React Dashboard",
|
||||
url: "#",
|
||||
},
|
||||
{
|
||||
name: "API Integration Best Practices",
|
||||
url: "#",
|
||||
},
|
||||
{
|
||||
name: "TypeScript Migration Guide",
|
||||
url: "#",
|
||||
},
|
||||
{
|
||||
name: "Database Optimization Tips",
|
||||
url: "#",
|
||||
},
|
||||
{
|
||||
name: "Docker Container Setup",
|
||||
url: "#",
|
||||
},
|
||||
{
|
||||
name: "GraphQL vs REST API",
|
||||
url: "#",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||
type RowboatSummary = {
|
||||
agents: string[]
|
||||
config: string[]
|
||||
runs: string[]
|
||||
}
|
||||
|
||||
type ResourceKind = "agent" | "config" | "run"
|
||||
|
||||
type SidebarSelect = (item: { kind: ResourceKind; name: string }) => void
|
||||
|
||||
type AppSidebarProps = React.ComponentProps<typeof Sidebar> & {
|
||||
onSelectResource?: SidebarSelect
|
||||
}
|
||||
|
||||
export function AppSidebar({ onSelectResource, ...props }: AppSidebarProps) {
|
||||
const { state: sidebarState } = useSidebar()
|
||||
const [summary, setSummary] = React.useState<RowboatSummary>({
|
||||
agents: [],
|
||||
config: [],
|
||||
runs: [],
|
||||
})
|
||||
const [loading, setLoading] = React.useState(true)
|
||||
|
||||
React.useEffect(() => {
|
||||
const load = async () => {
|
||||
try {
|
||||
const res = await fetch("/api/rowboat/summary")
|
||||
if (!res.ok) return
|
||||
const data = await res.json()
|
||||
setSummary({
|
||||
agents: data.agents || [],
|
||||
config: data.config || [],
|
||||
runs: data.runs || [],
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("Failed to load rowboat summary", error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
load()
|
||||
}, [])
|
||||
|
||||
// Limit runs shown and provide "View more" affordance similar to chat history.
|
||||
const runsLimit = 8
|
||||
const visibleRuns = summary.runs.slice(0, runsLimit)
|
||||
const hasMoreRuns = summary.runs.length > runsLimit
|
||||
|
||||
const handleSelect = (kind: ResourceKind, name: string) => {
|
||||
onSelectResource?.({ kind, name })
|
||||
}
|
||||
|
||||
const navInitial = React.useMemo(
|
||||
() =>
|
||||
data.navMain.reduce<Record<string, boolean>>((acc, item) => {
|
||||
acc[item.title] = false
|
||||
return acc
|
||||
}, {}),
|
||||
[]
|
||||
)
|
||||
|
||||
const [openGroups, setOpenGroups] = React.useState<Record<string, boolean>>({
|
||||
agents: false,
|
||||
config: false,
|
||||
runs: false,
|
||||
...navInitial,
|
||||
})
|
||||
|
||||
const isCollapsed = sidebarState === "collapsed"
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isCollapsed) {
|
||||
setOpenGroups((prev) => {
|
||||
const closed: Record<string, boolean> = {}
|
||||
for (const key of Object.keys(prev)) closed[key] = false
|
||||
return closed
|
||||
})
|
||||
}
|
||||
}, [isCollapsed])
|
||||
|
||||
const handleOpenChange = (key: string, next: boolean) => {
|
||||
if (isCollapsed) return
|
||||
setOpenGroups((prev) => ({ ...prev, [key]: next }))
|
||||
}
|
||||
|
||||
return (
|
||||
<Sidebar collapsible="icon" {...props}>
|
||||
<SidebarHeader>
|
||||
<TeamSwitcher teams={data.teams} />
|
||||
</SidebarHeader>
|
||||
<SidebarContent>
|
||||
<NavMain items={data.navMain} />
|
||||
<SidebarGroup>
|
||||
<SidebarGroupLabel>Platform</SidebarGroupLabel>
|
||||
<SidebarMenu>
|
||||
<Collapsible
|
||||
className="group/collapsible"
|
||||
open={openGroups.agents}
|
||||
onOpenChange={(open) => handleOpenChange("agents", open)}
|
||||
>
|
||||
<SidebarMenuItem>
|
||||
<CollapsibleTrigger asChild>
|
||||
<SidebarMenuButton className="h-9">
|
||||
<Folder className="mr-2 h-4 w-4" />
|
||||
<span className="truncate">Agents</span>
|
||||
<ChevronRight className="ml-auto h-3.5 w-3.5 transition-transform group-data-[state=open]/collapsible:rotate-90" />
|
||||
</SidebarMenuButton>
|
||||
</CollapsibleTrigger>
|
||||
</SidebarMenuItem>
|
||||
<CollapsibleContent asChild>
|
||||
<SidebarMenu className="pl-2">
|
||||
{loading ? (
|
||||
<div className="px-3 py-2 text-xs text-muted-foreground">Loading…</div>
|
||||
) : summary.agents.length === 0 ? (
|
||||
<div className="px-3 py-2 text-xs text-muted-foreground">No agents found</div>
|
||||
) : (
|
||||
summary.agents.map((name) => (
|
||||
<SidebarMenuItem key={name}>
|
||||
<SidebarMenuButton
|
||||
className="pl-8 h-8"
|
||||
onClick={() => handleSelect("agent", name)}
|
||||
>
|
||||
<FileText className="mr-2 h-3.5 w-3.5" />
|
||||
<span className="truncate">{name}</span>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
))
|
||||
)}
|
||||
</SidebarMenu>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
|
||||
<Collapsible
|
||||
className="group/collapsible"
|
||||
open={openGroups.config}
|
||||
onOpenChange={(open) => handleOpenChange("config", open)}
|
||||
>
|
||||
<SidebarMenuItem>
|
||||
<CollapsibleTrigger asChild>
|
||||
<SidebarMenuButton className="h-9">
|
||||
<Plug className="mr-2 h-4 w-4" />
|
||||
<span className="truncate">Config</span>
|
||||
<ChevronRight className="ml-auto h-3.5 w-3.5 transition-transform group-data-[state=open]/collapsible:rotate-90" />
|
||||
</SidebarMenuButton>
|
||||
</CollapsibleTrigger>
|
||||
</SidebarMenuItem>
|
||||
<CollapsibleContent asChild>
|
||||
<SidebarMenu className="pl-2">
|
||||
{loading ? (
|
||||
<div className="px-3 py-2 text-xs text-muted-foreground">Loading…</div>
|
||||
) : summary.config.length === 0 ? (
|
||||
<div className="px-3 py-2 text-xs text-muted-foreground">No config files</div>
|
||||
) : (
|
||||
summary.config.map((name) => (
|
||||
<SidebarMenuItem key={name}>
|
||||
<SidebarMenuButton
|
||||
className="pl-8 h-8"
|
||||
onClick={() => handleSelect("config", name)}
|
||||
>
|
||||
<FileText className="mr-2 h-3.5 w-3.5" />
|
||||
<span className="truncate">{name}</span>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
))
|
||||
)}
|
||||
</SidebarMenu>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
|
||||
<Collapsible
|
||||
className="group/collapsible"
|
||||
open={openGroups.runs}
|
||||
onOpenChange={(open) => handleOpenChange("runs", open)}
|
||||
>
|
||||
<SidebarMenuItem>
|
||||
<CollapsibleTrigger asChild>
|
||||
<SidebarMenuButton className="h-9">
|
||||
<Play className="mr-2 h-4 w-4" />
|
||||
<span className="truncate">Runs</span>
|
||||
<ChevronRight className="ml-auto h-3.5 w-3.5 transition-transform group-data-[state=open]/collapsible:rotate-90" />
|
||||
</SidebarMenuButton>
|
||||
</CollapsibleTrigger>
|
||||
</SidebarMenuItem>
|
||||
<CollapsibleContent asChild>
|
||||
<SidebarMenu className="pl-2">
|
||||
{loading ? (
|
||||
<div className="px-3 py-2 text-xs text-muted-foreground">Loading…</div>
|
||||
) : summary.runs.length === 0 ? (
|
||||
<div className="px-3 py-2 text-xs text-muted-foreground">No runs found</div>
|
||||
) : (
|
||||
<>
|
||||
{visibleRuns.map((name) => (
|
||||
<SidebarMenuItem key={name}>
|
||||
<SidebarMenuButton
|
||||
className="pl-8 h-8"
|
||||
onClick={() => handleSelect("run", name)}
|
||||
>
|
||||
<FileText className="mr-2 h-3.5 w-3.5" />
|
||||
<span className="truncate">{name}</span>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
))}
|
||||
{hasMoreRuns && (
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton className="pl-8 h-8 text-muted-foreground">
|
||||
<span className="truncate">View more…</span>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</SidebarMenu>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
|
||||
{data.navMain.map((item) => (
|
||||
<Collapsible
|
||||
key={item.title}
|
||||
className="group/collapsible"
|
||||
open={openGroups[item.title]}
|
||||
onOpenChange={(open) => handleOpenChange(item.title, open)}
|
||||
>
|
||||
<SidebarMenuItem>
|
||||
<CollapsibleTrigger asChild>
|
||||
<SidebarMenuButton className="h-9">
|
||||
{item.title === "Scheduled" ? (
|
||||
<Clock3 className="mr-2 h-4 w-4" />
|
||||
) : item.title === "Applets" ? (
|
||||
<Rocket className="mr-2 h-4 w-4" />
|
||||
) : (
|
||||
<Folder className="mr-2 h-4 w-4" />
|
||||
)}
|
||||
<span className="truncate">{item.title}</span>
|
||||
<ChevronRight className="ml-auto h-3.5 w-3.5 transition-transform group-data-[state=open]/collapsible:rotate-90" />
|
||||
</SidebarMenuButton>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent asChild>
|
||||
<SidebarMenu className="pl-2">
|
||||
{item.items?.map((sub) => (
|
||||
<SidebarMenuItem key={sub.title}>
|
||||
<SidebarMenuButton className="pl-8 h-8">
|
||||
<span className="truncate">{sub.title}</span>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
))}
|
||||
</SidebarMenu>
|
||||
</CollapsibleContent>
|
||||
</SidebarMenuItem>
|
||||
</Collapsible>
|
||||
))}
|
||||
</SidebarMenu>
|
||||
</SidebarGroup>
|
||||
<NavProjects projects={data.chatHistory} />
|
||||
</SidebarContent>
|
||||
<SidebarFooter>
|
||||
|
|
@ -192,5 +347,3 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
|||
</Sidebar>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,11 @@ import {
|
|||
CreditCard,
|
||||
LogOut,
|
||||
Sparkles,
|
||||
Moon,
|
||||
Sun,
|
||||
MonitorCog,
|
||||
} from "lucide-react"
|
||||
import { useEffect, useState } from "react"
|
||||
|
||||
import {
|
||||
Avatar,
|
||||
|
|
@ -40,6 +44,40 @@ export function NavUser({
|
|||
}
|
||||
}) {
|
||||
const { isMobile } = useSidebar()
|
||||
const [theme, setTheme] = useState<"light" | "dark" | "system">("system")
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === "undefined") return
|
||||
const saved = (localStorage.getItem("theme") as "light" | "dark" | "system") || "system"
|
||||
setTheme(saved)
|
||||
applyTheme(saved)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === "undefined") return
|
||||
if (theme !== "system") return
|
||||
const media = window.matchMedia("(prefers-color-scheme: dark)")
|
||||
const listener = () => applyTheme("system")
|
||||
media.addEventListener("change", listener)
|
||||
return () => media.removeEventListener("change", listener)
|
||||
}, [theme])
|
||||
|
||||
const applyTheme = (value: "light" | "dark" | "system") => {
|
||||
const resolved =
|
||||
value === "system"
|
||||
? (window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light")
|
||||
: value
|
||||
const root = document.documentElement
|
||||
root.classList.toggle("dark", resolved === "dark")
|
||||
localStorage.setItem("theme", value)
|
||||
}
|
||||
|
||||
const handleTheme = (value: "light" | "dark" | "system") => {
|
||||
setTheme(value)
|
||||
if (typeof window !== "undefined") {
|
||||
applyTheme(value)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<SidebarMenu>
|
||||
|
|
@ -87,6 +125,31 @@ export function NavUser({
|
|||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuLabel>Theme</DropdownMenuLabel>
|
||||
<DropdownMenuItem
|
||||
className={theme === "light" ? "bg-muted" : ""}
|
||||
onClick={() => handleTheme("light")}
|
||||
>
|
||||
<Sun className="mr-2" />
|
||||
Light
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className={theme === "dark" ? "bg-muted" : ""}
|
||||
onClick={() => handleTheme("dark")}
|
||||
>
|
||||
<Moon className="mr-2" />
|
||||
Dark
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className={theme === "system" ? "bg-muted" : ""}
|
||||
onClick={() => handleTheme("system")}
|
||||
>
|
||||
<MonitorCog className="mr-2" />
|
||||
System
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem>
|
||||
<BadgeCheck />
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue