Prerequisites
Get your Workspace ID
In the Trillet Portal sidebar, click the workspace name dropdown at the top. Your Workspace ID is displayed next to each workspace - click to copy.
Get your Agent ID
Navigate to Call Flows in the sidebar. Each call flow card displays its Agent ID - copy it from the card.
Whitelist your domain
Go to Settings → Domain and add the domain where you’ll embed the widget (e.g.
https://yoursite.com). This is required to avoid CORS errors. Allow up to 24 hours for propagation. Skip this if you’re using a whitelabelled custom domain.The voice widget requires microphone permissions and must be served over HTTPS (or localhost). Browsers block microphone access on insecure origins.
Integration
Choose between a minimal quick-start snippet or a complete styled widget, then pick your framework.Quick Start - Minimal snippet
Quick Start - Minimal snippet
Paste this before the closing Usage:
</body> tag. A mic button appears in the bottom-right corner.- HTML / CDN
- React / Next.js
Copy
Ask AI
<script type="module">
import { TrilletAgent } from 'https://cdn.jsdelivr.net/npm/@trillet-ai/web-sdk/+esm';
const WORKSPACE_ID = 'your-workspace-id';
const AGENT_ID = 'your-agent-id';
const btn = document.createElement('button');
btn.id = 'trillet-voice-btn';
btn.innerHTML = `<svg width="22" height="22" fill="white" viewBox="0 0 24 24"><path d="M12 14c1.66 0 3-1.34 3-3V5c0-1.66-1.34-3-3-3S9 3.34 9 5v6c0 1.66 1.34 3 3 3zm-1-9c0-.55.45-1 1-1s1 .45 1 1v6c0 .55-.45 1-1 1s-1-.45-1-1V5zm6 6c0 2.76-2.24 5-5 5s-5-2.24-5-5H5c0 3.53 2.61 6.43 6 6.92V21h2v-3.08c3.39-.49 6-3.39 6-6.92h-2z"/></svg>`;
btn.style.cssText = 'position:fixed;bottom:24px;right:24px;width:52px;height:52px;border-radius:12px;background:#0066ff;border:1px solid rgba(0,102,255,0.2);cursor:pointer;display:flex;align-items:center;justify-content:center;box-shadow:0 1px 3px rgba(0,0,0,0.08);z-index:9999;transition:all 0.1s ease-in-out;';
document.body.appendChild(btn);
let agent = null;
let isActive = false;
btn.addEventListener('click', async () => {
if (isActive) { agent?.endCall(); return; }
btn.style.background = '#d1d5db';
btn.disabled = true;
try {
agent = new TrilletAgent({ workspaceId: WORKSPACE_ID, agentId: AGENT_ID, mode: 'voice' });
agent.on('connected', () => {
isActive = true;
btn.disabled = false;
btn.style.background = '#ef4444';
btn.style.borderColor = 'rgba(239,68,68,0.2)';
btn.innerHTML = `<svg width="20" height="20" fill="white" viewBox="0 0 24 24"><path d="M12 9c-1.6 0-3.15.25-4.6.72v3.1c0 .39-.23.74-.56.9-.98.49-1.87 1.12-2.66 1.85-.18.18-.43.28-.7.28-.28 0-.53-.11-.71-.29L.29 13.08a.956.956 0 010-1.36C3.55 8.67 7.56 7 12 7s8.45 1.67 11.71 4.72c.18.18.29.43.29.71 0 .28-.11.53-.29.71l-2.48 2.48c-.18.18-.43.29-.71.29-.27 0-.52-.1-.7-.28-.79-.73-1.68-1.36-2.66-1.85a.993.993 0 01-.56-.9v-3.1C15.15 9.25 13.6 9 12 9z"/></svg>`;
});
agent.on('disconnected', () => {
isActive = false;
btn.style.background = '#0066ff';
btn.style.borderColor = 'rgba(0,102,255,0.2)';
btn.innerHTML = `<svg width="22" height="22" fill="white" viewBox="0 0 24 24"><path d="M12 14c1.66 0 3-1.34 3-3V5c0-1.66-1.34-3-3-3S9 3.34 9 5v6c0 1.66 1.34 3 3 3zm-1-9c0-.55.45-1 1-1s1 .45 1 1v6c0 .55-.45 1-1 1s-1-.45-1-1V5zm6 6c0 2.76-2.24 5-5 5s-5-2.24-5-5H5c0 3.53 2.61 6.43 6 6.92V21h2v-3.08c3.39-.49 6-3.39 6-6.92h-2z"/></svg>`;
btn.disabled = false;
});
agent.on('error', () => {
btn.style.background = '#0066ff';
btn.style.borderColor = 'rgba(0,102,255,0.2)';
btn.innerHTML = `<svg width="22" height="22" fill="white" viewBox="0 0 24 24"><path d="M12 14c1.66 0 3-1.34 3-3V5c0-1.66-1.34-3-3-3S9 3.34 9 5v6c0 1.66 1.34 3 3 3zm-1-9c0-.55.45-1 1-1s1 .45 1 1v6c0 .55-.45 1-1 1s-1-.45-1-1V5zm6 6c0 2.76-2.24 5-5 5s-5-2.24-5-5H5c0 3.53 2.61 6.43 6 6.92V21h2v-3.08c3.39-.49 6-3.39 6-6.92h-2z"/></svg>`;
btn.disabled = false;
isActive = false;
});
await agent.startPublicCall();
} catch (err) {
btn.style.background = '#0066ff';
btn.style.borderColor = 'rgba(0,102,255,0.2)';
btn.innerHTML = `<svg width="22" height="22" fill="white" viewBox="0 0 24 24"><path d="M12 14c1.66 0 3-1.34 3-3V5c0-1.66-1.34-3-3-3S9 3.34 9 5v6c0 1.66 1.34 3 3 3zm-1-9c0-.55.45-1 1-1s1 .45 1 1v6c0 .55-.45 1-1 1s-1-.45-1-1V5zm6 6c0 2.76-2.24 5-5 5s-5-2.24-5-5H5c0 3.53 2.61 6.43 6 6.92V21h2v-3.08c3.39-.49 6-3.39 6-6.92h-2z"/></svg>`;
btn.disabled = false;
}
});
</script>
Copy
Ask AI
npm install @trillet-ai/web-sdk
Copy
Ask AI
import { useRef, useState, useEffect } from 'react';
import { TrilletAgent } from '@trillet-ai/web-sdk';
export default function TrilletVoice({ workspaceId, agentId }) {
const agentRef = useRef(null);
const [isActive, setIsActive] = useState(false);
const [status, setStatus] = useState('idle');
const startCall = async () => {
const agent = new TrilletAgent({ workspaceId, agentId, mode: 'voice' });
agentRef.current = agent;
setStatus('connecting');
agent.on('connected', () => { setIsActive(true); setStatus('connected'); });
agent.on('disconnected', () => { setIsActive(false); setStatus('idle'); });
agent.on('error', () => setStatus('error'));
try {
await agent.startPublicCall();
} catch {
setStatus('error');
}
};
const endCall = () => agentRef.current?.endCall();
useEffect(() => () => agentRef.current?.endCall?.(), []);
return (
<button onClick={isActive ? endCall : startCall} disabled={status === 'connecting'}
style={{ position: 'fixed', bottom: 24, right: 24, width: 52, height: 52,
borderRadius: 12, background: isActive ? '#ef4444' : '#0066ff',
border: `1px solid ${isActive ? 'rgba(239,68,68,0.2)' : 'rgba(0,102,255,0.2)'}`,
cursor: 'pointer', color: '#fff', display: 'flex', alignItems: 'center',
justifyContent: 'center', zIndex: 9999, boxShadow: '0 1px 3px rgba(0,0,0,0.08)' }}>
{status === 'connecting' ? '...' : isActive ? '📞' : '🎙️'}
</button>
);
}
Copy
Ask AI
<TrilletVoice workspaceId="your-workspace-id" agentId="your-agent-id" />
Full Widget - Voice panel with controls and transcript
Full Widget - Voice panel with controls and transcript
A polished voice widget with mute/unmute, end call, audio visualizer, and live transcript display.Usage:
- HTML / CDN
- React / Next.js
Copy
Ask AI
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Trillet Voice Widget</title>
<style>
* { box-sizing: border-box; }
#trillet-v-bubble {
position: fixed;
bottom: 24px;
right: 24px;
width: 52px;
height: 52px;
background: #0066ff;
border-radius: 12px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
border: 1px solid rgba(0, 102, 255, 0.2);
z-index: 9999;
transition: all 0.1s ease-in-out;
}
#trillet-v-bubble:hover { background: #0052cc; }
#trillet-v-bubble svg { width: 22px; height: 22px; fill: white; }
#trillet-v-panel {
position: fixed;
bottom: 88px;
right: 24px;
width: 340px;
background: #fff;
border-radius: 12px;
border: 1px solid #e5e7eb;
box-shadow: 0 4px 12px -1px rgba(0, 0, 0, 0.05);
display: none;
flex-direction: column;
overflow: hidden;
z-index: 9999;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
}
#trillet-v-panel.open { display: flex; animation: trillet-v-in 0.15s cubic-bezier(0.16,1,0.3,1); }
@keyframes trillet-v-in {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}
.trillet-v-header {
padding: 14px 16px;
border-bottom: 1px solid #e5e7eb;
display: flex;
justify-content: space-between;
align-items: center;
}
.trillet-v-header h3 { font-size: 14px; font-weight: 600; margin: 0; color: #111827; }
.trillet-v-header .status {
font-size: 12px; color: #6b7280;
display: flex; align-items: center; gap: 5px; margin-top: 1px;
}
.trillet-v-header .status-dot {
width: 6px; height: 6px; border-radius: 50%; background: #d1d5db;
}
.trillet-v-header .status-dot.online { background: #10b981; }
.trillet-v-close {
background: none; border: none; color: #9ca3af;
cursor: pointer; font-size: 16px; padding: 4px; line-height: 1;
transition: color 0.1s;
}
.trillet-v-close:hover { color: #6b7280; }
.trillet-v-transcript {
max-height: 180px;
overflow-y: auto;
padding: 12px 16px;
display: flex;
flex-direction: column;
gap: 6px;
background: #f9fafb;
}
.trillet-v-msg {
max-width: 85%;
padding: 7px 11px;
border-radius: 8px;
font-size: 12px;
line-height: 1.4;
}
.trillet-v-msg.user {
background: #0066ff; color: #fff; align-self: flex-end;
}
.trillet-v-msg.assistant {
background: #fff; color: #111827; align-self: flex-start;
border: 1px solid #e5e7eb;
}
.trillet-v-msg.system {
background: #f3f4f6; color: #6b7280;
align-self: center; font-size: 11px; padding: 3px 8px; border-radius: 4px;
}
.trillet-v-controls {
padding: 20px;
background: #fff;
border-top: 1px solid #e5e7eb;
display: flex;
flex-direction: column;
align-items: center;
gap: 14px;
}
.trillet-v-vis {
display: flex; align-items: center; justify-content: center;
gap: 3px; height: 32px;
}
.trillet-v-vis .bar {
width: 3px; height: 6px;
background: #0066ff; border-radius: 2px;
transition: height 0.1s;
}
.trillet-v-vis.active .bar {
animation: trillet-pulse 0.8s infinite ease-in-out;
}
.trillet-v-vis.active .bar:nth-child(1) { animation-delay: 0s; }
.trillet-v-vis.active .bar:nth-child(2) { animation-delay: 0.1s; }
.trillet-v-vis.active .bar:nth-child(3) { animation-delay: 0.2s; }
.trillet-v-vis.active .bar:nth-child(4) { animation-delay: 0.3s; }
.trillet-v-vis.active .bar:nth-child(5) { animation-delay: 0.4s; }
.trillet-v-vis.active .bar:nth-child(6) { animation-delay: 0.3s; }
.trillet-v-vis.active .bar:nth-child(7) { animation-delay: 0.2s; }
@keyframes trillet-pulse {
0%, 100% { height: 6px; }
50% { height: 22px; }
}
.trillet-v-label {
font-size: 12px; color: #6b7280; font-weight: 500;
}
.trillet-v-btns {
display: flex; align-items: center; gap: 12px;
}
.trillet-v-mic {
width: 48px; height: 48px; border-radius: 12px; border: none;
cursor: pointer; display: flex; align-items: center; justify-content: center;
transition: all 0.1s;
}
.trillet-v-mic.idle {
background: #0066ff; color: white;
border: 1px solid rgba(0, 102, 255, 0.2);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
}
.trillet-v-mic.idle:hover { background: #0052cc; }
.trillet-v-mic.active {
background: #0066ff; color: white;
border: 1px solid rgba(0, 102, 255, 0.2);
}
.trillet-v-mic.muted {
background: #fef2f2; color: #ef4444;
border: 1px solid rgba(239, 68, 68, 0.15);
}
.trillet-v-mic svg { width: 20px; height: 20px; }
.trillet-v-end {
width: 40px; height: 40px; border-radius: 12px; border: none;
background: #ef4444; color: white; cursor: pointer;
border: 1px solid rgba(239, 68, 68, 0.2);
display: flex; align-items: center; justify-content: center;
transition: background 0.1s;
}
.trillet-v-end:hover { background: #dc2626; }
.trillet-v-end svg { width: 18px; height: 18px; }
@media (max-width: 480px) {
#trillet-v-panel {
width: calc(100vw - 24px);
right: 12px;
bottom: 88px;
}
}
</style>
</head>
<body>
<!-- Paste everything below into your website -->
<button id="trillet-v-bubble">
<svg viewBox="0 0 24 24"><path d="M12 14c1.66 0 3-1.34 3-3V5c0-1.66-1.34-3-3-3S9 3.34 9 5v6c0 1.66 1.34 3 3 3zm-1-9c0-.55.45-1 1-1s1 .45 1 1v6c0 .55-.45 1-1 1s-1-.45-1-1V5zm6 6c0 2.76-2.24 5-5 5s-5-2.24-5-5H5c0 3.53 2.61 6.43 6 6.92V21h2v-3.08c3.39-.49 6-3.39 6-6.92h-2z"/></svg>
</button>
<div id="trillet-v-panel">
<div class="trillet-v-header">
<div>
<h3>Voice Assistant</h3>
<div class="status">
<div class="status-dot" id="trillet-v-dot"></div>
<span id="trillet-v-status-text">Ready</span>
</div>
</div>
<button class="trillet-v-close" id="trillet-v-close">✕</button>
</div>
<div class="trillet-v-transcript" id="trillet-v-transcript"></div>
<div class="trillet-v-controls">
<div class="trillet-v-vis" id="trillet-v-vis">
<div class="bar"></div><div class="bar"></div><div class="bar"></div>
<div class="bar"></div>
<div class="bar"></div><div class="bar"></div><div class="bar"></div>
</div>
<div class="trillet-v-label" id="trillet-v-label">Tap the mic to start</div>
<div class="trillet-v-btns" id="trillet-v-btns">
<button class="trillet-v-mic idle" id="trillet-v-mic">
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 14c1.66 0 3-1.34 3-3V5c0-1.66-1.34-3-3-3S9 3.34 9 5v6c0 1.66 1.34 3 3 3zm-1-9c0-.55.45-1 1-1s1 .45 1 1v6c0 .55-.45 1-1 1s-1-.45-1-1V5zm6 6c0 2.76-2.24 5-5 5s-5-2.24-5-5H5c0 3.53 2.61 6.43 6 6.92V21h2v-3.08c3.39-.49 6-3.39 6-6.92h-2z"/></svg>
</button>
</div>
</div>
</div>
<script type="module">
import { TrilletAgent } from 'https://cdn.jsdelivr.net/npm/@trillet-ai/web-sdk/+esm';
// ── REPLACE THESE WITH YOUR VALUES ──
const WORKSPACE_ID = 'your-workspace-id';
const AGENT_ID = 'your-agent-id';
// ─────────────────────────────────────
const $ = (id) => document.getElementById(id);
const bubble = $('trillet-v-bubble');
const panel = $('trillet-v-panel');
const micSvg = `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 14c1.66 0 3-1.34 3-3V5c0-1.66-1.34-3-3-3S9 3.34 9 5v6c0 1.66 1.34 3 3 3zm-1-9c0-.55.45-1 1-1s1 .45 1 1v6c0 .55-.45 1-1 1s-1-.45-1-1V5zm6 6c0 2.76-2.24 5-5 5s-5-2.24-5-5H5c0 3.53 2.61 6.43 6 6.92V21h2v-3.08c3.39-.49 6-3.39 6-6.92h-2z"/></svg>`;
const micMutedSvg = `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M19 11h-1.7c0 .74-.16 1.43-.43 2.05l1.23 1.23c.56-.98.9-2.09.9-3.28zm-4.02.17c0-.06.02-.11.02-.17V5c0-1.66-1.34-3-3-3S9 3.34 9 5v.18l5.98 5.99zM4.27 3L3 4.27l6.01 6.01V11c0 1.66 1.33 3 2.99 3 .22 0 .44-.03.65-.08l1.66 1.66c-.71.33-1.5.52-2.31.52-2.76 0-5.3-2.1-5.3-5.1H5c0 3.41 2.72 6.23 6 6.72V21h2v-3.28c.91-.13 1.77-.45 2.54-.9L19.73 21 21 19.73 4.27 3z"/></svg>`;
const hangupSvg = `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 9c-1.6 0-3.15.25-4.6.72v3.1c0 .39-.23.74-.56.9-.98.49-1.87 1.12-2.66 1.85-.18.18-.43.28-.7.28-.28 0-.53-.11-.71-.29L.29 13.08a.956.956 0 010-1.36C3.55 8.67 7.56 7 12 7s8.45 1.67 11.71 4.72c.18.18.29.43.29.71 0 .28-.11.53-.29.71l-2.48 2.48c-.18.18-.43.29-.71.29-.27 0-.52-.1-.7-.28-.79-.73-1.68-1.36-2.66-1.85a.993.993 0 01-.56-.9v-3.1C15.15 9.25 13.6 9 12 9z"/></svg>`;
let agent = null;
let isActive = false;
let isMuted = false;
let pollInterval = null;
const displayed = new Set();
bubble.addEventListener('click', () => panel.classList.toggle('open'));
$('trillet-v-close').addEventListener('click', () => panel.classList.remove('open'));
$('trillet-v-mic').addEventListener('click', () => {
if (!isActive) connectAgent();
else toggleMute();
});
async function connectAgent() {
setStatus('Connecting...', false);
$('trillet-v-label').textContent = 'Connecting...';
$('trillet-v-mic').disabled = true;
try {
agent = new TrilletAgent({
workspaceId: WORKSPACE_ID,
agentId: AGENT_ID,
mode: 'voice',
});
agent.on('connected', () => {
isActive = true;
isMuted = false;
setStatus('Connected', true);
$('trillet-v-label').textContent = 'Listening...';
$('trillet-v-vis').classList.add('active');
$('trillet-v-mic').disabled = false;
showActiveControls();
addMsg('Voice call connected', 'system');
startPolling();
});
agent.on('disconnected', () => {
isActive = false;
setStatus('Disconnected', false);
$('trillet-v-label').textContent = 'Call ended';
$('trillet-v-vis').classList.remove('active');
showIdleControls();
stopPolling();
addMsg('Call ended', 'system');
});
agent.on('error', () => {
addMsg('An error occurred', 'system');
setStatus('Error', false);
});
agent.on('assistantStartedSpeaking', () => {
$('trillet-v-label').textContent = 'Agent speaking...';
});
agent.on('assistantStoppedSpeaking', () => {
$('trillet-v-label').textContent = isMuted ? 'Muted' : 'Listening...';
});
['message', 'transcript', 'transcriptUpdate'].forEach(evt => {
agent.on(evt, (data) => {
if (data?.isFinal === false) return;
const text = typeof data === 'string' ? data : data?.text || data?.content;
const role = data?.role || 'assistant';
if (text) addMsg(text, role);
});
});
await agent.startPublicCall();
} catch (err) {
setStatus('Failed', false);
$('trillet-v-label').textContent = 'Failed to connect';
$('trillet-v-mic').disabled = false;
showIdleControls();
}
}
function toggleMute() {
isMuted = !isMuted;
try { agent?.toggleMicrophone?.(!isMuted); } catch (e) {}
const mic = $('trillet-v-mic');
if (isMuted) {
mic.className = 'trillet-v-mic muted';
mic.innerHTML = micMutedSvg;
$('trillet-v-label').textContent = 'Muted';
} else {
mic.className = 'trillet-v-mic active';
mic.innerHTML = micSvg;
$('trillet-v-label').textContent = 'Listening...';
}
}
function endCall() { try { agent?.endCall(); } catch (e) {} }
function showActiveControls() {
$('trillet-v-btns').innerHTML = `
<button class="trillet-v-end" id="trillet-v-end">${hangupSvg}</button>
<button class="trillet-v-mic active" id="trillet-v-mic">${micSvg}</button>
`;
$('trillet-v-end').addEventListener('click', endCall);
$('trillet-v-mic').addEventListener('click', toggleMute);
}
function showIdleControls() {
$('trillet-v-btns').innerHTML = `
<button class="trillet-v-mic idle" id="trillet-v-mic">${micSvg}</button>
`;
$('trillet-v-mic').addEventListener('click', () => {
if (!isActive) connectAgent();
else toggleMute();
});
}
function startPolling() {
let lastCount = 0;
pollInterval = setInterval(() => {
if (!agent || !isActive) return;
try {
const transcripts = agent.getTranscripts?.() || [];
for (let i = lastCount; i < transcripts.length; i++) {
const t = transcripts[i];
if (t.text) addMsg(t.text, t.role || 'assistant');
}
lastCount = transcripts.length;
} catch (e) {}
}, 500);
}
function stopPolling() {
if (pollInterval) { clearInterval(pollInterval); pollInterval = null; }
}
function setStatus(text, online) {
$('trillet-v-status-text').textContent = text;
$('trillet-v-dot').classList.toggle('online', online);
}
function addMsg(text, type) {
if (!text?.trim()) return;
const key = type + ':' + text;
if (displayed.has(key)) return;
displayed.add(key);
const el = document.createElement('div');
el.className = 'trillet-v-msg ' + type;
el.textContent = text;
const container = $('trillet-v-transcript');
container.appendChild(el);
container.scrollTop = container.scrollHeight;
}
</script>
</body>
</html>
Copy
Ask AI
npm install @trillet-ai/web-sdk
Copy
Ask AI
import { useRef, useState, useEffect, useCallback } from 'react';
import { TrilletAgent } from '@trillet-ai/web-sdk';
export default function TrilletVoice({ workspaceId, agentId }) {
const agentRef = useRef(null);
const [isOpen, setIsOpen] = useState(false);
const [isActive, setIsActive] = useState(false);
const [isMuted, setIsMuted] = useState(false);
const [status, setStatus] = useState('idle');
const [label, setLabel] = useState('Tap the mic to start');
const [transcripts, setTranscripts] = useState([]);
const startCall = useCallback(async () => {
const agent = new TrilletAgent({ workspaceId, agentId, mode: 'voice' });
agentRef.current = agent;
setStatus('connecting');
setLabel('Connecting...');
agent.on('connected', () => {
setIsActive(true); setStatus('connected'); setLabel('Listening...');
let lastCount = 0;
const poll = setInterval(() => {
const t = agent.getTranscripts?.() || [];
for (let i = lastCount; i < t.length; i++) {
if (t[i].text) setTranscripts(prev => [...prev, { type: t[i].role || 'assistant', text: t[i].text }]);
}
lastCount = t.length;
}, 500);
agent.__poll = poll;
});
agent.on('disconnected', () => {
setIsActive(false); setStatus('idle'); setLabel('Call ended'); setIsMuted(false);
clearInterval(agent.__poll);
});
agent.on('assistantStartedSpeaking', () => setLabel('Agent speaking...'));
agent.on('assistantStoppedSpeaking', () => setLabel(prev => prev === 'Muted' ? 'Muted' : 'Listening...'));
agent.on('error', () => setStatus('error'));
['message', 'transcript'].forEach(evt => {
agent.on(evt, (data) => {
const text = typeof data === 'string' ? data : data?.text || data?.content;
const role = data?.role || 'assistant';
if (text) setTranscripts(prev => [...prev, { type: role, text }]);
});
});
try { await agent.startPublicCall(); }
catch { setStatus('error'); setLabel('Failed to connect'); }
}, [workspaceId, agentId]);
const endCall = () => agentRef.current?.endCall();
const toggleMute = () => {
const next = !isMuted;
setIsMuted(next);
setLabel(next ? 'Muted' : 'Listening...');
agentRef.current?.toggleMicrophone?.(!next);
};
useEffect(() => () => agentRef.current?.endCall?.(), []);
const s = {
bubble: { position: 'fixed', bottom: 24, right: 24, width: 52, height: 52,
borderRadius: 12, background: '#0066ff', border: '1px solid rgba(0,102,255,0.2)',
cursor: 'pointer', color: '#fff', display: 'flex', alignItems: 'center',
justifyContent: 'center', zIndex: 9999, boxShadow: '0 1px 3px rgba(0,0,0,0.08)' },
panel: { position: 'fixed', bottom: 88, right: 24, width: 340,
background: '#fff', borderRadius: 12, border: '1px solid #e5e7eb',
boxShadow: '0 4px 12px -1px rgba(0,0,0,0.05)',
display: 'flex', flexDirection: 'column', zIndex: 9999,
fontFamily: '-apple-system, BlinkMacSystemFont, system-ui, sans-serif' },
};
return (
<>
<button onClick={() => setIsOpen(!isOpen)} style={s.bubble}>
<svg width="22" height="22" fill="#fff" viewBox="0 0 24 24"><path d="M12 14c1.66 0 3-1.34 3-3V5c0-1.66-1.34-3-3-3S9 3.34 9 5v6c0 1.66 1.34 3 3 3zm-1-9c0-.55.45-1 1-1s1 .45 1 1v6c0 .55-.45 1-1 1s-1-.45-1-1V5zm6 6c0 2.76-2.24 5-5 5s-5-2.24-5-5H5c0 3.53 2.61 6.43 6 6.92V21h2v-3.08c3.39-.49 6-3.39 6-6.92h-2z"/></svg>
</button>
{isOpen && (
<div style={s.panel}>
<div style={{ padding: '14px 16px', borderBottom: '1px solid #e5e7eb',
display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div>
<div style={{ fontWeight: 600, fontSize: 14, color: '#111827' }}>Voice Assistant</div>
<div style={{ fontSize: 12, color: status === 'connected' ? '#10b981' : '#6b7280' }}>
{status === 'connected' ? 'Connected' : status === 'connecting' ? 'Connecting...' : 'Ready'}
</div>
</div>
<button onClick={() => setIsOpen(false)}
style={{ background: 'none', border: 'none', color: '#9ca3af', cursor: 'pointer', fontSize: 16 }}>
✕
</button>
</div>
{transcripts.length > 0 && (
<div style={{ maxHeight: 180, overflowY: 'auto', padding: '12px 16px',
display: 'flex', flexDirection: 'column', gap: 6, background: '#f9fafb' }}>
{transcripts.map((t, i) => (
<div key={i} style={{ alignSelf: t.type === 'user' ? 'flex-end' : 'flex-start',
background: t.type === 'user' ? '#0066ff' : '#fff',
color: t.type === 'user' ? '#fff' : '#111827',
border: t.type === 'user' ? 'none' : '1px solid #e5e7eb',
padding: '7px 11px', borderRadius: 8, maxWidth: '85%', fontSize: 12 }}>
{t.text}
</div>
))}
</div>
)}
<div style={{ padding: 20, borderTop: '1px solid #e5e7eb', display: 'flex',
flexDirection: 'column', alignItems: 'center', gap: 14 }}>
<div style={{ fontSize: 12, color: '#6b7280', fontWeight: 500 }}>{label}</div>
<div style={{ display: 'flex', gap: 12, alignItems: 'center' }}>
{isActive && (
<button onClick={endCall}
style={{ width: 40, height: 40, borderRadius: 12, background: '#ef4444',
border: '1px solid rgba(239,68,68,0.2)', color: '#fff', cursor: 'pointer',
display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
☎
</button>
)}
<button onClick={isActive ? toggleMute : startCall}
disabled={status === 'connecting'}
style={{ width: 48, height: 48, borderRadius: 12, cursor: 'pointer',
display: 'flex', alignItems: 'center', justifyContent: 'center',
background: isMuted ? '#fef2f2' : '#0066ff',
color: isMuted ? '#ef4444' : '#fff',
border: `1px solid ${isMuted ? 'rgba(239,68,68,0.15)' : 'rgba(0,102,255,0.2)'}`,
boxShadow: '0 1px 3px rgba(0,0,0,0.08)' }}>
{isMuted ? '🔇' : '🎙️'}
</button>
</div>
</div>
</div>
)}
</>
);
}
Copy
Ask AI
<TrilletVoice workspaceId="your-workspace-id" agentId="your-agent-id" />
Events Reference
| Event | Description |
|---|---|
connected | Voice call connected, agent is live |
disconnected | Call ended |
error | An error occurred |
assistantStartedSpeaking | Agent is speaking |
assistantStoppedSpeaking | Agent stopped speaking |
transcript | Transcript of speech (user or assistant) |
Copy
Ask AI
agent.on('assistantStartedSpeaking', () => {
console.log('Agent is talking...');
});
