Documentation Index
Fetch the complete documentation index at: https://chatjs.dev/docs/llms.txt
Use this file to discover all available pages before exploring further.
Split-panel chat interface with a collapsible secondary panel for artifacts, previews, or tools.
Overview
A responsive two-panel layout where the main chat occupies the primary space and a secondary panel (for artifacts, code previews, etc.) slides in when needed. On mobile, the secondary panel replaces the main panel. Composable shadcn-style primitives.
How it works
The layout uses react-resizable-panels wrapped in composable components:
| Component | Purpose |
|---|
ChatLayout | Root container with sidebar-aware max-width |
ChatLayoutMain | Primary panel, hides on mobile when secondary is visible |
ChatLayoutHandle | Drag handle between panels |
ChatLayoutSecondary | Conditional panel, renders only when visible |
Visibility is controlled via the isSecondaryPanelVisible prop on ChatLayout. This value is shared to child components through React context, so they automatically show/hide based on the state.
Code
Components
components/chat/chat-layout.tsx
"use client";
import type { ComponentProps } from "react";
import { createContext, useContext } from "react";
import {
ResizableHandle,
ResizablePanel,
ResizablePanelGroup,
} from "@/components/ui/resizable";
import { useSidebar } from "@/components/ui/sidebar";
import { cn } from "@/lib/utils";
type ChatLayoutContextValue = {
isSecondaryPanelVisible: boolean;
};
const ChatLayoutContext = createContext<ChatLayoutContextValue | null>(null);
function useChatLayoutContext() {
const context = useContext(ChatLayoutContext);
if (!context) {
throw new Error("ChatLayout components must be used within <ChatLayout />");
}
return context;
}
type ChatLayoutProps = Omit<
ComponentProps<typeof ResizablePanelGroup>,
"direction"
> & {
isSecondaryPanelVisible?: boolean;
};
export const ChatLayout = ({
className,
children,
isSecondaryPanelVisible = false,
...props
}: ChatLayoutProps) => {
const { state: sidebarState } = useSidebar();
return (
<ChatLayoutContext.Provider value={{ isSecondaryPanelVisible }}>
<ResizablePanelGroup
className={cn(
"@container flex h-dvh w-full min-w-0 max-w-screen flex-col bg-background md:max-w-[calc(100vw-var(--sidebar-width))]",
sidebarState === "collapsed" && "md:max-w-screen",
className
)}
direction="horizontal"
{...props}
>
{children}
</ResizablePanelGroup>
</ChatLayoutContext.Provider>
);
};
type ChatLayoutMainProps = ComponentProps<typeof ResizablePanel>;
export const ChatLayoutMain = ({
className,
defaultSize = 65,
minSize = 40,
...props
}: ChatLayoutMainProps) => {
const { isSecondaryPanelVisible } = useChatLayoutContext();
return (
<ResizablePanel
className={cn(isSecondaryPanelVisible && "hidden md:block", className)}
defaultSize={defaultSize}
minSize={minSize}
{...props}
/>
);
};
type ChatLayoutSecondaryProps = ComponentProps<typeof ResizablePanel>;
export const ChatLayoutSecondary = ({
defaultSize = 35,
minSize = 25,
...props
}: ChatLayoutSecondaryProps) => {
const { isSecondaryPanelVisible } = useChatLayoutContext();
if (!isSecondaryPanelVisible) {
return null;
}
return (
<ResizablePanel defaultSize={defaultSize} minSize={minSize} {...props} />
);
};
type ChatLayoutHandleProps = ComponentProps<typeof ResizableHandle>;
export const ChatLayoutHandle = ({
className,
withHandle = true,
...props
}: ChatLayoutHandleProps) => {
const { isSecondaryPanelVisible } = useChatLayoutContext();
if (!isSecondaryPanelVisible) {
return null;
}
return (
<ResizableHandle
className={cn("hidden md:flex", className)}
withHandle={withHandle}
{...props}
/>
);
};
Usage
"use client";
import {
ChatLayout,
ChatLayoutHandle,
ChatLayoutMain,
ChatLayoutSecondary,
} from "@/components/chat/chat-layout";
import { MainChatPanel } from "@/components/chat/main-chat-panel";
import { SecondaryChatPanel } from "@/components/chat/secondary-chat-panel";
import { useArtifactSelector } from "@/hooks/use-artifact";
export function Chat({ id, isReadonly, projectId }) {
const isSecondaryPanelVisible = useArtifactSelector(
(state) => state.isVisible
);
return (
<ChatLayout isSecondaryPanelVisible={isSecondaryPanelVisible}>
<ChatLayoutMain>
<MainChatPanel
chatId={id}
className="flex h-full min-w-0 flex-1 flex-col"
isReadonly={isReadonly}
projectId={projectId}
/>
</ChatLayoutMain>
<ChatLayoutHandle />
<ChatLayoutSecondary>
<SecondaryChatPanel
className="flex h-full min-w-0 flex-1 flex-col"
isReadonly={isReadonly}
/>
</ChatLayoutSecondary>
</ChatLayout>
);
}
Customization
Override default sizes per-instance:
<ChatLayoutMain defaultSize={70} minSize={50}>
{/* ... */}
</ChatLayoutMain>
<ChatLayoutSecondary defaultSize={30} minSize={20}>
{/* ... */}
</ChatLayoutSecondary>