@moveo-ai/web-client
Version:
Client side library to load the moveo chat widget and connect it with your agent
138 lines (123 loc) • 7.74 kB
HTML
<html lang="en"><head><meta charset="UTF-8"/><meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1"/><meta name="keywords" content="Moveo.ai, Voice, WebRTC, Preview"/><meta name="description" content="Test your WebRTC Voice Integration"/><meta name="twitter:card" content="summary"/><meta property="og:title" content="Moveo.ai WebRTC Preview"/><meta property="og:type" content="website"/><meta property="og:url" content="https://moveo.ai"/><meta property="og:image" content="https://web-client.moveo.ai/public/og-image.jpg"/><meta property="og:image:width" content="1200"/><meta property="og:image:height" content="630"/><meta property="og:description" content="Test your WebRTC Voice Integration"/><title>Moveo.ai WebRTC Preview</title><script src="https://cdn.tailwindcss.com"></script><script>tailwind.config = {
important: '#preview-content',
corePlugins: { preflight: false },
theme: {
extend: {
fontFamily: {
sans: ['Inter', 'system-ui', '-apple-system', 'sans-serif'],
},
colors: {
'moveo-primary': '#1B66D6',
'moveo-secondary': '#EEF2FF',
},
},
},
};</script><link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;700&display=swap" rel="stylesheet"/><script defer="defer" src="/webrtc-client.min.js"></script></head><body><div id="preview-content" class="relative min-h-screen font-sans text-slate-900 antialiased"><main class="relative z-10 flex min-h-screen items-start md:items-center"><section class="mx-auto flex w-full max-w-6xl flex-col gap-12 px-6 py-16 pb-32 md:flex-row md:items-start md:px-12 md:pb-16 lg:gap-16"><header class="max-w-2xl space-y-6 text-center md:text-left"><h1 class="text-4xl font-semibold leading-tight sm:text-5xl">Preview and test<br class="hidden sm:block"/>your WebRTC voice integration</h1><p class="text-lg text-slate-600 sm:text-xl">Click the widget button at the bottom right to start a test call and try your AI Agent with native WebRTC voice.</p><div id="status" class="text-sm font-medium"></div></header><div class="flex-1"><div id="transcript-log" class="hidden max-h-[60vh] w-full overflow-y-auto rounded-xl border border-slate-200 bg-white/80 p-4 font-mono text-sm shadow-sm backdrop-blur md:max-h-[70vh]"><h3 class="mb-3 text-xs font-semibold uppercase tracking-wider text-slate-400">Live Transcript</h3><div id="transcript-entries" class="space-y-2 pb-24"></div></div></div></section></main></div><script>localStorage.debug = 'moveo:webrtc:*';
const statusNode = document.getElementById('status');
const transcriptLog = document.getElementById('transcript-log');
const transcriptEntries = document.getElementById('transcript-entries');
const toneColor = {
warning: 'text-amber-600',
error: 'text-rose-600',
};
const setStatus = (message, tone = 'error') => {
if (!statusNode) return;
statusNode.className = `text-sm font-medium ${toneColor[tone] ?? toneColor.error}`;
statusNode.textContent = message;
};
const addTranscript = (speaker, text) => {
if (!transcriptEntries || !transcriptLog) return;
transcriptLog.classList.remove('hidden');
const entry = document.createElement('div');
const label = speaker === 'user' ? '🎤 You' : '🤖 Agent';
const color = speaker === 'user' ? 'text-sky-600' : 'text-emerald-600';
entry.innerHTML = `<span class="font-semibold ${color}">${label}:</span> ${text}`;
transcriptEntries.appendChild(entry);
transcriptLog.scrollTop = transcriptLog.scrollHeight;
};
window.addEventListener('load', async () => {
const params = new URLSearchParams(window.location.search);
const integrationId =
(params.get('integration_id') ?? params.get('integrationId') ?? '').trim();
if (!integrationId) {
setStatus('Add "?integration_id=YOUR_ID" to the URL to begin.', 'warning');
return;
}
const hostParam = (params.get('host') ?? '').trim();
const host = hostParam ? hostParam.replace(/\/+$/, '') : '';
if (!window.MoveoWebRTC?.init) {
console.error('MoveoWebRTC UMD build is not available on window.');
setStatus(
'Unable to load the WebRTC client. Check the bundle output.',
'error'
);
return;
}
try {
const controller = await window.MoveoWebRTC.init({
integrationId,
...(host ? { host } : {}),
});
controller.on?.('error', ({ message }) => {
setStatus(`Call error: ${message || 'unexpected error'}`, 'error');
});
// One entry element per request_id for the current call; reset when a
// new call starts so a prior call's lines don't carry over.
let brainEntries = {};
let lastState = 'idle';
const resetTranscript = () => {
if (transcriptEntries) transcriptEntries.innerHTML = '';
brainEntries = {};
if (transcriptLog) transcriptLog.classList.add('hidden');
};
// A fresh call is idle/error -> connecting. A mid-call reconnect goes
// through 'reconnecting' (or connected -> connecting on an ICE blip), so
// the live transcript survives it.
controller.on?.('state-change', ({ data }) => {
if (data === 'connecting' && (lastState === 'idle' || lastState === 'error')) {
resetTranscript();
}
lastState = data;
});
controller.on?.('transcript', ({ data }) => {
if (data && typeof data === 'object' && 'text' in data) {
addTranscript('user', data.text);
}
});
// brain-response streams in chunks; upsert one line per request_id so
// the assembled reply grows in place instead of appending each chunk.
controller.on?.('brain-response', ({ data }) => {
if (!data || typeof data !== 'object' || !('text' in data)) return;
if (!transcriptEntries || !transcriptLog) return;
transcriptLog.classList.remove('hidden');
const reqId = ('request_id' in data && data.request_id) || '_';
let entry = brainEntries[reqId];
if (!entry) {
entry = document.createElement('div');
transcriptEntries.appendChild(entry);
brainEntries[reqId] = entry;
}
entry.innerHTML =
`<span class="font-semibold text-emerald-600">🤖 Agent:</span> ${data.text}`;
transcriptLog.scrollTop = transcriptLog.scrollHeight;
});
// interrupt marks a barge-in: keep the heard text, gray out the unheard
// tail (never delete).
controller.on?.('interrupt', ({ data }) => {
if (!data || typeof data !== 'object' || !('request_id' in data)) return;
const reqId = data.request_id || '_';
const entry = brainEntries[reqId];
if (!entry) return;
const heard = ('heardText' in data && data.heardText) || '';
const unheard = ('unheardText' in data && data.unheardText) || '';
entry.innerHTML =
`<span class="font-semibold text-emerald-600">🤖 Agent:</span> ${heard}` +
(unheard ? ` <span class="text-slate-400 line-through">${unheard}</span>` : '');
transcriptLog.scrollTop = transcriptLog.scrollHeight;
});
console.log('✅ MoveoWebRTC widget initialized', controller);
} catch (error) {
console.error('❌ Failed to initialize MoveoWebRTC widget:', error);
setStatus('Failed to initialize the WebRTC widget', 'error');
}
});</script></body></html>