Voice Agent Widget
Embed an AI-powered voice agent on your website
Prerequisites
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.
Navigate to Call Flows in the sidebar. Each call flow card displays its Agent ID - copy it from the card.
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.
Open your call flow, go to the Settings tab, and toggle Public Access on. Without this, startPublicCall() will be rejected.
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.
Paste this before the closing </body> tag. A mic button appears in the bottom-right corner.
<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>A polished voice widget with mute/unmute, end call, audio visualizer, and live transcript display.
<!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 className="trillet-v-header">
<div>
<h3>Voice Assistant</h3>
<div className="status">
<div className="status-dot" id="trillet-v-dot"></div>
<span id="trillet-v-status-text">Ready</span>
</div>
</div>
<button className="trillet-v-close" id="trillet-v-close">✕</button>
</div>
<div className="trillet-v-transcript" id="trillet-v-transcript"></div>
<div className="trillet-v-controls">
<div className="trillet-v-vis" id="trillet-v-vis">
<div className="bar"></div><div className="bar"></div><div className="bar"></div>
<div className="bar"></div>
<div className="bar"></div><div className="bar"></div><div className="bar"></div>
</div>
<div className="trillet-v-label" id="trillet-v-label">Tap the mic to start</div>
<div className="trillet-v-btns" id="trillet-v-btns">
<button className="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 className="trillet-v-end" id="trillet-v-end">${hangupSvg}</button>
<button className="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 className="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>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) |
agent.on('assistantStartedSpeaking', () => {
console.log('Agent is talking...');
});