CI / build (push) Successful in 4m26s
Adds a Postgres-backed quotes domain (poems/quotes/favorites) exposed as five agent-native actions (list-quotes, get-random-quote, create-quote, toggle-favorite, list-favorites) callable from both chat and the React UI via useActionQuery/useActionMutation. Moves the primary UI from chat to a mobile-first Quotes/Favorites experience (chat moves to /chat), adds manual OpenTelemetry instrumentation exporting traces/metrics/logs over OTLP, and wires a local Docker Postgres for shared state.
189 lines
6.1 KiB
TypeScript
189 lines
6.1 KiB
TypeScript
import {
|
|
AgentSidebar,
|
|
focusAgentChat,
|
|
isAgentChatHomeHandoffActive,
|
|
navigateWithAgentChatViewTransition,
|
|
useAgentChatHomeHandoff,
|
|
useAgentChatHomeHandoffLinks,
|
|
} from "@agent-native/core/client/agent-chat";
|
|
import { useT } from "@agent-native/core/client/i18n";
|
|
import { HeaderActionsProvider } from "@agent-native/toolkit/app-shell";
|
|
import { IconMenu2 } from "@tabler/icons-react";
|
|
import { useState, useEffect } from "react";
|
|
import { useLocation, useNavigate } from "react-router";
|
|
|
|
import { Button } from "@/components/ui/button";
|
|
import {
|
|
Sheet,
|
|
SheetContent,
|
|
SheetDescription,
|
|
SheetTitle,
|
|
} from "@/components/ui/sheet";
|
|
import { APP_TITLE } from "@/lib/app-config";
|
|
import { TAB_ID } from "@/lib/tab-id";
|
|
|
|
import { Header } from "./Header";
|
|
import { Sidebar } from "./Sidebar";
|
|
|
|
interface LayoutProps {
|
|
children: React.ReactNode;
|
|
}
|
|
|
|
const SIDEBAR_COLLAPSE_KEY = "chat.sidebar.collapsed";
|
|
|
|
/**
|
|
* Routes whose page renders its own toolbar. Layout still wraps these with the
|
|
* left Sidebar and agent surfaces but skips the global Header so they don't
|
|
* double-stack chrome.
|
|
*/
|
|
function routeOwnsToolbar(pathname: string): boolean {
|
|
return (
|
|
pathname === "/chat" ||
|
|
pathname.startsWith("/chat/") ||
|
|
pathname === "/database" ||
|
|
pathname.startsWith("/extensions")
|
|
);
|
|
}
|
|
|
|
export function Layout({ children }: LayoutProps) {
|
|
const location = useLocation();
|
|
const navigate = useNavigate();
|
|
const t = useT();
|
|
const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false);
|
|
const [sidebarCollapsed, setSidebarCollapsed] = useState(true);
|
|
const isChatRoute =
|
|
location.pathname === "/chat" || location.pathname.startsWith("/chat/");
|
|
const chatHomeHandoffActive = useAgentChatHomeHandoff({
|
|
storageKey: "chat",
|
|
activePath: location.pathname,
|
|
enabled: !isChatRoute,
|
|
});
|
|
const chatHomeHandoffPending = isAgentChatHomeHandoffActive("chat");
|
|
useAgentChatHomeHandoffLinks({
|
|
storageKey: "chat",
|
|
isChatPath: (pathname) => pathname === "/chat" || pathname.startsWith("/chat/"),
|
|
requireActiveHandoff: true,
|
|
});
|
|
|
|
useEffect(() => {
|
|
setMobileSidebarOpen(false);
|
|
}, [location.pathname]);
|
|
|
|
useEffect(() => {
|
|
const closeMobileSidebar = () => setMobileSidebarOpen(false);
|
|
window.addEventListener("agent-chat:open-thread", closeMobileSidebar);
|
|
return () => {
|
|
window.removeEventListener("agent-chat:open-thread", closeMobileSidebar);
|
|
};
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
try {
|
|
const stored = window.localStorage.getItem(SIDEBAR_COLLAPSE_KEY);
|
|
if (stored !== null) setSidebarCollapsed(stored === "1");
|
|
} catch {
|
|
// Ignore storage access errors; the default collapsed state still works.
|
|
}
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
try {
|
|
window.localStorage.setItem(
|
|
SIDEBAR_COLLAPSE_KEY,
|
|
sidebarCollapsed ? "1" : "0",
|
|
);
|
|
} catch {
|
|
// Ignore storage access errors.
|
|
}
|
|
}, [sidebarCollapsed]);
|
|
|
|
const ownsToolbar = routeOwnsToolbar(location.pathname);
|
|
function openAskAgentFullscreen() {
|
|
focusAgentChat();
|
|
navigateWithAgentChatViewTransition(navigate, "/chat");
|
|
}
|
|
|
|
const contentFrame = (
|
|
<div className="flex h-full min-w-0 flex-1 flex-col overflow-hidden">
|
|
{isChatRoute ? (
|
|
<div className="flex h-12 shrink-0 items-center gap-3 border-b border-border bg-card px-3 md:hidden">
|
|
<Button
|
|
type="button"
|
|
variant="ghost"
|
|
size="icon"
|
|
onClick={() => setMobileSidebarOpen(true)}
|
|
aria-label={t("navigation.openNavigation")}
|
|
>
|
|
<IconMenu2 className="size-4" />
|
|
</Button>
|
|
<span className="truncate text-sm font-semibold">{APP_TITLE}</span>
|
|
</div>
|
|
) : ownsToolbar ? (
|
|
<div className="flex h-12 shrink-0 items-center border-b border-border px-4 md:hidden">
|
|
<button
|
|
type="button"
|
|
onClick={() => setMobileSidebarOpen(true)}
|
|
aria-label={t("navigation.openNavigation")}
|
|
className="flex h-9 w-9 cursor-pointer items-center justify-center rounded-md text-muted-foreground hover:bg-accent hover:text-foreground"
|
|
>
|
|
<IconMenu2 className="h-4 w-4" />
|
|
</button>
|
|
</div>
|
|
) : (
|
|
<Header onOpenMobileSidebar={() => setMobileSidebarOpen(true)} />
|
|
)}
|
|
<main className="agent-native-app-main min-w-0 flex-1 overflow-y-auto overscroll-contain">
|
|
{children}
|
|
</main>
|
|
</div>
|
|
);
|
|
|
|
return (
|
|
<HeaderActionsProvider>
|
|
<div className="agent-layout-shell flex h-screen w-full overflow-hidden bg-background text-foreground">
|
|
<div className="agent-layout-left-drawer hidden md:block">
|
|
<Sidebar
|
|
collapsed={sidebarCollapsed}
|
|
onCollapsedChange={setSidebarCollapsed}
|
|
/>
|
|
</div>
|
|
<Sheet open={mobileSidebarOpen} onOpenChange={setMobileSidebarOpen}>
|
|
<SheetContent side="left" className="p-0 w-[260px]">
|
|
<SheetTitle className="sr-only">
|
|
{t("navigation.navigation")}
|
|
</SheetTitle>
|
|
<SheetDescription className="sr-only">
|
|
{t("navigation.navigationDescription")}
|
|
</SheetDescription>
|
|
<Sidebar collapsed={false} collapsible={false} />
|
|
</SheetContent>
|
|
</Sheet>
|
|
{isChatRoute ? (
|
|
<div className="agent-layout-main-surface flex min-w-0 flex-1 overflow-hidden">
|
|
{contentFrame}
|
|
</div>
|
|
) : (
|
|
<AgentSidebar
|
|
position="right"
|
|
chatViewTransition
|
|
chatViewTransitionHandoff={chatHomeHandoffPending}
|
|
storageKey="chat"
|
|
browserTabId={TAB_ID}
|
|
openOnChatRunning={chatHomeHandoffActive}
|
|
onFullscreenRequest={openAskAgentFullscreen}
|
|
emptyStateText={t("chat.inspectEmptyState")}
|
|
agentPageHref="/agent"
|
|
suggestions={[
|
|
t("chat.inspectSuggestionCapabilities"),
|
|
t("chat.inspectSuggestionHello"),
|
|
t("chat.inspectSuggestionAction"),
|
|
]}
|
|
>
|
|
{contentFrame}
|
|
</AgentSidebar>
|
|
)}
|
|
</div>
|
|
</HeaderActionsProvider>
|
|
);
|
|
}
|