Templates
Copilot sidebar

AI Copilot Sidebar

A collapsible sidebar that gives users an AI assistant context-aware about the current page. The sidebar panel sits alongside your main content — pressing a keyboard shortcut or button toggles it open.

Common pattern for dashboards, docs sites, and code editors. The sidebar shares no state with the rest of the app.

Layout — app/layout.tsx

import { CopilotSidebar } from '@/components/CopilotSidebar'
 
export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body style={{ display: 'flex', height: '100vh', overflow: 'hidden' }}>
        <main style={{ flex: 1, overflowY: 'auto' }}>{children}</main>
        <CopilotSidebar />
      </body>
    </html>
  )
}

CopilotSidebar.tsx

'use client'
import { useAIChat } from '@react-ai-stream/react'
import { Chat } from '@react-ai-stream/ui'
import '@react-ai-stream/ui/styles'
import { useEffect, useState } from 'react'
 
const SYSTEM = "You are a helpful AI copilot embedded in a web app. Answer questions concisely."
 
export function CopilotSidebar() {
  const [open, setOpen] = useState(false)
  const chat = useAIChat({
    endpoint: '/api/chat',
    initialMessages: [{ role: 'system', content: SYSTEM }],
  })
 
  // Toggle with Cmd/Ctrl + K
  useEffect(() => {
    const handler = (e: KeyboardEvent) => {
      if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
        e.preventDefault()
        setOpen(o => !o)
      }
    }
    window.addEventListener('keydown', handler)
    return () => window.removeEventListener('keydown', handler)
  }, [])
 
  return (
    <aside
      style={{
        width: open ? 380 : 0,
        minWidth: open ? 380 : 0,
        height: '100%',
        borderLeft: open ? '1px solid #e5e7eb' : 'none',
        overflow: 'hidden',
        transition: 'width 200ms ease, min-width 200ms ease',
        display: 'flex',
        flexDirection: 'column',
        position: 'relative',
      }}
    >
      {/* Toggle button — always visible */}
      <button
        onClick={() => setOpen(o => !o)}
        title={open ? 'Close copilot (⌘K)' : 'Open copilot (⌘K)'}
        style={{
          position: 'absolute',
          top: 12,
          left: open ? 12 : -40,
          zIndex: 10,
          width: 32, height: 32,
          borderRadius: 6,
          background: '#f3f4f6',
          border: '1px solid #e5e7eb',
          cursor: 'pointer',
          fontSize: 14,
          display: 'flex', alignItems: 'center', justifyContent: 'center',
        }}
      >
        {open ? '→' : '✦'}
      </button>
 
      {open && (
        <>
          <div style={{ padding: '0.75rem 1rem 0.5rem', borderBottom: '1px solid #f3f4f6', display: 'flex', alignItems: 'center', gap: 8 }}>
            <span style={{ fontSize: '0.8rem', fontWeight: 600, color: '#6b7280', textTransform: 'uppercase', letterSpacing: '0.05em' }}>AI Copilot</span>
            <kbd style={{ fontSize: '0.7rem', padding: '0.1rem 0.35rem', background: '#f3f4f6', border: '1px solid #e5e7eb', borderRadius: 4, color: '#9ca3af' }}>⌘K</kbd>
          </div>
          <div style={{ flex: 1, overflow: 'hidden', display: 'flex', flexDirection: 'column' }}>
            <Chat {...chat} placeholder="Ask the copilot…" />
          </div>
        </>
      )}
    </aside>
  )
}

Injecting page context

Make the copilot aware of the current page by including a system message with the page content:

const SYSTEM = `You are an AI copilot. The user is currently viewing:
 
Title: ${document.title}
URL: ${window.location.href}
 
Answer questions about this page concisely.`

Or pass structured data from server components via props.