React Integration
Learn how to integrate AI Interview into your React application with a reusable component.
Installation
npm install @ai-interview/sdk
# or
yarn add @ai-interview/sdk
Complete Example Component
import { useEffect, useRef, useState } from 'react';
import InterviewSDK, { InterviewEmbed } from '@ai-interview/sdk';
interface InterviewProps {
inviteToken: string;
onCompleted?: (sessionId: string) => void;
onError?: (error: { message: string; code: string }) => void;
}
export function Interview({ inviteToken, onCompleted, onError }: InterviewProps) {
const containerRef = useRef<HTMLDivElement>(null);
const embedRef = useRef<InterviewEmbed | null>(null);
const [status, setStatus] = useState<'loading' | 'ready' | 'in-progress' | 'completed' | 'error'>('loading');
const [progress, setProgress] = useState(0);
const [errorMessage, setErrorMessage] = useState<string>('');
useEffect(() => {
if (!containerRef.current || embedRef.current) return;
try {
// Mount the interview embed
const embed = InterviewSDK.mount(containerRef.current, {
invite: inviteToken,
theme: {
primary: '#22d3ee',
background: '#0f172a',
text: '#e5e7eb',
},
});
embedRef.current = embed;
// Event handlers
embed.on('start', () => {
setStatus('in-progress');
console.log('Interview started');
});
embed.on('pause', () => {
console.log('Interview paused');
});
embed.on('resume', () => {
console.log('Interview resumed');
});
embed.on('progress', (payload) => {
setProgress(payload.percentage);
});
embed.on('completed', (payload) => {
setStatus('completed');
console.log('Interview completed:', payload.sessionId);
onCompleted?.(payload.sessionId);
});
embed.on('error', (payload) => {
setStatus('error');
setErrorMessage(payload.message);
console.error('Interview error:', payload);
onError?.(payload);
});
setStatus('ready');
} catch (error) {
setStatus('error');
setErrorMessage(error instanceof Error ? error.message : 'Failed to load interview');
console.error('Mount error:', error);
}
// Cleanup
return () => {
if (embedRef.current) {
embedRef.current.destroy();
embedRef.current = null;
}
};
}, [inviteToken, onCompleted, onError]);
return (
<div className="interview-wrapper">
<div ref={containerRef} className="interview-container" />
{status === 'in-progress' && progress > 0 && (
<div className="progress-bar">
<div className="progress-fill" style={{ width: `${progress}%` }} />
<span className="progress-text">{Math.round(progress)}% complete</span>
</div>
)}
{status === 'completed' && (
<div className="status-message success">
✓ Thank you! Your interview has been completed.
</div>
)}
{status === 'error' && (
<div className="status-message error">
✗ Error: {errorMessage}
</div>
)}
<style jsx>{`
.interview-wrapper {
width: 100%;
max-width: 900px;
margin: 0 auto;
}
.interview-container {
width: 100%;
height: 600px;
background: #1e293b;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
}
.progress-bar {
margin-top: 20px;
height: 40px;
background: #1e293b;
border-radius: 8px;
position: relative;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #22d3ee, #06b6d4);
transition: width 0.3s ease;
}
.progress-text {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: #e5e7eb;
font-weight: 600;
}
.status-message {
margin-top: 20px;
padding: 15px;
border-radius: 8px;
text-align: center;
font-weight: 500;
}
.status-message.success {
background: #10b981;
color: white;
}
.status-message.error {
background: #ef4444;
color: white;
}
@media (max-width: 768px) {
.interview-container {
height: 500px;
}
}
`}</style>
</div>
);
}
Usage in Your App
import { Interview } from './components/Interview';
function InterviewPage() {
const handleCompleted = (sessionId: string) => {
console.log('Interview completed:', sessionId);
// Navigate to thank you page
// router.push('/thank-you');
};
const handleError = (error: { message: string; code: string }) => {
console.error('Interview error:', error);
// Show error notification
};
return (
<div className="page">
<h1>Customer Feedback Interview</h1>
<p>Share your thoughts in a quick 5-minute voice interview</p>
<Interview
inviteToken="inv_abc123"
onCompleted={handleCompleted}
onError={handleError}
/>
</div>
);
}
With React Router
import { useParams } from 'react-router-dom';
import { Interview } from './components/Interview';
function InterviewPage() {
const { token } = useParams<{ token: string }>();
if (!token) {
return <div>Invalid interview link</div>;
}
return <Interview inviteToken={token} />;
}
// Route configuration
<Route path="/interview/:token" element={<InterviewPage />} />
With Next.js
// pages/interview/[token].tsx
import { useRouter } from 'next/router';
import { Interview } from '@/components/Interview';
export default function InterviewPage() {
const router = useRouter();
const { token } = router.query;
if (!token || typeof token !== 'string') {
return <div>Loading...</div>;
}
const handleCompleted = (sessionId: string) => {
router.push('/thank-you');
};
return (
<div>
<h1>AI Interview</h1>
<Interview
inviteToken={token}
onCompleted={handleCompleted}
/>
</div>
);
}
Fetching Tokens Dynamically
import { useState, useEffect } from 'react';
import { Interview } from './components/Interview';
function DynamicInterview({ userId }: { userId: string }) {
const [token, setToken] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
async function fetchToken() {
try {
const response = await fetch('/api/create-interview', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userId }),
});
if (!response.ok) {
throw new Error('Failed to create interview');
}
const data = await response.json();
setToken(data.inviteToken);
} catch (err) {
setError(err instanceof Error ? err.message : 'An error occurred');
} finally {
setLoading(false);
}
}
fetchToken();
}, [userId]);
if (loading) return <div>Loading interview...</div>;
if (error) return <div>Error: {error}</div>;
if (!token) return <div>No interview available</div>;
return <Interview inviteToken={token} />;
}
TypeScript Types
// types/interview.ts
export interface InterviewEmbedOptions {
invite: string;
baseUrl?: string;
theme?: {
primary?: string;
background?: string;
text?: string;
borderRadius?: string;
fontFamily?: string;
};
locale?: string;
autoStart?: boolean;
}
export interface InterviewCompletedPayload {
sessionId: string;
transcript?: string;
}
export interface InterviewErrorPayload {
message: string;
code: string;
}
export interface InterviewProgressPayload {
percentage: number;
}
Custom Hooks
Create a reusable hook:
// hooks/useInterview.ts
import { useEffect, useRef, useState } from 'react';
import InterviewSDK, { InterviewEmbed, InterviewEmbedOptions } from '@ai-interview/sdk';
export function useInterview(
containerRef: React.RefObject<HTMLElement>,
options: InterviewEmbedOptions
) {
const embedRef = useRef<InterviewEmbed | null>(null);
const [status, setStatus] = useState<'loading' | 'ready' | 'in-progress' | 'completed' | 'error'>('loading');
const [progress, setProgress] = useState(0);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
if (!containerRef.current || embedRef.current) return;
try {
const embed = InterviewSDK.mount(containerRef.current, options);
embedRef.current = embed;
embed.on('start', () => setStatus('in-progress'));
embed.on('progress', (p) => setProgress(p.percentage));
embed.on('completed', () => setStatus('completed'));
embed.on('error', (e) => {
setStatus('error');
setError(e.message);
});
setStatus('ready');
} catch (err) {
setStatus('error');
setError(err instanceof Error ? err.message : 'Unknown error');
}
return () => {
if (embedRef.current) {
embedRef.current.destroy();
embedRef.current = null;
}
};
}, [containerRef, options]);
return { status, progress, error, embed: embedRef.current };
}
// Usage
function InterviewComponent({ token }: { token: string }) {
const containerRef = useRef<HTMLDivElement>(null);
const { status, progress, error } = useInterview(containerRef, {
invite: token,
});
return (
<div>
<div ref={containerRef} style={{ height: '600px' }} />
{status === 'in-progress' && <p>Progress: {progress}%</p>}
{error && <p>Error: {error}</p>}
</div>
);
}
Best Practices
1. Error Boundaries
Wrap the interview component in an error boundary:
import { ErrorBoundary } from 'react-error-boundary';
function ErrorFallback({ error }: { error: Error }) {
return (
<div className="error-fallback">
<h2>Something went wrong</h2>
<p>{error.message}</p>
<button onClick={() => window.location.reload()}>
Try Again
</button>
</div>
);
}
function App() {
return (
<ErrorBoundary FallbackComponent={ErrorFallback}>
<Interview inviteToken={token} />
</ErrorBoundary>
);
}
2. Loading States
Show proper loading states:
{status === 'loading' && (
<div className="loading-spinner">
Loading interview...
</div>
)}
3. Cleanup on Unmount
Always clean up the embed on component unmount to prevent memory leaks:
useEffect(() => {
// ... mount logic
return () => {
if (embedRef.current) {
embedRef.current.destroy();
}
};
}, []);