ChatWidget component from @holocron.so/vite/chat adds a docs-aware AI chat to any React app. Point it at your holocron docs site and users get a floating textarea pill that opens into a full chat drawer, with streaming responses grounded in your documentation.123456import { ChatWidget } from '@holocron.so/vite/chat' <ChatWidget domain="docs.myapp.com" navigate={(path) => router.push(path)} />
.holocron-chat to prevent style leakage.1npm install @holocron.so/vite
domain prop must point to a running holocron docs site:123456789101112131415import { ChatWidget } from '@holocron.so/vite/chat' import { router } from 'your-framework/router' function App() { return ( <> {/* Your app content */} <ChatWidget domain="docs.myapp.com" siteName="My App" navigate={(path) => router.push(path)} /> </> ) }
/holocron-api/chat endpoint and streams responses back via RSC federation.domainstringrequired"docs.myapp.com"). The widget connects to https://{domain}/holocron-api/chat.navigate(path: string) => void | Promise<void>required(path) => router.push(path)(path) => navigate(path)(path) => router.push(path)triggerReact.ComponentType<{ onClick: () => void }>onClick to toggle the chat drawer.siteNamestringcurrentSlugstring"/quickstart"). Tells the AI which page the user is on. Defaults to "/".theme'light' | 'dark' | 'system''light' — always light mode'dark' — always dark mode'system' — follows OS prefers-color-scheme (default)styleReact.CSSPropertiesclassNamestringtoolsChatToolDefinition[]contextRecord<string, unknown>theme prop toggles between light and dark palettes:12345// Always dark widget <ChatWidget domain="docs.myapp.com" theme="dark" navigate={navigate} /> // Follow system preference (default) <ChatWidget domain="docs.myapp.com" theme="system" navigate={navigate} />
style prop. The widget uses zero-specificity defaults (:where(.holocron-chat)), so your overrides always win:12345678910<ChatWidget domain="docs.myapp.com" navigate={navigate} style={{ '--background': '#1a1a2e', '--foreground': '#eaeaea', '--primary': '#6366f1', '--border': '#2a2a4a', } as React.CSSProperties} />
--background, --foreground, --muted, --muted-foreground, --accent, --card, --border, --border-subtle, --destructive.--pill-width and --pill-expand CSS variables via the style prop:12345678<ChatWidget domain="docs.myapp.com" navigate={navigate} style={{ '--pill-width': '240px', // collapsed width (default: 300px) '--pill-expand': '80px', // extra width added on focus (default: 100px) } as React.CSSProperties} />
pill-width + pill-expand, so expansion stays relative regardless of the base size. Both values are clamped to 100vw - 32px on mobile so the pill never overflows the viewport.bg-background, so it blends into pages with the same background color. To make the pill stand out, use the opposite theme of your page:1234567891011function App() { const isDark = useIsDarkMode() // your app's dark mode detection return ( <ChatWidget domain="docs.myapp.com" theme={isDark ? 'light' : 'dark'} navigate={navigate} /> ) }
onClick prop to toggle the chat drawer:12345678910111213141516171819202122232425function MyTrigger({ onClick }: { onClick: () => void }) { return ( <button onClick={onClick} style={{ position: 'fixed', bottom: 24, right: 24, padding: '12px 20px', borderRadius: 12, background: '#0a0a0a', color: '#fff', cursor: 'pointer', }} > 💬 Ask AI </button> ) } <ChatWidget domain="docs.myapp.com" trigger={MyTrigger} navigate={navigate} />
.holocron-chat container so you can style them with your own page CSS. The default pill is hidden when a custom trigger is provided.useChatWidget() hook gives you full control over the chat from anywhere in your app:123456789101112131415import { useChatWidget } from '@holocron.so/vite/chat' function ChatControls() { const { isOpen, isGenerating, messages, open, close, toggle, clear } = useChatWidget() return ( <div> <p>Chat is {isOpen ? 'open' : 'closed'}</p> <p>{messages.length} messages</p> <button onClick={toggle}>Toggle</button> <button onClick={clear}>New chat</button> </div> ) }
isOpenbooleanisGeneratingbooleanmessagesChatMessage[]open() => voidclose() => voidtoggle() => voidclear() => voidrun function:1234567891011import { defineTool } from '@holocron.so/vite/chat' import { z } from 'zod' const timeTool = defineTool({ name: 'get_time', description: 'Get the current date and time.', input: z.object({}), async run() { return { time: new Date().toISOString() } }, })
tools prop:12345<ChatWidget domain="docs.myapp.com" tools={[timeTool]} navigate={navigate} />
document.modelContext (the WebMCP standard) so browser AI agents can discover them. Set exposeToModelContext: false to keep a tool internal to the holocron widget.bash is reserved for the server-side docs search tool.description string property is automatically added to every tool's input schema. The model fills it with a short summary like "Get the current time", which is shown as the tool call label in the chat UI. Your run function receives it inside input but can ignore it.pageTools() generates a set of browser automation tools from page declarations. These tools let the AI navigate pages, fill inputs, and highlight elements with an onboarding-style spotlight:123456789101112131415161718192021222324252627282930import { pageTools } from '@holocron.so/vite/chat' const browserTools = pageTools([ { path: '/settings', description: 'User settings page with email, name, and notification preferences.', actions: [ { name: 'update_email', description: 'Type into the email input', selector: 'input[name=email]', }, { name: 'save', description: 'Save settings', selector: 'button[data-action=save]', }, ], }, { path: '/billing', description: 'Billing page with subscription management.', }, ]) <ChatWidget domain="docs.myapp.com" tools={browserTools} navigate={navigate} />
| Tool | Description |
browser_navigate | Navigate to a declared page path |
browser_type | Type text into an input or textarea |
browser_select | Select a value in a dropdown |
browser_highlight | Show a persistent spotlight overlay on an element with a description card |
browser_read_page | List all interactive elements on the current page |
browser_highlight to point users at elements instead of clicking them directly. This keeps the user in control. For example, if a user asks "change my email", the AI types the new value with browser_type, then highlights the Save button with a message like "Click here to save".needsApproval on the tool definition:1234567891011const deleteTool = defineTool({ name: 'delete_account', description: 'Delete the user account.', input: z.object({ confirm: z.boolean() }), needsApproval: true, // always ask async run({ input }) { // only runs after user approves await deleteAccount() return { deleted: true } }, })
needsApproval can also be a function that returns true, false, or { message: string } with a custom confirmation message:1234needsApproval: ({ input }) => { if (input.force) return { message: 'This will permanently delete all data.' } return false }
browser_type, browser_select), wrap sensitive elements with data-holocron-requires-approval:123<div data-holocron-requires-approval="This will delete your account"> <button data-action="delete-account">Delete account</button> </div>
context prop. The object is serialized as XML in the system prompt:12345678910<ChatWidget domain="docs.myapp.com" context={{ userId: 'u_123', email: 'user@example.com', plan: 'pro', currentProject: { name: 'My App', id: 'prj_abc' }, }} navigate={navigate} />
holocron_chat).localStorage and sent via the x-holocron-chat-session header.localStorage; the server stores only the message history.clear() rotates to a fresh session; the previous conversation stays in the list.trigger is provided, the widget shows a textarea pill at the bottom of the page:backdrop-filter: blur(10px) surface with layered shadows for depth, matching the design language of fin.ai.