Инструкция для фронтенд-разработчиков: Интеграция Voice Widget
Дата: 17 ноября 2025
Для: Frontend разработчиков CRM
Backend API: https://backend-dev.dmva.acebox.eu (DEV) / https://backend-alpha.dmva.acebox.eu (ALPHA)
⚠️ ВАЖНО: НЕ ИСПОЛЬЗУЙТЕ /embed ENDPOINT!
❌ НЕПРАВИЛЬНО (старый подход с iframe):
// НЕ ДЕЛАЙТЕ ТАК!
fetch(`/v1/voice-widget/${channelId}/embed`) // ← УСТАРЕВШИЙ ENDPOINT✅ ПРАВИЛЬНО (новый подход без iframe):
// ИСПОЛЬЗУЙТЕ ЭТО!
fetch(`/v1/voice-widget/${channelId}/direct`) // ← ПРАВИЛЬНЫЙ ENDPOINTПочему /direct, а не /embed:
/embed- возвращает HTML для iframe (проблемы с resize, UX)/direct- возвращает код для прямой интеграции (идеальный UX)
🎯 Что нужно передать на фронтенд
1. Channel ID
После создания webvoice канала через API, сохраните и передайте на фронтенд:
const channelId = "30f45496-35a9-402c-9ca6-525fd8455d8c"; // Получен из POST /v1/channels2. JWT Token (для управления настройками)
Если фронтенд будет управлять настройками виджета:
const token = "eyJhbGciOiJSUzI1NiJ9..."; // Из POST /v1/auth/login🚀 Быстрая интеграция (3 шага)
Шаг 1: Получить код виджета
const channelId = "ВАШ_CHANNEL_ID";
// ✅ ПРАВИЛЬНЫЙ ENDPOINT: /direct (БЕЗ iframe)
fetch(`https://backend-dev.dmva.acebox.eu/v1/voice-widget/${channelId}/direct`)
.then(res => res.json())
.then(data => {
console.log(data.data.full_code); // Готовый HTML код
});
// ❌ НЕ ИСПОЛЬЗУЙТЕ: /embed (iframe - устаревший)
// fetch(`/v1/voice-widget/${channelId}/embed`) // ← НЕПРАВИЛЬНО!Шаг 2: Вставить код на страницу
// В useEffect или componentDidMount
const widget = document.createElement('div');
widget.innerHTML = data.data.html_snippet;
document.body.appendChild(widget);
const script = document.createElement('script');
script.src = 'https://unpkg.com/@elevenlabs/convai-widget-embed';
script.async = true;
document.body.appendChild(script);Шаг 3: Готово!
Виджет автоматически появится на странице.
📋 Что получает фронтенд из API
GET /v1/voice-widget/{channelId}/direct
Request:
curl "https://backend-dev.dmva.acebox.eu/v1/voice-widget/30f45496-35a9-402c-9ca6-525fd8455d8c/direct"Response:
{
"status": "success",
"data": {
"agent_id": "agent_2701ka2qwdvsfsx8k1a7zk6rwhk4",
"channel_id": "30f45496-35a9-402c-9ca6-525fd8455d8c",
"channel_name": "Website Voice Widget",
"html_snippet": "<elevenlabs-convai agent-id=\"agent_2701ka2qwdvsfsx8k1a7zk6rwhk4\"></elevenlabs-convai>",
"script_tag": "<script src=\"https://unpkg.com/@elevenlabs/convai-widget-embed\" async></script>",
"full_code": "<!-- Полный HTML код -->",
"configuration": {
"theme": "light",
"position": "bottom-right",
"size": "medium",
"language": "en",
"color": "#007bff",
"hide_branding": true
},
"usage_examples": {
"html": "...",
"react": "...",
"vue": "..."
}
}
}Что использовать:
full_code- готовый HTML для вставкиhtml_snippet- только тег виджетаscript_tag- только скрипт ElevenLabsusage_examples.react- пример для Reactusage_examples.vue- пример для Vue
⚛️ React Integration
Компонент VoiceWidget
import React, { useEffect, useState } from 'react';
const VoiceWidget = ({ channelId }) => {
const [loaded, setLoaded] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
if (!channelId) return;
// Проверяем, не загружен ли уже виджет
if (document.querySelector('elevenlabs-convai')) {
setLoaded(true);
return;
}
// Получаем код виджета
fetch(`https://backend-dev.dmva.acebox.eu/v1/voice-widget/${channelId}/direct`)
.then(res => res.json())
.then(data => {
if (data.status === 'error') {
throw new Error(data.message);
}
// Вставляем виджет
const widget = document.createElement('div');
widget.innerHTML = data.data.html_snippet;
document.body.appendChild(widget);
// Загружаем скрипт ElevenLabs
const script = document.createElement('script');
script.src = 'https://unpkg.com/@elevenlabs/convai-widget-embed';
script.async = true;
script.onload = () => setLoaded(true);
script.onerror = () => setError('Failed to load widget script');
document.body.appendChild(script);
})
.catch(err => {
console.error('VoiceWidget error:', err);
setError(err.message);
});
// Cleanup
return () => {
const widget = document.querySelector('elevenlabs-convai');
if (widget) widget.remove();
const script = document.querySelector('script[src*="convai-widget-embed"]');
if (script) script.remove();
};
}, [channelId]);
if (error) {
console.error('VoiceWidget error:', error);
return null;
}
return null; // Виджет рендерится напрямую в DOM
};
export default VoiceWidget;Использование в приложении
import VoiceWidget from './components/VoiceWidget';
function App() {
const channelId = "30f45496-35a9-402c-9ca6-525fd8455d8c";
return (
<div className="App">
<h1>Мой сайт</h1>
{/* Voice Widget */}
<VoiceWidget channelId={channelId} />
</div>
);
}С кастомизацией
const VoiceWidget = ({ channelId, theme = 'light', language = 'en' }) => {
useEffect(() => {
const params = new URLSearchParams({ theme, language });
const url = `https://backend-dev.dmva.acebox.eu/v1/voice-widget/${channelId}/direct?${params}`;
fetch(url)
.then(res => res.json())
.then(data => {
// ... вставка виджета
});
}, [channelId, theme, language]);
return null;
};
// Использование
<VoiceWidget
channelId="30f45496-35a9-402c-9ca6-525fd8455d8c"
theme="dark"
language="ru"
/>🎨 Vue.js Integration
Компонент VoiceWidget.vue
<template>
<div></div>
</template>
<script>
export default {
name: 'VoiceWidget',
props: {
channelId: {
type: String,
required: true
},
theme: {
type: String,
default: 'light'
},
language: {
type: String,
default: 'en'
}
},
data() {
return {
loaded: false,
error: null
};
},
async mounted() {
try {
// Проверяем, не загружен ли уже виджет
if (document.querySelector('elevenlabs-convai')) {
this.loaded = true;
return;
}
// Формируем URL с параметрами
const params = new URLSearchParams({
theme: this.theme,
language: this.language
});
const url = `https://backend-dev.dmva.acebox.eu/v1/voice-widget/${this.channelId}/direct?${params}`;
// Получаем код виджета
const response = await fetch(url);
const data = await response.json();
if (data.status === 'error') {
throw new Error(data.message);
}
// Вставляем виджет
const widget = document.createElement('div');
widget.innerHTML = data.data.html_snippet;
document.body.appendChild(widget);
// Загружаем скрипт ElevenLabs
const script = document.createElement('script');
script.src = 'https://unpkg.com/@elevenlabs/convai-widget-embed';
script.async = true;
script.onload = () => {
this.loaded = true;
this.$emit('loaded');
};
script.onerror = () => {
this.error = 'Failed to load widget script';
this.$emit('error', this.error);
};
document.body.appendChild(script);
} catch (err) {
console.error('VoiceWidget error:', err);
this.error = err.message;
this.$emit('error', err);
}
},
beforeUnmount() {
// Cleanup
const widget = document.querySelector('elevenlabs-convai');
if (widget) widget.remove();
const script = document.querySelector('script[src*="convai-widget-embed"]');
if (script) script.remove();
}
};
</script>Использование в приложении
<template>
<div id="app">
<h1>Мой сайт</h1>
<!-- Voice Widget -->
<VoiceWidget
channel-id="30f45496-35a9-402c-9ca6-525fd8455d8c"
theme="dark"
language="ru"
@loaded="onWidgetLoaded"
@error="onWidgetError"
/>
</div>
</template>
<script>
import VoiceWidget from './components/VoiceWidget.vue';
export default {
components: {
VoiceWidget
},
methods: {
onWidgetLoaded() {
console.log('Voice widget loaded successfully');
},
onWidgetError(error) {
console.error('Voice widget error:', error);
}
}
};
</script>🌐 Vanilla JavaScript
Простой вариант
<!DOCTYPE html>
<html>
<head>
<title>Мой сайт</title>
</head>
<body>
<h1>Добро пожаловать!</h1>
<script>
const channelId = "30f45496-35a9-402c-9ca6-525fd8455d8c";
async function loadVoiceWidget() {
try {
const response = await fetch(
`https://backend-dev.dmva.acebox.eu/v1/voice-widget/${channelId}/direct`
);
const data = await response.json();
if (data.status === 'error') {
throw new Error(data.message);
}
// Вставляем виджет
document.body.insertAdjacentHTML('beforeend', data.data.html_snippet);
// Загружаем скрипт
const script = document.createElement('script');
script.src = 'https://unpkg.com/@elevenlabs/convai-widget-embed';
script.async = true;
document.body.appendChild(script);
console.log('✅ Voice widget loaded');
} catch (error) {
console.error('❌ Failed to load voice widget:', error);
}
}
// Загружаем после загрузки страницы
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', loadVoiceWidget);
} else {
loadVoiceWidget();
}
</script>
</body>
</html>С кастомизацией
async function loadVoiceWidget(channelId, options = {}) {
const params = new URLSearchParams({
theme: options.theme || 'light',
language: options.language || 'en',
size: options.size || 'medium',
position: options.position || 'bottom-right'
});
const url = `https://backend-dev.dmva.acebox.eu/v1/voice-widget/${channelId}/direct?${params}`;
try {
const response = await fetch(url);
const data = await response.json();
if (data.status === 'error') {
throw new Error(data.message);
}
document.body.insertAdjacentHTML('beforeend', data.data.html_snippet);
const script = document.createElement('script');
script.src = 'https://unpkg.com/@elevenlabs/convai-widget-embed';
script.async = true;
document.body.appendChild(script);
return data.data;
} catch (error) {
console.error('Failed to load voice widget:', error);
throw error;
}
}
// Использование
loadVoiceWidget('30f45496-35a9-402c-9ca6-525fd8455d8c', {
theme: 'dark',
language: 'ru',
size: 'large',
position: 'bottom-left'
});🎛️ Управление настройками виджета (опционально)
Если фронтенд должен обновлять настройки виджета:
GET текущих настроек
const getWidgetSettings = async (channelId, token) => {
const response = await fetch(
`https://backend-dev.dmva.acebox.eu/v1/channels/${channelId}/voice-widget-settings`,
{
headers: {
'Authorization': `Bearer ${token}`
}
}
);
const data = await response.json();
return data.data; // { widget_variant, widget_placement, colors, etc. }
};PATCH обновление настроек
const updateWidgetSettings = async (channelId, token, settings) => {
const response = await fetch(
`https://backend-dev.dmva.acebox.eu/v1/channels/${channelId}/voice-widget-settings`,
{
method: 'PATCH',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
widget_btn_color: settings.buttonColor,
widget_placement: settings.position,
widget_variant: settings.variant,
widget_disable_banner: settings.hideBranding
})
}
);
const data = await response.json();
return data.data; // Обновлённые настройки + synced: true
};
// Использование
const newSettings = await updateWidgetSettings(
'30f45496-35a9-402c-9ca6-525fd8455d8c',
token,
{
buttonColor: '#ff0000',
position: 'bottom-left',
variant: 'full',
hideBranding: true
}
);
console.log('Synced with ElevenLabs:', newSettings.synced); // true🎨 Доступные параметры кастомизации
Query параметры для /direct endpoint
| Параметр | Тип | Значения | По умолчанию |
|---|---|---|---|
theme | string | light, dark, custom | light |
position | string | bottom-right, bottom-left, top-right, top-left | bottom-right |
size | string | small, medium, large | medium |
language | string | en, ru, pl, de, fr, etc. | en |
color | string | Hex код (URL encoded: %23007bff) | #007bff |
hidebranding | boolean | true, false | true |
Widget settings (для PATCH запроса)
| Поле | Тип | Описание |
|---|---|---|
widget_variant | string | tiny, compact, full, expandable |
widget_placement | string | Позиция на странице |
widget_btn_color | string | Hex цвет кнопки (#007bff) |
widget_btn_text_color | string | Hex цвет текста кнопки |
widget_bg_color | string | Hex цвет фона виджета |
widget_text_color | string | Hex цвет текста виджета |
widget_avatar_type | string | orb, url, image |
widget_avatar_color_1 | string | Hex цвет для orb (градиент 1) |
widget_avatar_color_2 | string | Hex цвет для orb (градиент 2) |
widget_avatar_url | string | URL аватара (для type=url) |
widget_disable_banner | boolean | Скрыть брендинг ElevenLabs |
🧪 Тестирование
Тестовые данные (DEV server)
const TEST_CHANNEL_ID = "30f45496-35a9-402c-9ca6-525fd8455d8c";
const TEST_AGENT_ID = "cc7ce3ab-b899-4f40-b700-3c94e2172f1e";
const API_URL = "https://backend-dev.dmva.acebox.eu";Проверка работы виджета
// 1. Получить код виджета
const testWidget = async () => {
const response = await fetch(
`${API_URL}/v1/voice-widget/${TEST_CHANNEL_ID}/direct`
);
const data = await response.json();
console.log('Widget code:', data.data.full_code);
console.log('Agent ID:', data.data.agent_id);
console.log('Configuration:', data.data.configuration);
};
testWidget();Live Demo
Откройте в браузере: https://backend-dev.dmva.acebox.eu/voice-widget-direct-demo.html
⚠️ Важные моменты
1. Используйте /direct, НЕ /embed!
КРИТИЧЕСКИ ВАЖНО: Фронтенд должен использовать endpoint /direct:
// ✅ ПРАВИЛЬНО
fetch(`/v1/voice-widget/${channelId}/direct`)
// ❌ НЕПРАВИЛЬНО
fetch(`/v1/voice-widget/${channelId}/embed`) // ← Возвращает iframe HTML!Как проверить что используется правильный endpoint:
- В Network tab браузера должен быть запрос к
/direct - Ответ должен содержать
html_snippet,script_tag,full_code - НЕ должен быть iframe с
srcdocилиsrc
2. Без iframe!
Виджет НЕ использует iframe. Он интегрируется напрямую в DOM через Web Component.
✅ Преимущества:
- Идеальное изменение размера
- Отличный mobile UX
- Плавные анимации
- Нет проблем с CORS
2. Один виджет на страницу
Не загружайте виджет несколько раз на одной странице. Проверяйте:
if (!document.querySelector('elevenlabs-convai')) {
// Загружаем виджет
}3. Cleanup в React/Vue
Удаляйте виджет при размонтировании компонента:
// React
useEffect(() => {
// ... загрузка виджета
return () => {
document.querySelector('elevenlabs-convai')?.remove();
document.querySelector('script[src*="convai-widget-embed"]')?.remove();
};
}, [channelId]);4. Асинхронная загрузка
Скрипт ElevenLabs загружается асинхронно. Виджет появится когда скрипт загрузится.
5. Error handling
Всегда обрабатывайте ошибки:
try {
const response = await fetch(url);
const data = await response.json();
if (data.status === 'error') {
throw new Error(data.message);
}
// ... успешная загрузка
} catch (error) {
console.error('Widget error:', error);
// Показать fallback UI
}🔄 Полный пример: CRM Dashboard
import React, { useState, useEffect } from 'react';
import VoiceWidget from './components/VoiceWidget';
const CustomerDashboard = ({ customerId }) => {
const [channelId, setChannelId] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
// Получаем channel_id для клиента
const fetchChannelId = async () => {
try {
const response = await fetch(
`https://backend-dev.dmva.acebox.eu/v1/customers/${customerId}/voice-channel`,
{
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}`
}
}
);
const data = await response.json();
setChannelId(data.data.channel_id);
} catch (error) {
console.error('Failed to fetch channel:', error);
} finally {
setLoading(false);
}
};
fetchChannelId();
}, [customerId]);
if (loading) {
return <div>Загрузка...</div>;
}
return (
<div className="dashboard">
<h1>Личный кабинет</h1>
{/* Остальной контент */}
{/* Voice Widget */}
{channelId && (
<VoiceWidget
channelId={channelId}
theme="light"
language="ru"
/>
)}
</div>
);
};📞 Support & Troubleshooting
Виджет не появляется
- Проверьте консоль браузера на ошибки
- Убедитесь что
channelIdкорректный - Проверьте что скрипт ElevenLabs загрузился:javascript
console.log(document.querySelector('script[src*="convai-widget-embed"]'));
Неправильные цвета
- Обновите настройки через PATCH
/voice-widget-settings - Убедитесь что изменения синхронизировались (
synced: true) - Обновите страницу (Ctrl+F5)
Брендинг ElevenLabs виден
Убедитесь что widget_disable_banner: true в настройках канала.
🎯 Чеклист для фронтенд-разработчика
- [ ] Получен
channelIdот бэкенда - [ ] Выбрана framework integration (React/Vue/Vanilla JS)
- [ ] Скопирован соответствующий компонент
- [ ] Добавлен channelId в пропсы/параметры
- [ ] Реализован cleanup (для React/Vue)
- [ ] Добавлен error handling
- [ ] Протестировано на DEV
- [ ] Протестировано на мобильных устройствах
- [ ] Проверен cleanup при размонтировании
- [ ] Готово для ALPHA/PRODUCTION
🔗 Полезные ссылки
- API Docs: https://backend-dev.dmva.acebox.eu/docs
- Live Demo: https://backend-dev.dmva.acebox.eu/voice-widget-direct-demo.html
- CRM Integration Guide: CRM-VOICE-CHANNEL-API-GUIDE.md
Всё готово для интеграции! 🚀
Если возникнут вопросы - смотрите примеры выше или тестируйте на live demo.