React/Next.js integration
The SDK is a plain script with a global Chatoshi class. There's no npm package to install. In React, you load the script once and manage the chat instance through the component lifecycle.
React component
import { useEffect, useRef } from 'react';
const ChatWidget = ({ partnerKey, mode = 'default' }) => {
const chatRef = useRef(null);
const containerRef = useRef(null);
useEffect(() => {
const init = () => {
chatRef.current = new window.Chatoshi({
partnerKey,
mode,
container: mode === 'default' ? containerRef.current : undefined
});
};
if (window.Chatoshi) {
init();
} else {
const script = document.createElement('script');
script.src = 'https://aichat-sdk.chatoshi.ai/chatoshi-sdk.js';
script.async = true;
script.onload = init;
document.head.appendChild(script);
}
return () => {
chatRef.current?.destroy();
chatRef.current = null;
};
}, [partnerKey, mode]);
if (mode !== 'default') {
return null;
}
return <div ref={containerRef} style={{ width: '100%', height: '500px' }} />;
};
export default ChatWidget;
Use it anywhere:
import ChatWidget from './components/ChatWidget';
function App() {
return (
<main>
<h1>My app</h1>
<ChatWidget partnerKey={process.env.REACT_APP_CHATOSHI_KEY} mode="default" />
</main>
);
}
Next.js
With the App Router, use the built-in Script component:
import { useEffect, useRef, useState } from 'react';
import Script from 'next/script';
export default function HomePage() {
const chatRef = useRef(null);
const [sdkLoaded, setSdkLoaded] = useState(false);
useEffect(() => {
if (!sdkLoaded || chatRef.current) {
return;
}
chatRef.current = new window.Chatoshi({
partnerKey: process.env.NEXT_PUBLIC_CHATOSHI_PARTNER_KEY,
mode: 'popup'
});
return () => {
chatRef.current?.destroy();
chatRef.current = null;
};
}, [sdkLoaded]);
return (
<>
<Script
src="https://aichat-sdk.chatoshi.ai/chatoshi-sdk.js"
strategy="afterInteractive"
onLoad={() => setSdkLoaded(true)}
/>
<h1>My app</h1>
</>
);
}
useChatoshi hook
For anything beyond a single component, wrap the lifecycle in a hook:
// hooks/useChatoshi.js
import { useEffect, useRef, useState } from 'react';
export const useChatoshi = (options) => {
const chatRef = useRef(null);
const [isReady, setIsReady] = useState(false);
useEffect(() => {
const load = () =>
new Promise((resolve, reject) => {
if (window.Chatoshi) {
resolve();
return;
}
const script = document.createElement('script');
script.src = 'https://aichat-sdk.chatoshi.ai/chatoshi-sdk.js';
script.onload = resolve;
script.onerror = reject;
document.head.appendChild(script);
});
load().then(() => {
chatRef.current = new window.Chatoshi(options);
chatRef.current.on('app:ready', () => setIsReady(true));
});
return () => {
chatRef.current?.destroy();
chatRef.current = null;
setIsReady(false);
};
}, [options.partnerKey, options.mode]);
return {
chat: chatRef.current,
isReady,
open: () => chatRef.current?.open(),
close: () => chatRef.current?.close(),
toggle: () => chatRef.current?.toggle()
};
};
import { useChatoshi } from '../hooks/useChatoshi';
const SupportChat = () => {
const { isReady, open, close } = useChatoshi({
partnerKey: process.env.NEXT_PUBLIC_CHATOSHI_PARTNER_KEY,
mode: 'drawer',
drawerOptions: { width: '400px', position: 'right' }
});
return (
<div>
{!isReady && <p>Loading chat…</p>}
<button onClick={open}>Open chat</button>
<button onClick={close}>Close chat</button>
</div>
);
};
App-wide chat (App Router)
To share one chat instance across the app, provide it from the layout:
// app/ChatProvider.js
'use client';
import { createContext, useContext, useEffect, useRef, useState } from 'react';
const ChatContext = createContext(null);
export const useChat = () => useContext(ChatContext);
export const ChatProvider = ({ children, config }) => {
const chatRef = useRef(null);
const [isReady, setIsReady] = useState(false);
useEffect(() => {
const script = document.createElement('script');
script.src = 'https://aichat-sdk.chatoshi.ai/chatoshi-sdk.js';
script.onload = () => {
chatRef.current = new window.Chatoshi(config);
chatRef.current.on('app:ready', () => setIsReady(true));
};
document.head.appendChild(script);
return () => {
chatRef.current?.destroy();
};
}, [config]);
return (
<ChatContext.Provider value={{ chat: chatRef.current, isReady }}>
{children}
</ChatContext.Provider>
);
};
// app/layout.js
import { ChatProvider } from './ChatProvider';
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>
<ChatProvider
config={{
partnerKey: process.env.NEXT_PUBLIC_CHATOSHI_PARTNER_KEY,
mode: 'popup'
}}
>
{children}
</ChatProvider>
</body>
</html>
);
}
Environment variables
Keep the partner key out of your source. In Next.js, put it in .env.local with the NEXT_PUBLIC_ prefix so it reaches the browser:
NEXT_PUBLIC_CHATOSHI_PARTNER_KEY=your-partner-key
TypeScript
The SDK publishes type definitions. Download them into your project:
curl -o types/chatoshi.d.ts https://aichat-sdk.chatoshi.ai/chatoshi-sdk.d.ts
Make sure types/ is covered by your tsconfig.json include. The file declares the global Window.Chatoshi (no declare global needed) and exports types you can use directly:
import type { ChatoshiInitOptions, MessageSentPayload } from '../types/chatoshi';
export const useChatoshi = (options: ChatoshiInitOptions) => {
// ...
};
const onMessageSent = (data: MessageSentPayload) => {
console.log(data.chatId, data.personality, data.characterCount);
};
Troubleshooting
Chatoshi is not defined
The script hadn't loaded when your code ran. Initialize in the script's onload (or a sdkLoaded state), not on first render.
Multiple chat instances appear
A re-render or React 18 StrictMode double-invocation created a second instance. Guard with a ref, and always destroy in the effect cleanup.
Hydration errors
Anything touching window belongs in useEffect, which never runs on the server.