mirror of
https://github.com/sipeed/picoclaw.git
synced 2026-06-12 18:08:54 +00:00
dea06c391c
* Improve the web launcher and gateway integration across backend and frontend. - add runtime model availability checks for local and OAuth-backed models - support launcher-driven gateway host overrides and websocket URL resolution - add gateway log clearing and keep incremental log sync consistent after resets - migrate session history APIs to JSONL metadata-backed storage with legacy fallback - expose session titles and improve chat history loading and error handling - move shared backend runtime helpers into the web utils package - avoid blocking web startup when automatic onboard initialization fails - add backend tests covering gateway readiness, host resolution, models, logs, and sessions * feat(agent): add skills and tools management APIs and UI - add backend APIs to list, view, import, and delete skills - add tool status and toggle endpoints with dependency-aware config updates - add agent skills/tools pages, routes, sidebar entries, and i18n strings - add backend tests for the new skills and tools flows * chore(frontend): upgrade shadcn to 4.0.5 and refresh lockfile * chore(web): keep backend dist placeholder tracked
239 lines
7.0 KiB
TypeScript
239 lines
7.0 KiB
TypeScript
import { IconChevronRight } from "@tabler/icons-react"
|
|
import {
|
|
IconAtom,
|
|
IconChevronsDown,
|
|
IconChevronsUp,
|
|
IconKey,
|
|
IconListDetails,
|
|
IconMessageCircle,
|
|
IconSettings,
|
|
IconSparkles,
|
|
IconTools,
|
|
} from "@tabler/icons-react"
|
|
import { Link, useRouterState } from "@tanstack/react-router"
|
|
import * as React from "react"
|
|
import { useTranslation } from "react-i18next"
|
|
|
|
import {
|
|
Collapsible,
|
|
CollapsibleContent,
|
|
CollapsibleTrigger,
|
|
} from "@/components/ui/collapsible"
|
|
import {
|
|
Sidebar,
|
|
SidebarContent,
|
|
SidebarGroup,
|
|
SidebarGroupContent,
|
|
SidebarGroupLabel,
|
|
SidebarMenu,
|
|
SidebarMenuButton,
|
|
SidebarMenuItem,
|
|
SidebarRail,
|
|
} from "@/components/ui/sidebar"
|
|
import { useSidebarChannels } from "@/hooks/use-sidebar-channels"
|
|
|
|
interface NavItem {
|
|
title: string
|
|
url: string
|
|
icon: React.ComponentType<{ className?: string }>
|
|
translateTitle?: boolean
|
|
}
|
|
|
|
interface NavGroup {
|
|
label: string
|
|
defaultOpen: boolean
|
|
items: NavItem[]
|
|
isChannelsGroup?: boolean
|
|
}
|
|
|
|
const baseNavGroups: Omit<NavGroup, "items">[] = [
|
|
{
|
|
label: "navigation.chat",
|
|
defaultOpen: true,
|
|
},
|
|
{
|
|
label: "navigation.model_group",
|
|
defaultOpen: true,
|
|
},
|
|
{
|
|
label: "navigation.agent_group",
|
|
defaultOpen: true,
|
|
},
|
|
{
|
|
label: "navigation.services",
|
|
defaultOpen: true,
|
|
},
|
|
]
|
|
|
|
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
|
const routerState = useRouterState()
|
|
const { t } = useTranslation()
|
|
const currentPath = routerState.location.pathname
|
|
const {
|
|
channelItems,
|
|
hasMoreChannels,
|
|
showAllChannels,
|
|
toggleShowAllChannels,
|
|
} = useSidebarChannels({ t })
|
|
|
|
const navGroups: NavGroup[] = React.useMemo(() => {
|
|
return [
|
|
{
|
|
...baseNavGroups[0],
|
|
items: [
|
|
{
|
|
title: "navigation.chat",
|
|
url: "/",
|
|
icon: IconMessageCircle,
|
|
translateTitle: true,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
...baseNavGroups[1],
|
|
items: [
|
|
{
|
|
title: "navigation.models",
|
|
url: "/models",
|
|
icon: IconAtom,
|
|
translateTitle: true,
|
|
},
|
|
{
|
|
title: "navigation.credentials",
|
|
url: "/credentials",
|
|
icon: IconKey,
|
|
translateTitle: true,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
label: "navigation.channels_group",
|
|
defaultOpen: true,
|
|
items: channelItems.map((item) => ({
|
|
title: item.title,
|
|
url: item.url,
|
|
icon: item.icon,
|
|
translateTitle: false,
|
|
})),
|
|
isChannelsGroup: true,
|
|
},
|
|
{
|
|
...baseNavGroups[2],
|
|
items: [
|
|
{
|
|
title: "navigation.skills",
|
|
url: "/agent/skills",
|
|
icon: IconSparkles,
|
|
translateTitle: true,
|
|
},
|
|
{
|
|
title: "navigation.tools",
|
|
url: "/agent/tools",
|
|
icon: IconTools,
|
|
translateTitle: true,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
...baseNavGroups[3],
|
|
items: [
|
|
{
|
|
title: "navigation.config",
|
|
url: "/config",
|
|
icon: IconSettings,
|
|
translateTitle: true,
|
|
},
|
|
{
|
|
title: "navigation.logs",
|
|
url: "/logs",
|
|
icon: IconListDetails,
|
|
translateTitle: true,
|
|
},
|
|
],
|
|
},
|
|
]
|
|
}, [channelItems])
|
|
|
|
return (
|
|
<Sidebar
|
|
{...props}
|
|
className="bg-background border-r-border/20 border-r pt-3"
|
|
>
|
|
<SidebarContent className="bg-background">
|
|
{navGroups.map((group) => (
|
|
<Collapsible
|
|
key={group.label}
|
|
defaultOpen={group.defaultOpen}
|
|
className="group/collapsible mb-1"
|
|
>
|
|
<SidebarGroup className="px-2 py-0">
|
|
<SidebarGroupLabel asChild>
|
|
<CollapsibleTrigger className="hover:bg-muted/60 flex w-full cursor-pointer items-center justify-between rounded-md px-2 py-1.5 transition-colors">
|
|
<span>{t(group.label)}</span>
|
|
<IconChevronRight className="size-3.5 opacity-50 transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
|
|
</CollapsibleTrigger>
|
|
</SidebarGroupLabel>
|
|
<CollapsibleContent>
|
|
<SidebarGroupContent className="pt-1">
|
|
<SidebarMenu>
|
|
{group.items.map((item) => {
|
|
const isActive =
|
|
currentPath === item.url ||
|
|
(item.url !== "/" &&
|
|
currentPath.startsWith(`${item.url}/`))
|
|
return (
|
|
<SidebarMenuItem key={item.title}>
|
|
<SidebarMenuButton
|
|
asChild
|
|
isActive={isActive}
|
|
className={`h-9 px-3 ${isActive ? "bg-accent/80 text-foreground font-medium" : "text-muted-foreground hover:bg-muted/60"}`}
|
|
>
|
|
<Link to={item.url}>
|
|
<item.icon
|
|
className={`size-4 ${isActive ? "opacity-100" : "opacity-60"}`}
|
|
/>
|
|
<span
|
|
className={
|
|
isActive ? "opacity-100" : "opacity-80"
|
|
}
|
|
>
|
|
{item.translateTitle === false
|
|
? item.title
|
|
: t(item.title)}
|
|
</span>
|
|
</Link>
|
|
</SidebarMenuButton>
|
|
</SidebarMenuItem>
|
|
)
|
|
})}
|
|
{group.isChannelsGroup && hasMoreChannels && (
|
|
<SidebarMenuItem key="channels-more-toggle">
|
|
<SidebarMenuButton
|
|
onClick={toggleShowAllChannels}
|
|
className="text-muted-foreground hover:bg-muted/60 h-9 px-3"
|
|
>
|
|
{showAllChannels ? (
|
|
<IconChevronsUp className="size-4 opacity-60" />
|
|
) : (
|
|
<IconChevronsDown className="size-4 opacity-60" />
|
|
)}
|
|
<span className="opacity-80">
|
|
{showAllChannels
|
|
? t("navigation.show_less_channels")
|
|
: t("navigation.show_more_channels")}
|
|
</span>
|
|
</SidebarMenuButton>
|
|
</SidebarMenuItem>
|
|
)}
|
|
</SidebarMenu>
|
|
</SidebarGroupContent>
|
|
</CollapsibleContent>
|
|
</SidebarGroup>
|
|
</Collapsible>
|
|
))}
|
|
</SidebarContent>
|
|
<SidebarRail />
|
|
</Sidebar>
|
|
)
|
|
}
|