Basic pattern — useEffect
The simplest approach: load the SDK script and init once on mount.import { useEffect } from 'react';
export function EPISChat() {
useEffect(() => {
// Load SDK if not already loaded
if (window.EPIS) {
window.EPIS.mountChat('#epis-chat');
return () => window.EPIS?.unmount('#epis-chat');
}
const script = document.createElement('script');
script.src = 'https://cdn.pastpaperlm.com/episbox/v1.js';
script.defer = true;
script.onload = () => {
window.EPIS.init({
clientId: 'your-client-id',
theme: { preset: 'dark', primary: '#4f46e5' }
}).mountChat('#epis-chat');
};
document.head.appendChild(script);
return () => window.EPIS?.unmount('#epis-chat');
}, []);
return <div id="epis-chat" style={{ width: '100%', height: '600px' }} />;
}
The cleanup function
window.EPIS?.unmount('#epis-chat') runs when the component unmounts — preventing iframe leaks if the user navigates away and back.Custom hook — reusable across your app
Extract the logic into a hook so any component can mount EPIS:// hooks/use-episbox.ts
import { useEffect, useRef } from 'react';
interface EpisBoxConfig {
clientId: string;
theme?: {
preset?: 'dark' | 'light' | 'auraspace' | 'minimal';
primary?: string;
secondary?: string;
background?: string;
surface?: string;
text?: string;
font?: string;
radius?: string;
mode?: 'dark' | 'light' | 'system';
};
locale?: 'en' | 'si' | 'ta';
model?: 'lite' | 'bright';
}
function loadScript(): Promise<void> {
return new Promise((resolve, reject) => {
if (window.EPIS) { resolve(); return; }
const existing = document.querySelector(
'script[src="https://cdn.pastpaperlm.com/episbox/v1.js"]'
);
if (existing) {
existing.addEventListener('load', () => resolve());
return;
}
const script = document.createElement('script');
script.src = 'https://cdn.pastpaperlm.com/episbox/v1.js';
script.defer = true;
script.onload = () => resolve();
script.onerror = reject;
document.head.appendChild(script);
});
}
export function useEpisBox(
selector: string,
config: EpisBoxConfig
) {
const initialized = useRef(false);
useEffect(() => {
if (initialized.current) return;
loadScript().then(() => {
if (!window.EPIS._initialized) {
window.EPIS.init(config);
}
window.EPIS.mountChat(selector);
initialized.current = true;
});
return () => {
window.EPIS?.unmount(selector);
initialized.current = false;
};
}, [selector]); // config intentionally excluded — init runs once
}
// usage
import { useEpisBox } from '@/hooks/use-episbox';
export function ChatPage() {
useEpisBox('#epis-chat', {
clientId: 'your-client-id',
theme: { primary: '#4f46e5', mode: 'dark' }
});
return <div id="epis-chat" style={{ height: '100vh' }} />;
}
Next.js — App Router
Use a Client Component with dynamic import to avoid SSR issues:// components/epis-chat.tsx
'use client';
import { useEffect } from 'react';
export default function EPISChat({ clientId }: { clientId: string }) {
useEffect(() => {
const load = async () => {
if (!window.EPIS) {
await new Promise<void>((resolve) => {
const s = document.createElement('script');
s.src = 'https://cdn.pastpaperlm.com/episbox/v1.js';
s.defer = true;
s.onload = () => resolve();
document.head.appendChild(s);
});
}
window.EPIS.init({ clientId, theme: { preset: 'dark' } })
.mountChat('#epis-chat');
};
load();
return () => window.EPIS?.unmount('#epis-chat');
}, [clientId]);
return <div id="epis-chat" style={{ width: '100%', height: '100%' }} />;
}
// app/chat/page.tsx
import dynamic from 'next/dynamic';
const EPISChat = dynamic(() => import('@/components/epis-chat'), { ssr: false });
export default function ChatPage() {
return (
<div style={{ height: 'calc(100vh - 64px)' }}>
<EPISChat clientId="your-client-id" />
</div>
);
}
Handling auth events in React
'use client';
import { useEffect, useState } from 'react';
export function EPISWithAuth() {
const [user, setUser] = useState<{ name: string } | null>(null);
useEffect(() => {
// ... load script and init ...
window.EPIS
.on('auth', (data: { user: { name: string } }) => {
setUser(data.user);
})
.on('auth-error', (err: { code: string; message: string }) => {
console.error('Auth failed:', err.code);
});
return () => {
window.EPIS?.off('auth', setUser);
window.EPIS?.unmount('#epis-chat');
};
}, []);
return (
<>
{user && <p>Welcome back, {user.name}!</p>}
<div id="epis-chat" style={{ height: '600px' }} />
</>
);
}
TypeScript types
Add these declarations to avoidwindow.EPIS type errors:
// types/episbox.d.ts
interface EpisBoxInstance {
init(config: EpisBoxConfig): this;
mountChat(selector: string, options?: Partial<EpisBoxConfig>): this;
unmount(selector: string): this;
unmountAll(): this;
on(event: EpisBoxEvent, callback: (payload: any) => void): this;
off(event: EpisBoxEvent, callback: (payload: any) => void): this;
VERSION: string;
THEMES: Record<string, EpisBoxTheme>;
_initialized: boolean;
}
type EpisBoxEvent = 'ready' | 'auth' | 'auth-error' | 'message' | 'error' | 'unmount';
interface EpisBoxConfig {
clientId: string;
theme?: EpisBoxTheme;
locale?: 'en' | 'si' | 'ta';
persona?: string;
model?: 'lite' | 'bright';
}
interface EpisBoxTheme {
preset?: 'dark' | 'light' | 'auraspace' | 'minimal';
primary?: string;
secondary?: string;
background?: string;
surface?: string;
text?: string;
textMuted?: string;
border?: string;
font?: string;
fontSize?: string;
radius?: string;
radiusSmall?: string;
mode?: 'dark' | 'light' | 'system';
}
declare global {
interface Window {
EPIS: EpisBoxInstance;
}
}