Something went wrong. Try again.
This repository has no description
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396import { ChatCircleIcon, ClockIcon, GearIcon, PuzzlePieceIcon, MagnifyingGlassIcon, PlusIcon,} from "@octanejs/phosphor-icons";import { ClientOnly, Link, Outlet, useNavigate, useRouterState,} from "@octanejs/tanstack-router";import { useEffect, useMemo, useRef, useState } from "octane";import { Button } from "octane-kumo/components/button";import { CloudflareLogo } from "octane-kumo/components/cloudflare-logo";import { CommandPalette } from "octane-kumo/components/command-palette";import { Sidebar, useSidebar } from "octane-kumo/components/sidebar";import { ThemeToggle } from "../components/theme-toggle";import { ShellSessionProvider, useShellSession,} from "../runtime/shell-session";
const NAV = [ { href: "/", label: "Conversations", icon: ChatCircleIcon }, { href: "/tasks", label: "Scheduled tasks", icon: ClockIcon }, { href: "/extensions", label: "Extensions", icon: PuzzlePieceIcon }, { href: "/settings", label: "Settings", icon: GearIcon },] as const;const STATUS = { connecting: "Connecting…", connected: "Connected", reconnecting: "Reconnecting…", offline: "Offline", unauthorized: "Sign-in required",};type Command = { id: string; label: string; href?: string };
function Navigation() { const { conversations, status, loading, creating, session } = useShellSession(); const { isMobile, setOpenMobile } = useSidebar(); const navigate = useNavigate(); const pathname = useRouterState({ select: (state) => state.location.pathname, }); useEffect(() => { setOpenMobile(false); session.refresh(); }, [pathname]); const go = (href: string, event: MouseEvent) => { if ( event.metaKey || event.ctrlKey || event.shiftKey || event.altKey || event.button !== 0 ) return; event.preventDefault(); if (isMobile) setOpenMobile(false); void navigate({ to: href }); }; return ( <Sidebar aria-label="Main navigation" className="app-navigation"> <Sidebar.Header> <Button variant="primary" icon={PlusIcon} disabled={status !== "connected" || creating} loading={creating} onClick={() => { const requestedFrom = window.location.pathname; void session.createConversation().then((item) => { if (item && window.location.pathname === requestedFrom) { setOpenMobile(false); void navigate({ to: "/conversations/$id", params: { id: item.id }, }); } }); }} > New conversation </Button> </Sidebar.Header> <Sidebar.Content> <Sidebar.Group> <Sidebar.Menu> {NAV.map((item) => ( <Sidebar.MenuButton key={item.href} href={item.href} icon={item.icon} active={pathname === item.href} onClick={(event) => go(item.href, event)} > {item.label} </Sidebar.MenuButton> ))} </Sidebar.Menu> </Sidebar.Group> <Sidebar.Group> <Sidebar.GroupLabel>Your conversations</Sidebar.GroupLabel> {loading && !conversations.length ? <Sidebar.Loading /> : null} {!loading && !conversations.length && status === "connected" ? ( <p className="sidebar-note">No conversations yet.</p> ) : null} <Sidebar.Menu> {conversations.map((item) => ( <Sidebar.MenuButton key={item.id} href={`/conversations/${item.id}`} className="sidebar-thread" title={item.name} active={pathname === `/conversations/${item.id}`} aria-current={ pathname === `/conversations/${item.id}` ? "page" : undefined } onClick={(event) => go(`/conversations/${item.id}`, event)} > {item.name} </Sidebar.MenuButton> ))} </Sidebar.Menu> </Sidebar.Group> </Sidebar.Content> <Sidebar.Footer> <p className="sidebar-note">Your personal Flarebot</p> </Sidebar.Footer> </Sidebar> );}
function ShellCommands({ open, setOpen,}: { open: boolean; setOpen: (value: boolean) => void;}) { const { conversations, status, creating, session } = useShellSession(); const navigate = useNavigate(); const [query, setQuery] = useState(""); const portal = useRef<HTMLDivElement | null>(null); useEffect(() => { if (!open || !portal.current) return; // The pinned ARIA adapter retains focusedNodeId when filtered rows unmount. // Keep its input aligned with the actual native focused ListBoxItem. const syncFocus = () => { const dialog = portal.current?.querySelector( '[role="dialog"][aria-label="Command palette"]', ); const input = dialog?.querySelector("input"); const option = dialog?.querySelector<HTMLElement>( '[role="option"][data-focused="true"]', ); if (option) input?.setAttribute("aria-activedescendant", option.id); else input?.removeAttribute("aria-activedescendant"); }; const observer = new MutationObserver(syncFocus); observer.observe(portal.current, { subtree: true, childList: true, attributes: true, attributeFilter: ["data-focused"], }); syncFocus(); return () => observer.disconnect(); }, [open]); const items = useMemo<Command[]>( () => [ { id: "new", label: "New conversation" }, ...NAV.map((item) => ({ id: item.href, label: item.label, href: item.href, })), ...conversations.map((item) => ({ id: item.id, label: item.name, href: `/conversations/${item.id}`, })), ], [conversations], ); const select = (item: Command, newTab = false) => { setOpen(false); setQuery(""); if (item.href) { if (newTab) window.open(item.href, "_blank", "noopener,noreferrer"); else void navigate({ to: item.href }); } else if (status === "connected" && !creating) { const requestedFrom = window.location.pathname; void session.createConversation().then((conversation) => { if (conversation && window.location.pathname === requestedFrom) void navigate({ to: "/conversations/$id", params: { id: conversation.id }, }); }); } }; return ( <> <div className="command-portal" ref={portal} /> <CommandPalette.Root container={portal} open={open} onOpenChange={(value) => { setOpen(value); if (!value) setQuery(""); }} items={items} getSelectableItems={(values) => values} value={query} onValueChange={setQuery} itemToStringValue={(item) => item.label} filter={(item, value) => item.label.toLocaleLowerCase().includes(value.toLocaleLowerCase()) } onSelect={(item, options) => select(item, options.newTab)} > <CommandPalette.Input aria-label="Search conversations and commands" placeholder="Search conversations and commands…" onKeyDown={(event) => { if (event.key !== "Enter" || event.isComposing) return; const input = event.target as HTMLInputElement; const option = input .closest('[role="dialog"]') ?.querySelector<HTMLElement>( '[role="option"][data-focused="true"]', ); if (!option || option.getAttribute("aria-disabled") === "true") return; event.preventDefault(); option.dispatchEvent( new MouseEvent("click", { bubbles: true, cancelable: true, metaKey: event.metaKey, ctrlKey: event.ctrlKey, }), ); }} /> <CommandPalette.List> <CommandPalette.Results> {(item: Command) => ( <CommandPalette.Item value={item} disabled={ item.id === "new" && (status !== "connected" || creating) } onClick={(event) => select(item, event.metaKey || event.ctrlKey) } > {item.label} </CommandPalette.Item> )} </CommandPalette.Results> <CommandPalette.Empty> No matching conversations or commands. </CommandPalette.Empty> </CommandPalette.List> <CommandPalette.Footer> Use ↑ ↓ to navigate · Enter to open · Esc to close </CommandPalette.Footer> </CommandPalette.Root> </> );}
function MobileNavigationTrigger() { const { openMobile } = useSidebar(); return ( <Sidebar.Trigger className="shell-menu-trigger" aria-label={openMobile ? "Close navigation" : "Open navigation"} aria-expanded={openMobile} /> );}
function SidebarLayout() { const { status, error, session } = useShellSession(); const [paletteOpen, setPaletteOpen] = useState(false); useEffect(() => { const shortcut = (event: KeyboardEvent) => { if ( !event.isComposing && (event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "k" ) { event.preventDefault(); setPaletteOpen((value) => !value); } }; window.addEventListener("keydown", shortcut); return () => window.removeEventListener("keydown", shortcut); }, []); return ( <Sidebar.Provider className="shell" defaultOpen> <a href="#main-content" className="skip-link"> Skip to content </a> <header className="shell-header"> <MobileNavigationTrigger /> <Link to="/" className="shell-brand"> <CloudflareLogo variant="glyph" className="shell-brand-mark" /> <span>Flarebot</span> </Link> <div className="header-actions"> <ThemeToggle /> <span className="connection-status" data-state={status} role="status"> <span className="connection-dot" /> {STATUS[status]} </span> <Button variant="ghost" shape="square" icon={MagnifyingGlassIcon} aria-label="Search conversations and commands" onClick={() => setPaletteOpen(true)} /> </div> </header> <div className="shell-body"> <Navigation /> <div className="shell-main" id="main-content" tabIndex={-1}> {(error || status === "offline") && ( <div className="connection-notice" role="alert"> <p> {status === "offline" ? "You’re offline. Your saved conversations will be available when you reconnect." : error} </p> <a href="/auth/login">Sign in as owner</a> <Button onClick={ status === "connected" ? session.refresh : session.reconnect } > {status === "connected" ? "Refresh conversations" : "Reconnect"} </Button> </div> )} <Outlet /> </div> </div> <ShellCommands open={paletteOpen} setOpen={setPaletteOpen} /> </Sidebar.Provider> );}function ServerLayout() { return ( <div className="shell"> <header className="shell-header"> <Link to="/" className="shell-brand"> Flarebot </Link> <span className="muted">Connecting…</span> </header> <div className="shell-body"> <nav className="shell-fallback-nav" aria-label="Main navigation"> {NAV.map(({ href, label }) => ( <Link key={href} to={href}> {label} </Link> ))} </nav> <div className="shell-main"> <Outlet /> </div> </div> </div> );}export function RootLayout() { // The published sidebar requires a browser. SSR still renders usable navigation // and generic route content; customer metadata is fetched only after hydration. return ( <ShellSessionProvider> <ClientOnly fallback={<ServerLayout />}> <SidebarLayout /> </ClientOnly> </ShellSessionProvider> );}