React Example
Prerequisites
Add the SDK script to the <head> of your index.html.
<script src="https://klleon.k1.klleon.io/{VERSION}/klleon-sdk.umd.js"></script>Basic Example
A basic React example using SDK Web Components.
StrictMode Behavior
The SDK serializes init()/destroy() calls through an internal Mutex. Under React StrictMode's double mount (mount → unmount → remount), the cleanup's destroy() and the second init() are automatically serialized, so remounting reconnects safely. destroy() is safe to call even before initialization completes (it becomes a no-op), so call it unconditionally in the cleanup.
import { useEffect, useState } from 'react';
const SDK_KEY = 'YOUR_SDK_KEY';
const AVATAR_ID = 'YOUR_AVATAR_ID';
function App() {
const [status, setStatus] = useState('IDLE');
const [speakText, setSpeakText] = useState('');
useEffect(() => {
const SDK = window.KlleonSDK;
SDK.onStatus((s) => setStatus(s));
SDK.onSignal((data) => {
console.log('Signal:', data.signal, data.payload);
});
SDK.onError((error) => {
console.error('Error:', error.code, error.message);
});
SDK.init({ sdk_key: SDK_KEY, avatar_id: AVATAR_ID })
.catch((e) => console.error('Initialization failed:', e.message));
return () => {
// destroy() is safe even if init hasn't completed — it is serialized behind any in-flight init
SDK.destroy();
};
}, []);
const handleSpeak = () => {
if (speakText.trim()) {
window.KlleonSDK.speak(speakText);
setSpeakText('');
}
};
const isReady = status === 'CONNECTED_FINISH';
return (
<div style={{ display: 'flex', width: 800, height: 720, gap: 24 }}>
<avatar-container
style={{ flex: 1, borderRadius: 24, overflow: 'hidden' }}
/>
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', gap: 12 }}>
<p>Status: {status}</p>
{/* Chat area — chat-container has its own input UI that sends sendMessage() */}
<chat-container style={{ flex: 1, borderRadius: 24 }} />
{/* Direct speak() call demo */}
<p style={{ margin: 0, fontWeight: 'bold' }}>Direct speak() call demo</p>
<p style={{ margin: 0, fontSize: 12, color: '#666' }}>
The avatar reads the entered text as-is (no LLM response).
For chat messages, use the chat-container input above.
</p>
<input
value={speakText}
onChange={(e) => setSpeakText(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && !e.nativeEvent.isComposing && handleSpeak()}
placeholder="Enter speak text..."
disabled={!isReady}
/>
<button onClick={handleSpeak} disabled={!isReady}>Send speak</button>
</div>
</div>
);
}
export default App;TypeScript Project
If you get type errors when using Web Components in TypeScript, add the type definition file from the TypeScript Support page.
Custom Example (Without Web Components)
An advanced example that builds the UI directly without SDK Web Components. You can freely control the UI using SDK events and methods.
import { useEffect, useState } from 'react';
const SDK_KEY = 'YOUR_SDK_KEY';
const AVATAR_ID = 'YOUR_AVATAR_ID';
function CustomChat() {
const [status, setStatus] = useState('IDLE');
const [messages, setMessages] = useState<{ type: 'request' | 'response'; text: string }[]>([]);
const [text, setText] = useState('');
const [isSpeaking, setIsSpeaking] = useState(false);
useEffect(() => {
const SDK = window.KlleonSDK;
SDK.onStatus((s) => setStatus(s));
SDK.onSignal((data) => {
switch (data.signal) {
case 'RESPONSE_TEXT':
setMessages((prev) => [...prev, {
type: 'response', text: String(data.payload?.text ?? '')
}]);
setIsSpeaking(true);
break;
case 'STT_RESULT':
setMessages((prev) => [...prev, {
type: 'request', text: String(data.payload?.text ?? '')
}]);
break;
case 'RESPONSE_PREPARING':
setIsSpeaking(true);
break;
case 'RESPONSE_ENDED':
setIsSpeaking(false);
break;
}
});
SDK.onError((error) => {
console.error(error.code, error.message);
});
SDK.init({ sdk_key: SDK_KEY, avatar_id: AVATAR_ID })
.catch((e) => console.error(e.message));
return () => {
// destroy() is safe even if init hasn't completed — it is serialized behind any in-flight init
SDK.destroy();
};
}, []);
const send = () => {
if (!text.trim() || isSpeaking) return;
window.KlleonSDK.sendMessage(text);
setMessages((prev) => [...prev, { type: 'request', text }]);
setText('');
};
const isReady = status === 'CONNECTED_FINISH';
return (
<div style={{ display: 'flex', gap: 24, height: 720 }}>
{/* Avatar area */}
<avatar-container style={{ flex: 1, borderRadius: 24, overflow: 'hidden' }} />
{/* Chat area (custom implementation) */}
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', gap: 12 }}>
<p>Status: {status}</p>
<div style={{
flex: 1, overflowY: 'auto', border: '1px solid #ccc',
borderRadius: 12, padding: 12
}}>
{messages.map((m, i) => (
<div key={i} style={{
textAlign: m.type === 'request' ? 'right' : 'left',
margin: '4px 0'
}}>
<span style={{
display: 'inline-block', padding: '8px 12px', borderRadius: 8,
background: m.type === 'request' ? '#3579CC' : '#f0f0f0',
color: m.type === 'request' ? '#fff' : '#333',
}}>
{m.text}
</span>
</div>
))}
</div>
{/* Controls */}
<div style={{ display: 'flex', gap: 8 }}>
<input
value={text}
onChange={(e) => setText(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && !e.nativeEvent.isComposing && send()}
placeholder={isSpeaking ? 'Avatar is speaking...' : 'Enter message'}
disabled={!isReady || isSpeaking}
style={{ flex: 1, padding: 8 }}
/>
<button onClick={send} disabled={!isReady || isSpeaking}>Send</button>
</div>
<div style={{ display: 'flex', gap: 8 }}>
<button onClick={() => window.KlleonSDK.startListening()} disabled={!isReady}>Start STT</button>
<button onClick={() => window.KlleonSDK.endListening()} disabled={!isReady}>End STT</button>
<button onClick={() => window.KlleonSDK.stopSpeaking()} disabled={!isSpeaking}>Stop Speech</button>
</div>
</div>
</div>
);
}
export default CustomChat;