Alchemist: Embedded Agent — Floating Widget
Add a persistent, floating chat widget that users can toggle on any page.
The floating widget is a persistent, toggleable chat interface that floats over your application content. It appears as a button in the bottom-right corner (or custom position) and opens a chat panel when clicked.
When to use the floating widget
Use the floating widget when:
- The agent is supplementary to your main application (support, help, search assistant)
- You want a low-intrusion interface that users can hide when not needed
- You want the agent available on every page with minimal code
- You want persistent state across page navigation
For a prominent agent interface that is part of your main navigation, use the inline panel instead.
Basic setup
1. Install the feature pack
Follow the installation guide first. Once installed, your project has a FloatingWidget component ready to import.
2. Import the component
In your root layout or main app file:
import { FloatingWidget } from '@/components/embedded-agent/FloatingWidget';3. Render the widget
Place the component once in your root layout (it renders globally and floats over all content):
export default function RootLayout({ children }) {
return (
<>
<header>
{/* Your app header */}
</header>
<main>
{children}
</main>
<FloatingWidget
projectId="proj_..."
sessionEndpoint="/api/chat/session"
/>
</>
);
}The widget is now available on every page. Users see a chat button in the bottom-right corner.
Component props
interface FloatingWidgetProps {
// Required
projectId: string; // Your Alchemist project ID
sessionEndpoint: string; // Your backend session endpoint
// Optional: Identity
externalUserId?: string; // Authenticated user ID (email)
metadata?: Record<string, any>; // Additional user context
// Optional: Appearance
position?: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left';
buttonLabel?: string; // Label on the toggle button (default: "Chat")
buttonIcon?: React.ReactNode; // Custom icon for the button
buttonColor?: string; // CSS color for button background
theme?: 'light' | 'dark'; // Default: inherits from page
// Optional: Behavior
initiallyOpen?: boolean; // Open by default (default: false)
hideOnMobile?: boolean; // Don't show on mobile devices (default: false)
allowMinimize?: boolean; // Show minimize button (default: true)
persistState?: boolean; // Remember open/closed state (default: true)
// Optional: Callbacks
onOpen?: () => void; // Fired when widget opens
onClose?: () => void; // Fired when widget closes
onError?: (error: EmbedError) => void; // Fired on chat errors
}Positioning
Custom position
Place the widget in a different corner:
<FloatingWidget
projectId="..."
sessionEndpoint="..."
position="bottom-left"
/>Custom offset
Adjust the distance from the edge via CSS:
<div className="custom-widget-wrapper">
<FloatingWidget projectId="..." sessionEndpoint="..." />
</div>.custom-widget-wrapper :global(.embed-widget-button) {
bottom: 2rem; /* Default: 1rem */
right: 2rem; /* Default: 1rem */
}Authentication & identity
Authenticated users
Pass the logged-in user’s email:
const user = useAuthContext();
<FloatingWidget
projectId="proj_..."
sessionEndpoint="/api/chat/session"
externalUserId={user.email}
metadata={{ plan: user.plan }}
/>The user’s conversations are retained across sessions and pages.
Anonymous/visitor mode
For public pages or unauthenticated visitors:
<FloatingWidget
projectId="proj_..."
sessionEndpoint="/api/chat/session"
// externalUserId omitted → anonymous mode
/>Upgrading sessions
When a visitor logs in, upgrade their anonymous session to authenticated:
// In your login handler
const newSessionId = await fetch('/api/chat/session/upgrade', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
currentSessionId: anonSessionId,
externalUserId: user.email,
}),
});Their previous conversation is merged into the authenticated session.
Styling & customization
Custom button appearance
<FloatingWidget
projectId="..."
sessionEndpoint="..."
buttonLabel="💬 Get Help"
buttonColor="#6366f1" // Indigo
position="bottom-left"
/>Custom icon
import { HelpCircle } from 'lucide-react';
<FloatingWidget
projectId="..."
sessionEndpoint="..."
buttonIcon={<HelpCircle size={24} />}
/>Dark mode override
<FloatingWidget
projectId="..."
sessionEndpoint="..."
theme="dark"
/>Mobile behavior
Hide the widget on mobile devices:
<FloatingWidget
projectId="..."
sessionEndpoint="..."
hideOnMobile={true}
/>Or show a different component instead:
{isMobile ? (
<MobileAssistantTab projectId="..." />
) : (
<FloatingWidget projectId="..." sessionEndpoint="..." />
)}State persistence
By default, the widget remembers whether the user opened or closed it (stored in localStorage). On each page load, it restores that state.
Disable persistence
<FloatingWidget
projectId="..."
sessionEndpoint="..."
persistState={false}
/>Reset state
Clear the persisted state programmatically:
localStorage.removeItem('chipp-embed-widget-state');Opening/closing programmatically
You can control the widget state via a ref:
import { useRef } from 'react';
const widgetRef = useRef<FloatingWidgetHandle>(null);
const handleOpenChat = () => {
widgetRef.current?.open();
};
const handleCloseChat = () => {
widgetRef.current?.close();
};
<>
<button onClick={handleOpenChat}>Contact Support</button>
<FloatingWidget ref={widgetRef} projectId="..." sessionEndpoint="..." />
</>Callbacks & events
Widget lifecycle
<FloatingWidget
projectId="..."
sessionEndpoint="..."
onOpen={() => {
console.log('User opened chat');
analytics.track('chat_opened');
}}
onClose={() => {
console.log('User closed chat');
analytics.track('chat_closed');
}}
/>Error handling
const handleChatError = (error: EmbedError) => {
if (error.code === 'AUTH_FAILED') {
// Prompt to re-authenticate
showModal('Login Required', 'Please sign in to use chat.');
} else {
showToast(`Chat unavailable: ${error.message}`);
}
};
<FloatingWidget
projectId="..."
sessionEndpoint="..."
onError={handleChatError}
/>Responsive layout
The widget is responsive by default. On narrow screens:
- The chat panel shrinks to fit the available width
- The button position adjusts to avoid keyboard on mobile (if the user taps the input)
Disable on specific breakpoints
const isMobile = useMediaQuery('(max-width: 768px)');
const isSmallScreen = useMediaQuery('(max-height: 600px)');
return !isMobile && !isSmallScreen ? (
<FloatingWidget projectId="..." sessionEndpoint="..." />
) : null;Z-index & layering
The widget has a default z-index of 999 (below modals, above most content). Adjust if needed:
:global(.embed-widget-container) {
z-index: 50 !important; /* Higher or lower than default */
}Conversation history
The widget maintains the current conversation across page navigations. Reloading the page or starting a new session (different externalUserId) creates a new conversation.
Clearing the conversation
Users can clear the conversation by clicking the Clear button in the widget header. You can also programmatically clear it:
localStorage.removeItem('chipp-embed-widget-messages');Troubleshooting
”Widget button not visible”
- Verify the component is rendered at the root level (not conditionally hidden).
- Check z-index: other elements may be covering it. Use browser DevTools to inspect z-index values.
- Ensure CSS is loaded (check for styling overrides).
”Widget opens but chat doesn’t load”
- Verify
projectIdandsessionEndpointare correct. - Check browser console for network errors (401, 404, 5xx).
- Ensure your backend session endpoint is responding correctly.
”Session expires quickly or chat resets”
- Verify token TTL is reasonable (default: 8 hours for authenticated, 1 hour for anonymous).
- If using anonymous mode, clearing the browser’s cookies will reset the session.
- For authenticated users, check that
externalUserIdis consistent across page loads.
”Widget not persisting state across page reloads”
- Ensure
persistState={true}(default). - Check if browser localStorage is available and not full (quota exceeded).
- Inspect localStorage:
localStorage.getItem('chipp-embed-widget-state')should return JSON.
Next steps
- Inline panel — Embed a full panel as part of your layout
- Backend client — Integrate agent calls in your server routes