Skip to content

Инструкция для фронтенд-разработчиков: Интеграция 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):

javascript
// НЕ ДЕЛАЙТЕ ТАК!
fetch(`/v1/voice-widget/${channelId}/embed`)  // ← УСТАРЕВШИЙ ENDPOINT

✅ ПРАВИЛЬНО (новый подход без iframe):

javascript
// ИСПОЛЬЗУЙТЕ ЭТО!
fetch(`/v1/voice-widget/${channelId}/direct`)  // ← ПРАВИЛЬНЫЙ ENDPOINT

Почему /direct, а не /embed:

  • /embed - возвращает HTML для iframe (проблемы с resize, UX)
  • /direct - возвращает код для прямой интеграции (идеальный UX)

🎯 Что нужно передать на фронтенд

1. Channel ID

После создания webvoice канала через API, сохраните и передайте на фронтенд:

javascript
const channelId = "30f45496-35a9-402c-9ca6-525fd8455d8c"; // Получен из POST /v1/channels

2. JWT Token (для управления настройками)

Если фронтенд будет управлять настройками виджета:

javascript
const token = "eyJhbGciOiJSUzI1NiJ9..."; // Из POST /v1/auth/login

🚀 Быстрая интеграция (3 шага)

Шаг 1: Получить код виджета

javascript
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: Вставить код на страницу

javascript
// В 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:

bash
curl "https://backend-dev.dmva.acebox.eu/v1/voice-widget/30f45496-35a9-402c-9ca6-525fd8455d8c/direct"

Response:

json
{
  "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 - только скрипт ElevenLabs
  • usage_examples.react - пример для React
  • usage_examples.vue - пример для Vue

⚛️ React Integration

Компонент VoiceWidget

jsx
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;

Использование в приложении

jsx
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>
  );
}

С кастомизацией

jsx
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

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>

Использование в приложении

vue
<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

Простой вариант

html
<!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>

С кастомизацией

javascript
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 текущих настроек

javascript
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 обновление настроек

javascript
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

ПараметрТипЗначенияПо умолчанию
themestringlight, dark, customlight
positionstringbottom-right, bottom-left, top-right, top-leftbottom-right
sizestringsmall, medium, largemedium
languagestringen, ru, pl, de, fr, etc.en
colorstringHex код (URL encoded: %23007bff)#007bff
hidebrandingbooleantrue, falsetrue

Widget settings (для PATCH запроса)

ПолеТипОписание
widget_variantstringtiny, compact, full, expandable
widget_placementstringПозиция на странице
widget_btn_colorstringHex цвет кнопки (#007bff)
widget_btn_text_colorstringHex цвет текста кнопки
widget_bg_colorstringHex цвет фона виджета
widget_text_colorstringHex цвет текста виджета
widget_avatar_typestringorb, url, image
widget_avatar_color_1stringHex цвет для orb (градиент 1)
widget_avatar_color_2stringHex цвет для orb (градиент 2)
widget_avatar_urlstringURL аватара (для type=url)
widget_disable_bannerbooleanСкрыть брендинг ElevenLabs

🧪 Тестирование

Тестовые данные (DEV server)

javascript
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";

Проверка работы виджета

javascript
// 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:

javascript
// ✅ ПРАВИЛЬНО
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. Один виджет на страницу

Не загружайте виджет несколько раз на одной странице. Проверяйте:

javascript
if (!document.querySelector('elevenlabs-convai')) {
  // Загружаем виджет
}

3. Cleanup в React/Vue

Удаляйте виджет при размонтировании компонента:

javascript
// React
useEffect(() => {
  // ... загрузка виджета
  
  return () => {
    document.querySelector('elevenlabs-convai')?.remove();
    document.querySelector('script[src*="convai-widget-embed"]')?.remove();
  };
}, [channelId]);

4. Асинхронная загрузка

Скрипт ElevenLabs загружается асинхронно. Виджет появится когда скрипт загрузится.

5. Error handling

Всегда обрабатывайте ошибки:

javascript
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

jsx
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

Виджет не появляется

  1. Проверьте консоль браузера на ошибки
  2. Убедитесь что channelId корректный
  3. Проверьте что скрипт ElevenLabs загрузился:
    javascript
    console.log(document.querySelector('script[src*="convai-widget-embed"]'));

Неправильные цвета

  1. Обновите настройки через PATCH /voice-widget-settings
  2. Убедитесь что изменения синхронизировались (synced: true)
  3. Обновите страницу (Ctrl+F5)

Брендинг ElevenLabs виден

Убедитесь что widget_disable_banner: true в настройках канала.


🎯 Чеклист для фронтенд-разработчика

  • [ ] Получен channelId от бэкенда
  • [ ] Выбрана framework integration (React/Vue/Vanilla JS)
  • [ ] Скопирован соответствующий компонент
  • [ ] Добавлен channelId в пропсы/параметры
  • [ ] Реализован cleanup (для React/Vue)
  • [ ] Добавлен error handling
  • [ ] Протестировано на DEV
  • [ ] Протестировано на мобильных устройствах
  • [ ] Проверен cleanup при размонтировании
  • [ ] Готово для ALPHA/PRODUCTION

🔗 Полезные ссылки


Всё готово для интеграции! 🚀

Если возникнут вопросы - смотрите примеры выше или тестируйте на live demo.

Twinlix platform documentation.