Alchemist Operations

Alchemist: Embedded Agent — Floating Widget

Add a persistent, floating chat widget that users can toggle on any page.

|View as Markdown
Chipp EngineeringEngineering at Chipp
|1 min read
#alchemist#embedded-agent#ui#floating-widget

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:

tsx
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):

tsx
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

typescript
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:

tsx
<FloatingWidget
  projectId="..."
  sessionEndpoint="..."
  position="bottom-left"
/>

Custom offset

Adjust the distance from the edge via CSS:

tsx
<div className="custom-widget-wrapper">
  <FloatingWidget projectId="..." sessionEndpoint="..." />
</div>
css
.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:

tsx
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:

tsx
<FloatingWidget
  projectId="proj_..."
  sessionEndpoint="/api/chat/session"
  // externalUserId omitted → anonymous mode
/>

Upgrading sessions

When a visitor logs in, upgrade their anonymous session to authenticated:

tsx
// 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

tsx
<FloatingWidget
  projectId="..."
  sessionEndpoint="..."
  buttonLabel="💬 Get Help"
  buttonColor="#6366f1"  // Indigo
  position="bottom-left"
/>

Custom icon

tsx
import { HelpCircle } from 'lucide-react';

<FloatingWidget
  projectId="..."
  sessionEndpoint="..."
  buttonIcon={<HelpCircle size={24} />}
/>

Dark mode override

tsx
<FloatingWidget
  projectId="..."
  sessionEndpoint="..."
  theme="dark"
/>

Mobile behavior

Hide the widget on mobile devices:

tsx
<FloatingWidget
  projectId="..."
  sessionEndpoint="..."
  hideOnMobile={true}
/>

Or show a different component instead:

tsx
{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

tsx
<FloatingWidget
  projectId="..."
  sessionEndpoint="..."
  persistState={false}
/>

Reset state

Clear the persisted state programmatically:

tsx
localStorage.removeItem('chipp-embed-widget-state');

Opening/closing programmatically

You can control the widget state via a ref:

tsx
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

tsx
<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

tsx
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

tsx
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:

css
: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:

tsx
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 projectId and sessionEndpoint are 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 externalUserId is 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