Appearance
Widget Integration
Learn how to install, customize, and optimize the Carla widget in your Next.js application.
What is the Carla Widget?
The Carla widget is a lightweight, embeddable chat interface that:
- Connects your users to the AI assistant powered by your API routes
- Loads asynchronously to avoid blocking page load
- Works with all pages or specific routes you choose
- Auto-cleans up when navigating between pages
- Supports TypeScript and JavaScript projects
Quick Installation
Install the widget with a single command:
bash
npx @interworky/carla-nextjs installThis will:
- Detect your project type (TypeScript/JavaScript)
- Create a
InterworkyWidgetcomponent in yourcomponents/directory - Add the widget to your root layout
- Configure it with your API key from
.env.local
Installation Options
Interactive Setup (Recommended)
The interactive wizard guides you through configuration:
bash
npx @interworky/carla-nextjs interactiveYou'll be prompted for:
- Which pages to display Carla on (all or specific)
- Load delay timing
- Landing page mode (minimal UI)
Command Line Options
Install with specific options:
bash
# Install on all pages (default)
npx @interworky/carla-nextjs install
# Install on specific pages only
npx @interworky/carla-nextjs install --pages "/,/products,/pricing"
# Custom delay (in milliseconds)
npx @interworky/carla-nextjs install --delay 2000
# Landing page mode (minimal UI)
npx @interworky/carla-nextjs install --landing
# Combine options
npx @interworky/carla-nextjs install --pages "/,/docs" --delay 1000 --landingHow It Works
Installation Process
Finds your layout file:
src/app/layout.tsx (or .js) app/layout.tsx (or .js)Creates widget component:
src/components/InterworkyWidget.tsx (or .jsx) components/InterworkyWidget.tsx (or .jsx)Updates your layout:
- Adds import statement
- Inserts
<InterworkyWidget />in the body
Generated Widget Component
TypeScript version:
typescript
'use client';
import { useEffect } from 'react';
import { usePathname } from 'next/navigation';
const SCRIPT_SRC = 'https://storage.googleapis.com/multisync/interworky/production/interworky.js';
const API_KEY = process.env.NEXT_PUBLIC_CARLA_API_KEY!;
export default function InterworkyWidget() {
const pathname = usePathname();
useEffect(() => {
// Delay script loading to prioritize core content rendering
const timeoutId = setTimeout(() => {
const script = document.createElement('script');
script.src = SCRIPT_SRC;
script.dataset.apiKey = API_KEY;
script.dataset.position = 'bottom-50 right-50';
script.async = true;
script.defer = true;
script.onload = () => {
setTimeout(() => {
window.Interworky?.init?.();
}, 100);
};
script.onerror = e => {
console.error('Interworky Plugin failed to load', e);
};
document.body.appendChild(script);
}, 1500); // Delay loading until after critical content is rendered
return () => {
clearTimeout(timeoutId);
window.Interworky?.remove?.();
document.querySelectorAll('script[data-api-key]').forEach(s => s.remove());
};
}, [pathname]);
return null;
}Updated Layout File
Before:
tsx
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}After:
tsx
import InterworkyWidget from '../components/InterworkyWidget';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<InterworkyWidget />
{children}
</body>
</html>
);
}Configuration
Environment Variables
The widget requires your Carla API key:
bash
# .env.local or .env
NEXT_PUBLIC_CARLA_API_KEY="your-api-key-here"TIP
The NEXT_PUBLIC_ prefix makes the key available to client-side code. This is safe because the key is scoped to your domain.
Getting your API key:
- Visit interworky.com
- Sign in or create an account
- Navigate to Integrations in the sidebar
- Copy your API key
Page-Specific Display
Show Carla only on specific pages:
During installation:
bash
npx @interworky/carla-nextjs install --pages "/,/products,/pricing"Manual configuration: Edit the generated widget component:
typescript
export default function InterworkyWidget() {
const pathname = usePathname();
useEffect(() => {
// Only show on home, products, and pricing pages
if (pathname === '/' || pathname === '/products' || pathname === '/pricing') {
const timeoutId = setTimeout(() => {
// ... widget loading code
}, 1500);
return () => {
clearTimeout(timeoutId);
window.Interworky?.remove?.();
};
}
}, [pathname]);
return null;
}Load Delay
Control when the widget loads:
typescript
// Default: 1500ms (1.5 seconds)
const timeoutId = setTimeout(() => {
// Load widget
}, 1500);Recommended delays:
- Fast connection: 1000ms (1 second)
- Balanced (default): 1500ms (1.5 seconds)
- Slow connection/heavy page: 2000-3000ms (2-3 seconds)
TIP
The delay ensures your main content loads first, improving perceived performance.
Widget Position
Customize the widget position via data attributes:
typescript
script.dataset.position = 'bottom-50 right-50';Position options:
bottom-right(default)bottom-lefttop-righttop-left- Custom:
bottom-50 right-50(pixels from edge)
Landing Page Mode
Enable minimal UI for landing pages:
bash
npx @interworky/carla-nextjs install --landingOr manually:
typescript
script.dataset.landing = 'true';This creates a more subtle widget appearance suitable for marketing pages.
Customization
Custom Styling
Override default styles in your global CSS:
css
/* Custom widget button styling */
[data-interworky-button] {
background: #your-brand-color !important;
border-radius: 50% !important;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15) !important;
}
/* Custom chat window styling */
[data-interworky-chat] {
border-radius: 12px !important;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2) !important;
}
/* Custom header color */
[data-interworky-header] {
background: linear-gradient(135deg, #your-color-1, #your-color-2) !important;
}Conditional Loading
Load based on user authentication status:
typescript
'use client';
import { useEffect } from 'react';
import { usePathname } from 'next/navigation';
import { useSession } from 'next-auth/react';
export default function InterworkyWidget() {
const pathname = usePathname();
const { data: session } = useSession();
useEffect(() => {
// Only show to authenticated users
if (!session) return;
const timeoutId = setTimeout(() => {
// ... widget loading code
}, 1500);
return () => {
clearTimeout(timeoutId);
window.Interworky?.remove?.();
};
}, [pathname, session]);
return null;
}User Context
Pass user information to the widget:
typescript
useEffect(() => {
const timeoutId = setTimeout(() => {
const script = document.createElement('script');
script.src = SCRIPT_SRC;
script.dataset.apiKey = API_KEY;
// Add user context
if (session?.user) {
script.dataset.userId = session.user.id;
script.dataset.userName = session.user.name || '';
script.dataset.userEmail = session.user.email || '';
}
document.body.appendChild(script);
}, 1500);
return () => {
clearTimeout(timeoutId);
window.Interworky?.remove?.();
};
}, [pathname, session]);Performance Optimization
Loading Strategy
The widget uses several optimizations:
Async + Defer:
typescriptscript.async = true; script.defer = true;Doesn't block page rendering or parsing
Delayed Loading:
typescriptsetTimeout(() => { /* load widget */ }, 1500);Waits for critical content to load first
Lazy Initialization:
typescriptscript.onload = () => { setTimeout(() => { window.Interworky?.init?.(); }, 100); };Initializes after script loads, with additional delay
Bundle Size
The widget script is:
- Compressed: ~45KB gzipped
- Cached: Browser caches after first load
- CDN-delivered: Fast global delivery via Google Cloud Storage
Impact on Lighthouse Score
Typical impact on Lighthouse metrics:
- Performance: -1 to -3 points (minimal)
- First Contentful Paint: No impact (async loading)
- Time to Interactive: +0.1-0.2s (negligible)
- Cumulative Layout Shift: 0 (no layout shift)
TIP
The 1.5s delay ensures the widget doesn't affect your initial page load metrics.
Cleanup and Unmounting
The widget automatically cleans up on page navigation:
typescript
return () => {
clearTimeout(timeoutId); // Cancel delayed loading
window.Interworky?.remove?.(); // Remove widget UI
document.querySelectorAll('script[data-api-key]').forEach(s => s.remove()); // Remove script tags
};This prevents:
- Memory leaks
- Duplicate widgets
- Stale event listeners
Troubleshooting
Widget Not Appearing
Problem: Widget doesn't show on your page
Solutions:
Check API key:
bashecho $NEXT_PUBLIC_CARLA_API_KEYMake sure it's set in
.env.localRestart dev server:
bash# Environment changes require restart npm run devCheck browser console: Look for errors related to Interworky
Verify script loading: Open DevTools → Network tab → Look for
interworky.js
Widget Loads Too Slowly
Problem: Widget appears too late
Solutions:
Reduce delay:
typescriptsetTimeout(() => { /* load */ }, 1000); // Reduced from 1500msCheck network speed: Slow connections may take longer to download the script
Multiple Widgets Appear
Problem: Widget duplicates on navigation
Solutions:
Check cleanup function: Ensure the
return () => {}cleanup is presentVerify component structure: Widget should be in layout, not individual pages
Clear cache and reload:
bashrm -rf .next npm run dev
Widget Doesn't Update After Changes
Problem: Changes to widget component aren't reflected
Solutions:
Hard refresh browser:
- Mac:
Cmd + Shift + R - Windows/Linux:
Ctrl + Shift + R
- Mac:
Clear Next.js cache:
bashrm -rf .next npm run dev
TypeScript Errors
Problem: TypeScript complains about window.Interworky
Solution:
Add global type declaration to your widget:
typescript
declare global {
interface Window {
Interworky?: {
init?: () => void;
remove?: () => void;
};
}
}Or create a types/interworky.d.ts file:
typescript
interface Window {
Interworky?: {
init?: () => void;
remove?: () => void;
open?: () => void;
close?: () => void;
};
}Advanced Usage
Programmatic Control
Control the widget programmatically:
typescript
'use client';
import { useEffect, useRef } from 'react';
export default function CustomWidget() {
const widgetLoadedRef = useRef(false);
useEffect(() => {
const timeoutId = setTimeout(() => {
const script = document.createElement('script');
script.src = 'https://storage.googleapis.com/multisync/interworky/production/interworky.js';
script.dataset.apiKey = process.env.NEXT_PUBLIC_CARLA_API_KEY!;
script.onload = () => {
widgetLoadedRef.current = true;
};
document.body.appendChild(script);
}, 1500);
return () => clearTimeout(timeoutId);
}, []);
// Open widget programmatically
const openWidget = () => {
if (widgetLoadedRef.current) {
window.Interworky?.open?.();
}
};
// Close widget programmatically
const closeWidget = () => {
if (widgetLoadedRef.current) {
window.Interworky?.close?.();
}
};
return (
<div>
<button onClick={openWidget}>Open Chat</button>
<button onClick={closeWidget}>Close Chat</button>
</div>
);
}Event Tracking
Track widget interactions:
typescript
useEffect(() => {
const handleMessage = (event: MessageEvent) => {
if (event.data?.type === 'interworky:opened') {
// Track widget open
analytics.track('Widget Opened');
}
if (event.data?.type === 'interworky:closed') {
// Track widget close
analytics.track('Widget Closed');
}
};
window.addEventListener('message', handleMessage);
return () => {
window.removeEventListener('message', handleMessage);
};
}, []);A/B Testing
Test different widget configurations:
typescript
'use client';
import { useEffect } from 'react';
import { usePathname } from 'next/navigation';
export default function InterworkyWidget() {
const pathname = usePathname();
useEffect(() => {
// A/B test: 50% get 1s delay, 50% get 2s delay
const delay = Math.random() < 0.5 ? 1000 : 2000;
const timeoutId = setTimeout(() => {
const script = document.createElement('script');
script.src = 'https://storage.googleapis.com/multisync/interworky/production/interworky.js';
script.dataset.apiKey = process.env.NEXT_PUBLIC_CARLA_API_KEY!;
script.dataset.variant = delay === 1000 ? 'fast' : 'slow';
document.body.appendChild(script);
}, delay);
return () => clearTimeout(timeoutId);
}, [pathname]);
return null;
}Best Practices
1. Use Appropriate Delays
typescript
// ✅ Good - Balanced
setTimeout(() => {
/* load */
}, 1500);
// ❌ Too fast - May affect page load metrics
setTimeout(() => {
/* load */
}, 100);
// ❌ Too slow - Poor user experience
setTimeout(() => {
/* load */
}, 5000);2. Place in Layout, Not Pages
typescript
// ✅ Good - In app/layout.tsx
export default function RootLayout({ children }) {
return (
<html>
<body>
<InterworkyWidget />
{children}
</body>
</html>
)
}
// ❌ Bad - In individual pages
export default function HomePage() {
return (
<div>
<InterworkyWidget /> {/* Don't do this */}
{/* Page content */}
</div>
)
}3. Always Include Cleanup
typescript
// ✅ Good - Proper cleanup
useEffect(() => {
const timeoutId = setTimeout(() => {
/* load */
}, 1500);
return () => {
clearTimeout(timeoutId);
window.Interworky?.remove?.();
};
}, [pathname]);
// ❌ Bad - No cleanup
useEffect(() => {
setTimeout(() => {
/* load */
}, 1500);
// Missing cleanup!
}, [pathname]);4. Handle Errors Gracefully
typescript
// ✅ Good - Error handling
script.onerror = e => {
console.error('Interworky Plugin failed to load', e);
// Optionally: report to error tracking service
};
// ❌ Bad - No error handling
script.onload = () => {
window.Interworky?.init?.();
};5. Keep API Key Secure
bash
# ✅ Good - In environment variable
NEXT_PUBLIC_CARLA_API_KEY="sk_..."
# ❌ Bad - Hardcoded in component
const API_KEY = 'sk_...' // Never do this!Next Steps
Now that your widget is installed:
- Tool Generation - Add AI capabilities
- API Scanning - Connect your API routes
- Customization - Tailor Carla to your brand
- Dashboard - Manage and monitor your assistant