feat: add headless mode, redesign floating widget, refactor lifecycle callbacks (#268)

* feat: add headless widget for deployment

* feat: call callbacks at the right time

* feat: add onCallConnected & onCallDisconnected callback

* feat: add a button with text for floating widget

* feat: add headless widget for deployment

* feat: call callbacks at the right time

* feat: add onCallConnected & onCallDisconnected callback

* feat: add a button with text for floating widget

* docs: web widget

* fix: format issue in pre-pr drift check

* fix: fix CD to rely on pipecat dev dependey

* chore: update message

---------

Co-authored-by: Abhishek Kumar <abhishek@a6k.me>
This commit is contained in:
Sabiha Khan 2026-05-07 12:23:41 +05:30 committed by GitHub
parent 31e2c135b0
commit d2a119c38a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
75 changed files with 803 additions and 485 deletions

View file

@ -4,13 +4,13 @@ on:
pull_request:
branches: [main]
paths:
- 'api/**'
- 'ui/**'
- 'pipecat/**'
- 'scripts/dump_docs_openapi.py'
- 'scripts/format.sh'
- 'docs/api-reference/openapi.json'
- '.github/workflows/pre-pr-drift-check.yml'
- "api/**"
- "ui/**"
- "pipecat/**"
- "scripts/dump_docs_openapi.py"
- "scripts/format.sh"
- "docs/api-reference/openapi.json"
- ".github/workflows/pre-pr-drift-check.yml"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
@ -30,7 +30,7 @@ jobs:
- name: Set up Python 3.12
uses: actions/setup-python@v5
with:
python-version: '3.12'
python-version: "3.12"
cache: pip
cache-dependency-path: |
api/requirements.txt
@ -40,15 +40,12 @@ jobs:
- name: Set up Node 22
uses: actions/setup-node@v4
with:
node-version: '22'
node-version: "22"
cache: npm
cache-dependency-path: ui/package-lock.json
- name: Install api dependencies
run: |
pip install -r api/requirements.txt
pip install -r api/requirements.dev.txt
pip install './pipecat[cartesia,deepgram,openai,elevenlabs,groq,google,azure,sarvam,soundfile,silero,webrtc,speechmatics,openrouter,camb]'
- name: Install api and pipecat dependencies
run: ./scripts/setup_requirements.sh --dev
- name: Install ui dependencies
working-directory: ui

View file

@ -3,6 +3,7 @@ import os
import sys
import loguru
from pipecat.utils.run_context import run_id_var
from api.constants import (
ENVIRONMENT,
@ -15,7 +16,6 @@ from api.constants import (
)
from api.enums import Environment
from api.utils.worker import get_worker_id, is_worker_process
from pipecat.utils.run_context import run_id_var
# Track if logging has been initialized
_logging_initialized = False

View file

@ -1,11 +1,5 @@
mypy==1.15.0
ruff==0.11.3
pytest==8.3.5
pytest-asyncio==0.26.0
pre-commit==4.2.0
watchfiles==1.1.0
python-dotenv==1.2.1
mypy==2.0.0
watchfiles==1.1.1
datamodel-code-generator==0.56.1
twine==6.2.0
build==1.2.2
-e ./sdk/python

View file

@ -17,13 +17,13 @@ from typing import Optional
from fastapi import APIRouter, WebSocket
from loguru import logger
from pipecat.utils.run_context import set_current_org_id, set_current_run_id
from starlette.websockets import WebSocketDisconnect
from api.db import db_client
from api.enums import CallType, WorkflowRunState
from api.services.quota_service import check_dograh_quota_by_user_id
from api.services.telephony import registry as telephony_registry
from pipecat.utils.run_context import set_current_org_id, set_current_run_id
router = APIRouter(prefix="/agent-stream")

View file

@ -15,6 +15,7 @@ from fastapi import (
WebSocket,
)
from loguru import logger
from pipecat.utils.run_context import set_current_run_id
from pydantic import BaseModel
from starlette.websockets import WebSocketDisconnect
@ -43,7 +44,6 @@ from api.utils.telephony_helper import (
numbers_match,
parse_webhook_request,
)
from pipecat.utils.run_context import set_current_run_id
router = APIRouter(prefix="/telephony")

View file

@ -24,6 +24,8 @@ from aiortc import RTCIceServer
from aiortc.sdp import candidate_from_sdp
from fastapi import APIRouter, Depends, HTTPException, WebSocket, WebSocketDisconnect
from loguru import logger
from pipecat.transports.smallwebrtc.connection import SmallWebRTCConnection
from pipecat.utils.run_context import set_current_org_id, set_current_run_id
from starlette.websockets import WebSocketState
from api.constants import ENVIRONMENT
@ -43,8 +45,6 @@ from api.services.pipecat.ws_sender_registry import (
unregister_ws_sender,
)
from api.services.quota_service import check_dograh_quota
from pipecat.transports.smallwebrtc.connection import SmallWebRTCConnection
from pipecat.utils.run_context import set_current_org_id, set_current_run_id
router = APIRouter(prefix="/ws")

View file

@ -9,7 +9,6 @@ import asyncio
from typing import Dict, Set
from loguru import logger
from pipecat.audio.utils import mix_audio
from pipecat.frames.frames import (
Frame,

View file

@ -3,6 +3,10 @@
from typing import Any, Dict
from loguru import logger
from pipecat.pipeline.pipeline import Pipeline
from pipecat.processors.aggregators.llm_response_universal import (
LLMContextAggregatorPair,
)
from api.db.db_client import DBClient
from api.services.looptalk.audio_streamer import get_or_create_audio_streamer
@ -23,10 +27,6 @@ from api.services.pipecat.service_factory import (
from api.services.workflow.dto import ReactFlowDTO
from api.services.workflow.pipecat_engine import PipecatEngine
from api.services.workflow.workflow import WorkflowGraph
from pipecat.pipeline.pipeline import Pipeline
from pipecat.processors.aggregators.llm_response_universal import (
LLMContextAggregatorPair,
)
class LoopTalkPipelineBuilder:

View file

@ -7,7 +7,6 @@
"""Internal frame serializer for agent-to-agent communication."""
from loguru import logger
from pipecat.frames.frames import (
Frame,
InputAudioRawFrame,

View file

@ -11,8 +11,6 @@ import time
from typing import Dict, Optional, Tuple
from loguru import logger
from api.services.looptalk.internal_serializer import InternalFrameSerializer
from pipecat.frames.frames import (
CancelFrame,
EndFrame,
@ -29,6 +27,8 @@ from pipecat.transports.base_input import BaseInputTransport
from pipecat.transports.base_output import BaseOutputTransport
from pipecat.transports.base_transport import BaseTransport, TransportParams
from api.services.looptalk.internal_serializer import InternalFrameSerializer
class InternalInputTransport(BaseInputTransport):
"""Input side of internal transport for agent-to-agent communication."""

View file

@ -6,6 +6,8 @@ from pathlib import Path
from typing import Any, Dict, Optional
from loguru import logger
from pipecat.pipeline.task import PipelineTask
from pipecat.utils.run_context import set_current_run_id
from api.db.db_client import DBClient
from api.services.looptalk.internal_transport import (
@ -13,8 +15,6 @@ from api.services.looptalk.internal_transport import (
InternalTransportManager,
)
from api.services.pipecat.transport_setup import create_internal_transport
from pipecat.pipeline.task import PipelineTask
from pipecat.utils.run_context import set_current_run_id
from .core.pipeline_builder import LoopTalkPipelineBuilder
from .core.recording_manager import RecordingManager

View file

@ -21,9 +21,8 @@ from fastapi import (
status,
)
from fastapi.websockets import WebSocketState
from scipy.io import wavfile
from pipecat.audio.turn.smart_turn.local_smart_turn_v2 import LocalSmartTurnAnalyzerV2
from scipy.io import wavfile
LOG_LEVEL = (
logging.DEBUG

View file

@ -20,7 +20,6 @@ from typing import Any, Dict, Optional
import numpy as np
import websockets
from loguru import logger
from pipecat.audio.turn.smart_turn.base_smart_turn import (
BaseSmartTurn,
SmartTurnTimeoutException,

View file

@ -6,7 +6,6 @@ This module contains the business logic for Asterisk ARI call operations.
from typing import Any, Dict
from loguru import logger
from pipecat.serializers.call_strategies import HangupStrategy, TransferStrategy

View file

@ -1,15 +1,15 @@
"""ARI (Asterisk) transport factory."""
from fastapi import WebSocket
from api.services.pipecat.audio_config import AudioConfig
from api.services.pipecat.audio_mixer import build_audio_out_mixer
from api.services.telephony.factory import load_credentials_for_transport
from pipecat.transports.websocket.fastapi import (
FastAPIWebsocketParams,
FastAPIWebsocketTransport,
)
from api.services.pipecat.audio_config import AudioConfig
from api.services.pipecat.audio_mixer import build_audio_out_mixer
from api.services.telephony.factory import load_credentials_for_transport
from .serializers import AsteriskFrameSerializer
from .strategies import ARIBridgeSwapStrategy, ARIHangupStrategy

View file

@ -8,6 +8,7 @@ import json
from fastapi import APIRouter, Request
from loguru import logger
from pipecat.utils.run_context import set_current_run_id
from api.db import db_client
from api.services.telephony.factory import get_telephony_provider_for_run
@ -15,7 +16,6 @@ from api.services.telephony.status_processor import (
StatusCallbackRequest,
_process_status_update,
)
from pipecat.utils.run_context import set_current_run_id
router = APIRouter()

View file

@ -3,9 +3,9 @@
from typing import Any, Dict
from loguru import logger
from pipecat.serializers.call_strategies import HangupStrategy
from api.services.telephony.providers.cloudonix.provider import CLOUDONIX_API_BASE_URL
from pipecat.serializers.call_strategies import HangupStrategy
class CloudonixHangupStrategy(HangupStrategy):

View file

@ -1,15 +1,15 @@
"""Cloudonix transport factory."""
from fastapi import WebSocket
from api.services.pipecat.audio_config import AudioConfig
from api.services.pipecat.audio_mixer import build_audio_out_mixer
from api.services.telephony.factory import load_credentials_for_transport
from pipecat.transports.websocket.fastapi import (
FastAPIWebsocketParams,
FastAPIWebsocketTransport,
)
from api.services.pipecat.audio_config import AudioConfig
from api.services.pipecat.audio_mixer import build_audio_out_mixer
from api.services.telephony.factory import load_credentials_for_transport
from .serializers import CloudonixFrameSerializer
from .strategies import CloudonixHangupStrategy

View file

@ -9,6 +9,7 @@ from typing import Optional
from fastapi import APIRouter, Header, Request
from loguru import logger
from pipecat.utils.run_context import set_current_run_id
from starlette.responses import HTMLResponse
from api.db import db_client
@ -18,7 +19,6 @@ from api.services.telephony.status_processor import (
_process_status_update,
)
from api.utils.common import get_backend_endpoints
from pipecat.utils.run_context import set_current_run_id
router = APIRouter()

View file

@ -1,15 +1,15 @@
"""Plivo transport factory."""
from fastapi import WebSocket
from api.services.pipecat.audio_config import AudioConfig
from api.services.pipecat.audio_mixer import build_audio_out_mixer
from api.services.telephony.factory import load_credentials_for_transport
from pipecat.transports.websocket.fastapi import (
FastAPIWebsocketParams,
FastAPIWebsocketTransport,
)
from api.services.pipecat.audio_config import AudioConfig
from api.services.pipecat.audio_mixer import build_audio_out_mixer
from api.services.telephony.factory import load_credentials_for_transport
from .serializers import PlivoFrameSerializer

View file

@ -9,7 +9,7 @@ import random
from typing import TYPE_CHECKING, Any, Dict, List, Optional
import aiohttp
from fastapi import HTTPException
from fastapi import HTTPException, WebSocketDisconnect
from loguru import logger
from api.enums import WorkflowRunMode
@ -22,8 +22,6 @@ from api.services.telephony.base import (
from api.utils.common import get_backend_endpoints
from api.utils.telephony_address import normalize_telephony_address
from fastapi import WebSocketDisconnect
if TYPE_CHECKING:
from fastapi import WebSocket

View file

@ -8,6 +8,7 @@ import json
from fastapi import APIRouter, Request
from loguru import logger
from pipecat.utils.run_context import set_current_run_id
from api.db import db_client
from api.services.telephony.factory import get_telephony_provider_for_run
@ -16,7 +17,6 @@ from api.services.telephony.status_processor import (
StatusCallbackRequest,
_process_status_update,
)
from pipecat.utils.run_context import set_current_run_id
router = APIRouter()

View file

@ -1,15 +1,15 @@
"""Telnyx transport factory."""
from fastapi import WebSocket
from api.services.pipecat.audio_config import AudioConfig
from api.services.pipecat.audio_mixer import build_audio_out_mixer
from api.services.telephony.factory import load_credentials_for_transport
from pipecat.transports.websocket.fastapi import (
FastAPIWebsocketParams,
FastAPIWebsocketTransport,
)
from api.services.pipecat.audio_config import AudioConfig
from api.services.pipecat.audio_mixer import build_audio_out_mixer
from api.services.telephony.factory import load_credentials_for_transport
from .serializers import TelnyxFrameSerializer

View file

@ -9,6 +9,7 @@ from typing import Optional
from fastapi import APIRouter, Header, Request
from loguru import logger
from pipecat.utils.run_context import set_current_run_id
from starlette.responses import HTMLResponse
from api.db import db_client
@ -18,7 +19,6 @@ from api.services.telephony.status_processor import (
_process_status_update,
)
from api.utils.common import get_backend_endpoints
from pipecat.utils.run_context import set_current_run_id
router = APIRouter()

View file

@ -8,7 +8,6 @@ from typing import Any, Dict
import aiohttp
from loguru import logger
from pipecat.serializers.call_strategies import HangupStrategy, TransferStrategy

View file

@ -1,15 +1,15 @@
"""Twilio transport factory."""
from fastapi import WebSocket
from api.services.pipecat.audio_config import AudioConfig
from api.services.pipecat.audio_mixer import build_audio_out_mixer
from api.services.telephony.factory import load_credentials_for_transport
from pipecat.transports.websocket.fastapi import (
FastAPIWebsocketParams,
FastAPIWebsocketTransport,
)
from api.services.pipecat.audio_config import AudioConfig
from api.services.pipecat.audio_mixer import build_audio_out_mixer
from api.services.telephony.factory import load_credentials_for_transport
from .serializers import TwilioFrameSerializer
from .strategies import TwilioConferenceStrategy, TwilioHangupStrategy

View file

@ -10,6 +10,7 @@ from typing import Optional
from fastapi import APIRouter, Header, Request
from loguru import logger
from pipecat.utils.run_context import set_current_run_id
from starlette.responses import HTMLResponse
from api.db import db_client
@ -24,7 +25,6 @@ from api.utils.common import get_backend_endpoints
from api.utils.telephony_helper import (
parse_webhook_request,
)
from pipecat.utils.run_context import set_current_run_id
router = APIRouter()

View file

@ -7,15 +7,15 @@ Vobiz uses Plivo-compatible WebSocket protocol:
from fastapi import WebSocket
from loguru import logger
from api.services.pipecat.audio_config import AudioConfig
from api.services.pipecat.audio_mixer import build_audio_out_mixer
from api.services.telephony.factory import load_credentials_for_transport
from pipecat.transports.websocket.fastapi import (
FastAPIWebsocketParams,
FastAPIWebsocketTransport,
)
from api.services.pipecat.audio_config import AudioConfig
from api.services.pipecat.audio_mixer import build_audio_out_mixer
from api.services.telephony.factory import load_credentials_for_transport
from .serializers import VobizFrameSerializer

View file

@ -9,6 +9,7 @@ from typing import Optional
from fastapi import APIRouter, Request
from loguru import logger
from pipecat.utils.run_context import set_current_run_id
from api.db import db_client
from api.services.telephony.factory import get_telephony_provider_for_run
@ -16,7 +17,6 @@ from api.services.telephony.status_processor import (
StatusCallbackRequest,
_process_status_update,
)
from pipecat.utils.run_context import set_current_run_id
router = APIRouter()

View file

@ -1,13 +1,14 @@
"""Vonage transport factory."""
from api.services.pipecat.audio_config import AudioConfig
from api.services.pipecat.audio_mixer import build_audio_out_mixer
from api.services.telephony.factory import load_credentials_for_transport
from pipecat.transports.websocket.fastapi import (
FastAPIWebsocketParams,
FastAPIWebsocketTransport,
)
from api.services.pipecat.audio_config import AudioConfig
from api.services.pipecat.audio_mixer import build_audio_out_mixer
from api.services.telephony.factory import load_credentials_for_transport
from .serializers import VonageFrameSerializer

View file

@ -1,9 +1,5 @@
from typing import TYPE_CHECKING, Awaitable, Callable, Optional, Union
from api.db import db_client
from api.services.pipecat.audio_playback import play_audio
from api.services.workflow.disposition_mapper import apply_disposition_mapping
from api.services.workflow.workflow import Node, WorkflowGraph
from pipecat.adapters.schemas.tools_schema import ToolsSchema
from pipecat.frames.frames import (
BotStartedSpeakingFrame,
@ -19,6 +15,11 @@ from pipecat.services.llm_service import FunctionCallParams
from pipecat.services.settings import LLMSettings
from pipecat.utils.enums import EndTaskReason
from api.db import db_client
from api.services.pipecat.audio_playback import play_audio
from api.services.workflow.disposition_mapper import apply_disposition_mapping
from api.services.workflow.workflow import Node, WorkflowGraph
if TYPE_CHECKING:
from pipecat.frames.frames import Frame
from pipecat.services.anthropic.llm import AnthropicLLMService

View file

@ -14,7 +14,6 @@ import re
from typing import TYPE_CHECKING
from loguru import logger
from pipecat.frames.frames import (
LLMMessagesAppendFrame,
)

View file

@ -6,8 +6,6 @@ from typing import TYPE_CHECKING, Optional
from loguru import logger
from opentelemetry import trace
from api.services.pipecat.tracing_config import ensure_tracing
from pipecat.frames.frames import LLMContextSummaryRequestFrame
from pipecat.utils.context.llm_context_summarization import (
LLMContextSummarizationUtil,
@ -15,6 +13,8 @@ from pipecat.utils.context.llm_context_summarization import (
)
from pipecat.utils.tracing.service_attributes import add_llm_span_attributes
from api.services.pipecat.tracing_config import ensure_tracing
if TYPE_CHECKING:
from api.services.workflow.pipecat_engine import PipecatEngine

View file

@ -13,6 +13,13 @@ import uuid
from typing import TYPE_CHECKING, Any, Dict, List, Optional
from loguru import logger
from pipecat.adapters.schemas.function_schema import FunctionSchema
from pipecat.frames.frames import (
FunctionCallResultProperties,
TTSSpeakFrame,
)
from pipecat.services.llm_service import FunctionCallParams
from pipecat.utils.enums import EndTaskReason
from api.db import db_client
from api.enums import ToolCategory, WorkflowRunMode
@ -25,13 +32,6 @@ from api.services.workflow.tools.custom_tool import (
execute_http_tool,
tool_to_function_schema,
)
from pipecat.adapters.schemas.function_schema import FunctionSchema
from pipecat.frames.frames import (
FunctionCallResultProperties,
TTSSpeakFrame,
)
from pipecat.services.llm_service import FunctionCallParams
from pipecat.utils.enums import EndTaskReason
if TYPE_CHECKING:
from api.services.workflow.pipecat_engine import PipecatEngine

View file

@ -5,12 +5,12 @@ from typing import TYPE_CHECKING, Any, List
from loguru import logger
from opentelemetry import trace
from pipecat.processors.aggregators.llm_context import LLMContext
from pipecat.utils.tracing.service_attributes import add_llm_span_attributes
from api.services.gen_ai.json_parser import parse_llm_json
from api.services.pipecat.tracing_config import ensure_tracing
from api.services.workflow.dto import ExtractionVariableDTO
from pipecat.processors.aggregators.llm_context import LLMContext
from pipecat.utils.tracing.service_attributes import add_llm_span_attributes
if TYPE_CHECKING:
from api.services.workflow.pipecat_engine import PipecatEngine

View file

@ -4,6 +4,7 @@ import json
from typing import Any
from loguru import logger
from pipecat.processors.aggregators.llm_context import LLMContext
from api.db.models import WorkflowRunModel
from api.services.gen_ai.json_parser import parse_llm_json
@ -26,7 +27,6 @@ from api.services.workflow.qa.tracing import (
setup_langfuse_parent_context,
)
from api.utils.template_renderer import render_template
from pipecat.processors.aggregators.llm_context import LLMContext
async def _run_llm_inference(

View file

@ -3,6 +3,7 @@
from typing import Any
from loguru import logger
from pipecat.processors.aggregators.llm_context import LLMContext
from api.db import db_client
from api.db.models import WorkflowRunModel
@ -10,7 +11,6 @@ from api.services.pipecat.service_factory import create_llm_service_from_provide
from api.services.workflow.dto import NodeType, QANodeData
from api.services.workflow.qa.llm_config import resolve_llm_config
from api.services.workflow.qa.tracing import create_node_summary_trace
from pipecat.processors.aggregators.llm_context import LLMContext
NODE_SUMMARY_SYSTEM_PROMPT = (
"You are analyzing a voice AI agent script. This is only a part of a larger script. "

View file

@ -78,7 +78,6 @@ def add_qa_span_to_trace(
return
try:
from opentelemetry import trace as otel_trace
from pipecat.utils.tracing.service_attributes import add_llm_span_attributes
tracer = otel_trace.get_tracer("pipecat")
@ -122,9 +121,9 @@ def create_node_summary_trace(
try:
from opentelemetry import trace as otel_trace
from opentelemetry.context import Context
from pipecat.utils.tracing.service_attributes import add_llm_span_attributes
from api.services.pipecat.tracing_config import ensure_tracing
from pipecat.utils.tracing.service_attributes import add_llm_span_attributes
if not ensure_tracing():
return None

View file

@ -5,6 +5,8 @@ from typing import Any, Dict, Optional
import httpx
from loguru import logger
from pipecat.utils.enums import EndTaskReason
from pipecat.utils.run_context import set_current_org_id, set_current_run_id
from pydantic import ValidationError
from api.constants import BACKEND_API_ENDPOINT
@ -21,8 +23,6 @@ from api.services.workflow.dto import (
from api.services.workflow.qa import run_per_node_qa_analysis
from api.utils.credential_auth import build_auth_header
from api.utils.template_renderer import render_template
from pipecat.utils.enums import EndTaskReason
from pipecat.utils.run_context import set_current_org_id, set_current_run_id
def _should_skip_qa(

View file

@ -2,12 +2,12 @@ import os
from typing import Optional
from loguru import logger
from pipecat.utils.run_context import set_current_run_id
from api.db import db_client
from api.services.pricing.workflow_run_cost import calculate_workflow_run_cost
from api.services.storage import get_current_storage_backend, storage_fs
from api.tasks.run_integrations import run_integrations_post_workflow_run
from pipecat.utils.run_context import set_current_run_id
async def upload_voicemail_audio_to_s3(

View file

@ -29,11 +29,12 @@ from contextlib import ExitStack, contextmanager
from typing import Any
from unittest.mock import AsyncMock, patch
from api.db.models import OrganizationModel, UserModel
from api.enums import WorkflowRunMode
from pipecat.frames.frames import Frame
from pipecat.observers.base_observer import BaseObserver
from pipecat.processors.frame_processor import FrameDirection, FrameProcessor
from api.db.models import OrganizationModel, UserModel
from api.enums import WorkflowRunMode
from pipecat.tests import MockLLMService, MockTTSService
USER_CONFIGURATION: dict[str, Any] = {

View file

@ -17,6 +17,8 @@ completion flag, and ``gathered_context`` entries.
import asyncio
import pytest
from pipecat.tests.mock_transport import MockTransport
from pipecat.transports.base_transport import TransportParams
from api.enums import WorkflowRunMode, WorkflowRunState
from api.services.pipecat.audio_config import create_audio_config
@ -25,8 +27,6 @@ from api.tests.integrations._run_pipeline_helpers import (
create_workflow_run_rows,
patch_run_pipeline_externals,
)
from pipecat.tests.mock_transport import MockTransport
from pipecat.transports.base_transport import TransportParams
WORKFLOW_DEFINITION = {
"nodes": [

View file

@ -28,6 +28,10 @@ deterministic and the synthesised audio length is short.
import asyncio
import pytest
from pipecat.frames.frames import TranscriptionFrame
from pipecat.tests.mock_transport import MockTransport
from pipecat.transports.base_transport import TransportParams
from pipecat.utils.time import time_now_iso8601
from api.enums import WorkflowRunMode, WorkflowRunState
from api.services.pipecat.audio_config import create_audio_config
@ -36,11 +40,7 @@ from api.tests.integrations._run_pipeline_helpers import (
create_workflow_run_rows,
patch_run_pipeline_externals,
)
from pipecat.frames.frames import TranscriptionFrame
from pipecat.tests import MockLLMService, MockTTSService
from pipecat.tests.mock_transport import MockTransport
from pipecat.transports.base_transport import TransportParams
from pipecat.utils.time import time_now_iso8601
GREETING_TEXT = (
"Thanks for calling Happy Feet, this is Sarah. How can I help you today?"

View file

@ -12,12 +12,6 @@ from typing import Any, Dict
from unittest.mock import AsyncMock, Mock, patch
import pytest
from api.services.workflow.pipecat_engine_custom_tools import get_function_schema
from api.services.workflow.tools.custom_tool import (
execute_http_tool,
tool_to_function_schema,
)
from pipecat.adapters.schemas.tools_schema import ToolsSchema
from pipecat.frames.frames import (
FunctionCallInProgressFrame,
@ -31,6 +25,12 @@ from pipecat.frames.frames import (
from pipecat.pipeline.pipeline import Pipeline
from pipecat.processors.aggregators.llm_context import LLMContext
from pipecat.services.llm_service import FunctionCallParams
from api.services.workflow.pipecat_engine_custom_tools import get_function_schema
from api.services.workflow.tools.custom_tool import (
execute_http_tool,
tool_to_function_schema,
)
from pipecat.tests import MockLLMService, run_test
@ -721,9 +721,10 @@ class TestCustomToolManagerUnit:
async def test_get_tool_schemas_returns_correct_format(self):
"""Test that get_tool_schemas returns FunctionSchema objects."""
# Create a mock engine
from pipecat.adapters.schemas.function_schema import FunctionSchema
from api.services.workflow.pipecat_engine import PipecatEngine
from api.services.workflow.pipecat_engine_custom_tools import CustomToolManager
from pipecat.adapters.schemas.function_schema import FunctionSchema
mock_engine = Mock()
mock_engine._workflow_run_id = 1

View file

@ -9,15 +9,15 @@ This module tests the full flow of:
from unittest.mock import AsyncMock, patch
import pytest
from pipecat.adapters.schemas.function_schema import FunctionSchema
from pipecat.adapters.schemas.tools_schema import ToolsSchema
from pipecat.processors.aggregators.llm_context import LLMContext
from api.services.workflow.pipecat_engine_custom_tools import (
CustomToolManager,
get_function_schema,
)
from api.tests.conftest import MockToolModel
from pipecat.adapters.schemas.function_schema import FunctionSchema
from pipecat.adapters.schemas.tools_schema import ToolsSchema
from pipecat.processors.aggregators.llm_context import LLMContext
def _update_llm_context(context, system_message, functions):

View file

@ -18,14 +18,6 @@ from typing import List
from unittest.mock import AsyncMock, patch
import pytest
from api.services.workflow.pipecat_engine import PipecatEngine
from api.services.workflow.workflow import WorkflowGraph
from api.tests.conftest import (
AGENT_SYSTEM_PROMPT,
END_CALL_SYSTEM_PROMPT,
START_CALL_SYSTEM_PROMPT,
)
from pipecat.frames.frames import LLMContextFrame
from pipecat.pipeline.pipeline import Pipeline
from pipecat.pipeline.runner import PipelineRunner
@ -35,13 +27,21 @@ from pipecat.processors.aggregators.llm_response_universal import (
LLMAssistantAggregatorParams,
LLMContextAggregatorPair,
)
from pipecat.tests.mock_transport import MockTransport
from pipecat.transports.base_transport import TransportParams
from api.services.workflow.pipecat_engine import PipecatEngine
from api.services.workflow.workflow import WorkflowGraph
from api.tests.conftest import (
AGENT_SYSTEM_PROMPT,
END_CALL_SYSTEM_PROMPT,
START_CALL_SYSTEM_PROMPT,
)
from pipecat.tests import (
ContextCapturingMockLLM,
MockLLMService,
MockTTSService,
)
from pipecat.tests.mock_transport import MockTransport
from pipecat.transports.base_transport import TransportParams
async def run_pipeline_and_capture_context(

View file

@ -23,6 +23,23 @@ from typing import Any, Dict, List
from unittest.mock import AsyncMock, patch
import pytest
from pipecat.frames.frames import Frame, LLMContextFrame
from pipecat.pipeline.pipeline import Pipeline
from pipecat.pipeline.runner import PipelineRunner
from pipecat.pipeline.task import PipelineParams, PipelineTask
from pipecat.processors.aggregators.llm_context import LLMContext
from pipecat.processors.aggregators.llm_response_universal import (
LLMAssistantAggregatorParams,
LLMContextAggregatorPair,
LLMUserAggregatorParams,
)
from pipecat.tests.mock_transport import MockTransport
from pipecat.transports.base_transport import TransportParams
from pipecat.turns.user_mute import (
CallbackUserMuteStrategy,
MuteUntilFirstBotCompleteUserMuteStrategy,
)
from pipecat.utils.enums import EndTaskReason
from api.enums import ToolCategory
from api.services.workflow.dto import (
@ -42,24 +59,7 @@ from api.services.workflow.pipecat_engine_variable_extractor import (
)
from api.services.workflow.workflow import WorkflowGraph
from api.tests.conftest import END_CALL_SYSTEM_PROMPT, START_CALL_SYSTEM_PROMPT
from pipecat.frames.frames import Frame, LLMContextFrame
from pipecat.pipeline.pipeline import Pipeline
from pipecat.pipeline.runner import PipelineRunner
from pipecat.pipeline.task import PipelineParams, PipelineTask
from pipecat.processors.aggregators.llm_context import LLMContext
from pipecat.processors.aggregators.llm_response_universal import (
LLMAssistantAggregatorParams,
LLMContextAggregatorPair,
LLMUserAggregatorParams,
)
from pipecat.tests import MockLLMService, MockTTSService
from pipecat.tests.mock_transport import MockTransport
from pipecat.transports.base_transport import TransportParams
from pipecat.turns.user_mute import (
CallbackUserMuteStrategy,
MuteUntilFirstBotCompleteUserMuteStrategy,
)
from pipecat.utils.enums import EndTaskReason
class EndCallTestHelper:

View file

@ -15,9 +15,6 @@ import asyncio
from unittest.mock import AsyncMock, patch
import pytest
from api.services.workflow.pipecat_engine import PipecatEngine
from api.services.workflow.workflow import WorkflowGraph
from pipecat.frames.frames import (
Frame,
FunctionCallResultFrame,
@ -36,7 +33,6 @@ from pipecat.processors.aggregators.llm_response_universal import (
LLMUserAggregatorParams,
)
from pipecat.processors.frame_processor import FrameDirection, FrameProcessor
from pipecat.tests import MockLLMService, MockTTSService
from pipecat.tests.mock_transport import MockTransport
from pipecat.transports.base_transport import TransportParams
from pipecat.turns.user_mute import (
@ -52,6 +48,10 @@ from pipecat.turns.user_stop import (
from pipecat.turns.user_turn_strategies import UserTurnStrategies
from pipecat.utils.time import time_now_iso8601
from api.services.workflow.pipecat_engine import PipecatEngine
from api.services.workflow.workflow import WorkflowGraph
from pipecat.tests import MockLLMService, MockTTSService
class UserSpeechInjector(FrameProcessor):
"""Processor that injects user speaking frames on FunctionCallResultFrame.

View file

@ -9,10 +9,6 @@ from typing import Any, Dict, List
from unittest.mock import AsyncMock, patch
import pytest
from api.services.workflow.pipecat_engine import PipecatEngine
from api.services.workflow.workflow import WorkflowGraph
from api.tests.conftest import END_CALL_SYSTEM_PROMPT
from pipecat.frames.frames import LLMContextFrame
from pipecat.pipeline.pipeline import Pipeline
from pipecat.pipeline.runner import PipelineRunner
@ -22,10 +18,14 @@ from pipecat.processors.aggregators.llm_response_universal import (
LLMAssistantAggregatorParams,
LLMContextAggregatorPair,
)
from pipecat.tests import MockLLMService, MockTTSService
from pipecat.tests.mock_transport import MockTransport
from pipecat.transports.base_transport import TransportParams
from api.services.workflow.pipecat_engine import PipecatEngine
from api.services.workflow.workflow import WorkflowGraph
from api.tests.conftest import END_CALL_SYSTEM_PROMPT
from pipecat.tests import MockLLMService, MockTTSService
async def run_pipeline_with_tool_calls(
workflow: WorkflowGraph,

View file

@ -13,12 +13,6 @@ import asyncio
from unittest.mock import AsyncMock, patch
import pytest
from api.services.workflow.pipecat_engine import PipecatEngine
from api.services.workflow.pipecat_engine_variable_extractor import (
VariableExtractionManager,
)
from api.services.workflow.workflow import WorkflowGraph
from pipecat.frames.frames import LLMContextFrame
from pipecat.pipeline.pipeline import Pipeline
from pipecat.pipeline.runner import PipelineRunner
@ -29,7 +23,6 @@ from pipecat.processors.aggregators.llm_response_universal import (
LLMContextAggregatorPair,
LLMUserAggregatorParams,
)
from pipecat.tests import MockLLMService, MockTTSService
from pipecat.tests.mock_transport import MockTransport
from pipecat.transports.base_transport import TransportParams
from pipecat.turns.user_mute import (
@ -38,6 +31,13 @@ from pipecat.turns.user_mute import (
MuteUntilFirstBotCompleteUserMuteStrategy,
)
from api.services.workflow.pipecat_engine import PipecatEngine
from api.services.workflow.pipecat_engine_variable_extractor import (
VariableExtractionManager,
)
from api.services.workflow.workflow import WorkflowGraph
from pipecat.tests import MockLLMService, MockTTSService
async def _build_engine_and_pipeline(
workflow: WorkflowGraph,

View file

@ -16,12 +16,6 @@ from typing import Any, Dict, List
from unittest.mock import AsyncMock, patch
import pytest
from api.services.workflow.pipecat_engine import PipecatEngine
from api.services.workflow.pipecat_engine_variable_extractor import (
VariableExtractionManager,
)
from api.services.workflow.workflow import WorkflowGraph
from pipecat.frames.frames import LLMContextFrame
from pipecat.pipeline.pipeline import Pipeline
from pipecat.pipeline.runner import PipelineRunner
@ -31,10 +25,16 @@ from pipecat.processors.aggregators.llm_response_universal import (
LLMAssistantAggregatorParams,
LLMContextAggregatorPair,
)
from pipecat.tests import MockLLMService, MockTTSService
from pipecat.tests.mock_transport import MockTransport
from pipecat.transports.base_transport import TransportParams
from api.services.workflow.pipecat_engine import PipecatEngine
from api.services.workflow.pipecat_engine_variable_extractor import (
VariableExtractionManager,
)
from api.services.workflow.workflow import WorkflowGraph
from pipecat.tests import MockLLMService, MockTTSService
class TestVariableExtractionDuringTransitions:
"""Test that variable extraction is triggered for the correct node during transitions."""

View file

@ -2,7 +2,6 @@ import asyncio
import pytest
from loguru import logger
from pipecat.frames.frames import (
EndTaskFrame,
Frame,

View file

@ -12,6 +12,14 @@ and inspect what arrives downstream.
from typing import Optional
import pytest
from pipecat.frames.frames import (
LLMFullResponseEndFrame,
LLMTextFrame,
TTSAudioRawFrame,
TTSStartedFrame,
TTSStoppedFrame,
TTSTextFrame,
)
from api.services.pipecat.recording_audio_cache import RecordingAudio
from api.services.pipecat.recording_router_processor import (
@ -21,14 +29,6 @@ from api.services.workflow.pipecat_engine_context_composer import (
RECORDING_MARKER,
TTS_MARKER,
)
from pipecat.frames.frames import (
LLMFullResponseEndFrame,
LLMTextFrame,
TTSAudioRawFrame,
TTSStartedFrame,
TTSStoppedFrame,
TTSTextFrame,
)
from pipecat.tests import run_test
# ---------------------------------------------------------------------------

View file

@ -11,21 +11,6 @@ from typing import Any, Dict, List
from unittest.mock import AsyncMock, Mock, patch
import pytest
from api.services.pipecat.recording_audio_cache import RecordingAudio
from api.services.workflow.dto import (
EdgeDataDTO,
EndCallNodeData,
EndCallRFNode,
Position,
ReactFlowDTO,
RFEdgeDTO,
StartCallNodeData,
StartCallRFNode,
)
from api.services.workflow.pipecat_engine import PipecatEngine
from api.services.workflow.pipecat_engine_custom_tools import CustomToolManager
from api.services.workflow.workflow import WorkflowGraph
from pipecat.frames.frames import (
Frame,
LLMContextFrame,
@ -42,10 +27,25 @@ from pipecat.processors.aggregators.llm_response_universal import (
LLMAssistantAggregatorParams,
LLMContextAggregatorPair,
)
from pipecat.tests import MockLLMService, MockTTSService
from pipecat.tests.mock_transport import MockTransport
from pipecat.transports.base_transport import TransportParams
from api.services.pipecat.recording_audio_cache import RecordingAudio
from api.services.workflow.dto import (
EdgeDataDTO,
EndCallNodeData,
EndCallRFNode,
Position,
ReactFlowDTO,
RFEdgeDTO,
StartCallNodeData,
StartCallRFNode,
)
from api.services.workflow.pipecat_engine import PipecatEngine
from api.services.workflow.pipecat_engine_custom_tools import CustomToolManager
from api.services.workflow.workflow import WorkflowGraph
from pipecat.tests import MockLLMService, MockTTSService
# ─── Constants ──────────────────────────────────────────────────
START_PROMPT = "Start Call System Prompt"

View file

@ -32,12 +32,6 @@ import asyncio
from unittest.mock import AsyncMock, patch
import pytest
from api.services.workflow.pipecat_engine import PipecatEngine
from api.services.workflow.pipecat_engine_variable_extractor import (
VariableExtractionManager,
)
from api.services.workflow.workflow import WorkflowGraph
from pipecat.frames.frames import LLMContextFrame
from pipecat.pipeline.pipeline import Pipeline
from pipecat.pipeline.runner import PipelineRunner
@ -48,7 +42,6 @@ from pipecat.processors.aggregators.llm_response_universal import (
LLMContextAggregatorPair,
LLMUserAggregatorParams,
)
from pipecat.tests import MockLLMService, MockTTSService
from pipecat.tests.mock_transport import MockTransport
from pipecat.transports.base_transport import TransportParams
from pipecat.turns.user_mute import (
@ -57,6 +50,13 @@ from pipecat.turns.user_mute import (
)
from pipecat.utils.enums import EndTaskReason
from api.services.workflow.pipecat_engine import PipecatEngine
from api.services.workflow.pipecat_engine_variable_extractor import (
VariableExtractionManager,
)
from api.services.workflow.workflow import WorkflowGraph
from pipecat.tests import MockLLMService, MockTTSService
async def create_test_pipeline_with_failing_transport(
workflow: WorkflowGraph,

View file

@ -1,7 +1,6 @@
"""Tests for LLM behavior when calling an unregistered function."""
import pytest
from pipecat.frames.frames import (
FunctionCallInProgressFrame,
FunctionCallResultFrame,
@ -13,6 +12,7 @@ from pipecat.frames.frames import (
)
from pipecat.pipeline.pipeline import Pipeline
from pipecat.processors.aggregators.llm_context import LLMContext
from pipecat.tests import MockLLMService, run_test

View file

@ -13,9 +13,6 @@ import asyncio
from unittest.mock import AsyncMock, patch
import pytest
from api.services.workflow.pipecat_engine import PipecatEngine
from api.services.workflow.workflow import WorkflowGraph
from pipecat.frames.frames import (
BotStoppedSpeakingFrame,
Frame,
@ -35,7 +32,6 @@ from pipecat.processors.aggregators.llm_response_universal import (
LLMUserAggregatorParams,
)
from pipecat.processors.frame_processor import FrameDirection, FrameProcessor
from pipecat.tests import MockLLMService, MockTTSService
from pipecat.tests.mock_transport import MockTransport
from pipecat.transports.base_transport import TransportParams
from pipecat.turns.user_mute import (
@ -47,6 +43,10 @@ from pipecat.turns.user_stop import ExternalUserTurnStopStrategy
from pipecat.turns.user_turn_strategies import UserTurnStrategies
from pipecat.utils.time import time_now_iso8601
from api.services.workflow.pipecat_engine import PipecatEngine
from api.services.workflow.workflow import WorkflowGraph
from pipecat.tests import MockLLMService, MockTTSService
class UserSpeechInjector(FrameProcessor):
"""Processor that injects user speaking frames after the bot finishes speaking.

View file

@ -15,12 +15,6 @@ from typing import List
from unittest.mock import AsyncMock, patch
import pytest
from api.services.workflow.pipecat_engine import PipecatEngine
from api.services.workflow.pipecat_engine_variable_extractor import (
VariableExtractionManager,
)
from api.services.workflow.workflow import WorkflowGraph
from pipecat.frames.frames import (
BotStartedSpeakingFrame,
BotStoppedSpeakingFrame,
@ -41,7 +35,6 @@ from pipecat.processors.aggregators.llm_response_universal import (
LLMUserAggregatorParams,
)
from pipecat.processors.frame_processor import FrameDirection, FrameProcessor
from pipecat.tests import MockLLMService, MockTTSService
from pipecat.tests.mock_transport import MockTransport
from pipecat.transports.base_transport import TransportParams
from pipecat.turns.user_mute import (
@ -51,6 +44,13 @@ from pipecat.turns.user_mute import (
from pipecat.turns.user_turn_strategies import ExternalUserTurnStrategies
from pipecat.utils.time import time_now_iso8601
from api.services.workflow.pipecat_engine import PipecatEngine
from api.services.workflow.pipecat_engine_variable_extractor import (
VariableExtractionManager,
)
from api.services.workflow.workflow import WorkflowGraph
from pipecat.tests import MockLLMService, MockTTSService
class BotSpeakingObserverProcessor(FrameProcessor):
"""Observer that records mute status when bot speaking events flow upstream.

View file

@ -8,7 +8,6 @@ incoming speech as CONVERSATION or VOICEMAIL and how the main LLM responds.
import asyncio
import pytest
from pipecat.extensions.voicemail.voicemail_detector import VoicemailDetector
from pipecat.frames.frames import (
EndTaskFrame,
@ -27,7 +26,6 @@ from pipecat.processors.aggregators.llm_response_universal import (
LLMUserAggregatorParams,
)
from pipecat.processors.frame_processor import FrameDirection, FrameProcessor
from pipecat.tests import MockLLMService
from pipecat.turns.user_start import (
TranscriptionUserTurnStartStrategy,
VADUserTurnStartStrategy,
@ -38,6 +36,8 @@ from pipecat.turns.user_stop import (
from pipecat.turns.user_turn_strategies import UserTurnStrategies
from pipecat.utils.time import time_now_iso8601
from pipecat.tests import MockLLMService
class FrameInjector(FrameProcessor):
"""Simple processor that can inject frames into the pipeline."""

View file

@ -18,9 +18,9 @@ All commands below are shown for **macOS / Linux**. Expand the **Windows** tab f
### Steps
1. Fork the Dograh repository by going to https://github.com/dograh-hq/dograh
2. Clone the forked repository on your machine
2. Clone the forked repository on your machine (use `--recurse-submodules` so the pipecat submodule is pulled in too)
```
git clone https://github.com/<GITHUB_HANDLE>/dograh
git clone --recurse-submodules https://github.com/<GITHUB_HANDLE>/dograh
cd dograh
```
3. Create a python virtual environment
@ -34,19 +34,15 @@ python -m venv venv
.\venv\Scripts\Activate.ps1
```
</CodeGroup>
4. Install the requirements
```
pip install -r api/requirements.txt
```
5. Ensure you are on right version of Node.js using `node --version`
4. Ensure you are on right version of Node.js using `node --version`
```
nvm use 24
```
6. Install UI dependencies
5. Install UI dependencies
```
cd ui && npm install && cd ..
```
7. Start local docker services
6. Start local docker services
<Note>Please ensure you dont have any other instance of conflicting services running by checking `docker ps`</Note>
```
docker compose -f docker-compose-local.yaml up -d
@ -59,7 +55,7 @@ CONTAINER ID IMAGE COMMAND CREATED STATUS
6c7cb8afdf18 redis:7 "docker-entrypoint.s…" 18 seconds ago Up 18 seconds (healthy) 0.0.0.0:6379->6379/tcp, [::]:6379->6379/tcp dograh-redis-1
a57e3e92b02c minio/minio "/usr/bin/docker-ent…" 18 seconds ago Up 18 seconds (healthy) 127.0.0.1:9000-9001->9000-9001/tcp dograh-minio-1
```
8. Setup environment variables
7. Setup environment variables
<CodeGroup>
```bash macOS/Linux
cp api/.env.example api/.env && cp ui/.env.example ui/.env
@ -69,16 +65,24 @@ Copy-Item api/.env.example api/.env
Copy-Item ui/.env.example ui/.env
```
</CodeGroup>
9. Setup pipecat git submodule
8. Install Python requirements. The script initializes the pipecat submodule, installs `api/requirements.txt`, and installs pipecat with the required extras. Add the dev flag if you also want the pipecat dev dependency group (pytest, ruff, pre-commit, etc.).
<CodeGroup>
```bash macOS/Linux
bash scripts/setup_pipecat.sh
# Default (runtime only)
bash scripts/setup_requirements.sh
# Include pipecat dev dependencies
bash scripts/setup_requirements.sh --dev
```
```powershell Windows
.\scripts\setup_pipecat.ps1
# Default (runtime only)
.\scripts\setup_requirements.ps1
# Include pipecat dev dependencies
.\scripts\setup_requirements.ps1 -Dev
```
</CodeGroup>
10. Start backend services
9. Start backend services
<CodeGroup>
```bash macOS/Linux
bash scripts/start_services_dev.sh
@ -105,11 +109,11 @@ tail -f logs/latest/*.log
Get-Content logs/latest/*.log -Wait
```
</CodeGroup>
11. Start the UI
10. Start the UI
```
cd ui && npm run dev
```
12. You should be able to open the application on `localhost:3000` now
11. You should be able to open the application on `localhost:3000` now
### Next Steps
We ship with AGENTS.md and CLAUDE.md which will help the Coding Agents get started quickly with the codebase. This should help your favourite coding agents to be able to navigate the codebase quickly and you can make changes to it and suit your specification better.

View file

@ -8,5 +8,5 @@ You can deploy Dograh Platform using Docker on a remote server using Docker, eit
- [Docker](docker)
- [Custom Domain](custom-domain)
- [Web Widget](web-widget)
- [Add to Website](web-widget)
- [Heroku](heroku)

View file

@ -1,11 +1,11 @@
---
title: Web Widget
description: You can deploy and embed a Voice Agent that you create on Dograh on any Website or Mobile App, where the visitor of the website can interact with your Voice Agent.
title: Add to Website
description: Add your Dograh voice agent to any website so visitors can talk to it.
---
### How to deploy
### How to add it
You can embed your Voice Agent on any external website using the Deploy Agent dialog in your agent's settings.
Add your voice agent to any website using the Deploy Agent dialog in your agent's settings.
Step 1: Open the agent settings by clicking the gear icon in the top-right of the agent editor.
@ -15,10 +15,212 @@ Step 2: Scroll to the **Deployment** section and click **Configure Embed**.
![Go to Deployment](../images/go-to-deployment.png)
Step 3: Enable embedding, add your website's domain to **Allowed Domains**, choose either **Floating Widget** or **Inline Component**, customize the button (position, color, text), and click **Save Configurations**.
Step 3: Enable embedding, add your website's domain to **Allowed Domains**, choose **Floating Widget**, **Inline Component**, or **Headless (Bring Your Own UI)**, customize the button (position, color, text) if applicable, and click **Save Configurations**.
![Save configurations](../images/save-configurations.png)
Step 4: Copy the generated embed code and paste it into your web page to test your agent.
![Copy deployment code](../images/copy-deployment-code.png)
## Embed modes
| Mode | What it renders | When to use |
| --------------------- | -------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
| **Floating Widget** | A pill-shaped CTA button anchored to a corner of the page. | You want a turn-key chat-bubble experience that doesn't disturb your existing layout. |
| **Inline Component** | A panel rendered inside a `<div id="dograh-inline-container">` that you place in your page. | You want the agent embedded in a specific section (landing-page hero, support tab, etc.). |
| **Headless** | No UI. Only the audio pipeline plus a JavaScript API on `window.DograhWidget`. | You want full control over the UI — your own buttons, design system, framework state, animations. |
## Prerequisites
These apply to all three modes:
- Serve your page over **HTTPS** or from `http://localhost`. Browsers refuse microphone access on plain HTTP origins or `file://`.
- If you set **Allowed Domains** in the dashboard, include your test origin (e.g. `localhost`) — otherwise the widget's config and signaling requests are rejected. Leave the list empty to allow all domains.
- The embed snippet you copy from the dashboard is a single `<script>` tag that loads `dograh-widget.js` **asynchronously**. The widget auto-initializes once it loads and exposes `window.DograhWidget`. Code that registers callbacks must wait for the widget to be available.
## Floating Widget
![Floating widget shown in the corner of a host page](../images/floating-widget-example.png)
Renders a pill-shaped button (microphone icon + text) anchored to a corner of the page. Clicking it starts a call; clicking again ends it. The button auto-updates its label and color across the call lifecycle: configured text → "Connecting…" → "End Call" → "Retry" on failure.
Configure **Button Text**, **Button Color**, and **Position** (top/bottom + left/right) from the dashboard.
The host page writes no JavaScript — pasting the embed snippet is the entire integration. If you want to subscribe to call lifecycle events (e.g. analytics), see [Lifecycle callbacks](#lifecycle-callbacks-all-modes) below
## Inline Component
![Inline widget rendered inside a page section](../images/inline-widget-example.png)
Renders a panel (status icon + status text + CTA button) inside a `<div>` you place in your page. Status changes update the panel in place.
Configure **Button Text**, **Button Color**, and **Call to Action Text** from the dashboard.
### Plain HTML
Place a container `<div>` where you want the widget to render. The widget auto-attaches to it.
```html
<!-- Paste the dograh embed snippet from the dashboard somewhere on the page -->
<div id="dograh-inline-container"></div>
```
### React
Because React mounts after the widget script may have already loaded, integrate via `initInline` on first mount and `refresh` on remount. Poll for `window.DograhWidget` to handle the async script load.
```tsx
import { useEffect } from 'react';
declare global {
interface Window {
DograhWidget?: {
initInline: (options: { container: HTMLElement }) => void;
refresh: () => void;
getState: () => { isInitialized: boolean };
};
}
}
export function Assistant() {
useEffect(() => {
let retries = 0;
const tryInit = () => {
const container = document.getElementById('dograh-inline-container');
if (window.DograhWidget && container) {
const { isInitialized } = window.DograhWidget.getState();
if (isInitialized) window.DograhWidget.refresh();
else window.DograhWidget.initInline({ container });
} else if (retries++ < 50) {
setTimeout(tryInit, 100);
}
};
tryInit();
}, []);
return <div id="dograh-inline-container" />;
}
```
## Headless Mode
![Headless widget driven by host-page UI](../images/headless-widget-example.png)
In Headless mode the widget injects no UI of its own. You render whatever buttons, banners, or in-call indicators you want, and call the JavaScript API to start and end calls.
### JavaScript API
| Method / Callback | Description |
| ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `window.DograhWidget.start()` | Begin a voice call. Must be called from inside a user-gesture handler (e.g. `click`) so the browser grants microphone access. |
| `window.DograhWidget.end()` | End the active call. |
| `window.DograhWidget.onCallStart(cb)` | Fires when `start()` is invoked (status `connecting`). No payload. |
| `window.DograhWidget.onCallConnected(cb)` | Fires when the WebRTC connection is established. Payload: `{ agentId, workflowRunId, token }`. |
| `window.DograhWidget.onCallDisconnected(cb)` | Fires only if the call had connected, when teardown runs. Payload: `{ agentId, workflowRunId, token, durationSeconds }`. |
| `window.DograhWidget.onCallEnd(cb)` | Fires whenever the call session is torn down (including failed-to-connect attempts). No payload. |
| `window.DograhWidget.onStatusChange(cb)` | Fires on every status change. Callback receives `(status, text, subtext)`. Status values: `idle`, `connecting`, `connected`, `failed`. |
| `window.DograhWidget.onError(cb)` | Fires on errors (mic permission denied, server error, etc.). Callback receives an `Error` object. |
All `on*` setters are single-listener — calling the same one again replaces the previous handler.
<Note>
**About timing.** The widget script loads asynchronously, so `window.DograhWidget` may not exist at the moment your inline `<script>` first runs. The examples below assume `window.DograhWidget` is already available when registration runs. To guarantee that:
- **Vanilla JS:** wrap your registration code in `window.addEventListener('load', () => { /* register here */ })`.
- **React:** inside `useEffect`, register immediately if `document.readyState === 'complete'`, otherwise add a one-time `window.load` listener that registers on fire.
- **Click handlers** that call `start()` / `end()` don't need a guard — by the time a user clicks, the widget has long since loaded.
</Note>
### Vanilla JS
```html
<button id="talk-btn">Talk to AI</button>
<script>
let callStatus = 'idle';
const btn = document.getElementById('talk-btn');
function render() {
btn.textContent =
callStatus === 'connected' ? 'End Call'
: callStatus === 'connecting' ? 'Connecting…'
: callStatus === 'failed' ? 'Retry'
: 'Talk to AI';
}
window.DograhWidget.onStatusChange((status) => {
callStatus = status;
render();
});
window.DograhWidget.onError((err) => {
console.error('Dograh error:', err.message);
});
btn.addEventListener('click', () => {
if (callStatus === 'connected' || callStatus === 'connecting') {
window.DograhWidget.end();
} else {
window.DograhWidget.start();
}
});
</script>
```
### React + TypeScript
```tsx
import { useEffect, useState } from 'react';
type CallStatus = 'idle' | 'connecting' | 'connected' | 'failed';
declare global {
interface Window {
DograhWidget: {
start: () => void;
end: () => void;
onStatusChange: (cb: (status: CallStatus, text?: string, subtext?: string) => void) => void;
onError: (cb: (err: Error) => void) => void;
};
}
}
export function TalkButton() {
const [status, setStatus] = useState<CallStatus>('idle');
useEffect(() => {
window.DograhWidget.onStatusChange((s) => setStatus(s));
window.DograhWidget.onError((err) => console.error('Dograh error:', err.message));
}, []);
const isLive = status === 'connected' || status === 'connecting';
const label = { idle: 'Talk to AI', connecting: 'Connecting…', connected: 'End Call', failed: 'Retry' }[status];
return (
<button onClick={() => (isLive ? window.DograhWidget.end() : window.DograhWidget.start())}>
{label}
</button>
);
}
```
<Note>
`start()` must run inside a real user-gesture handler (`click`, `touchend`, etc.). Browsers refuse to grant microphone access to scripts that request it outside of one — calling `start()` from a `setTimeout` or on page load will fail with a permission error.
</Note>
## Lifecycle callbacks (all modes)
The `on*` callbacks in the [Headless JavaScript API](#javascript-api) work in **all three embed modes**, not just Headless. Use them for analytics or to trigger UI in the host page even when the widget is rendering its own UI (Floating or Inline).
```js
window.DograhWidget.onCallConnected(({ agentId, workflowRunId }) => {
analytics.track('voice_call_started', { agentId, workflowRunId });
});
window.DograhWidget.onCallDisconnected(({ workflowRunId, durationSeconds }) => {
analytics.track('voice_call_ended', { workflowRunId, durationSeconds });
});
```
`onCallConnected` and `onCallDisconnected` only fire when the call actually establishes a media connection — failed-to-connect attempts (e.g. denied mic, network failure) don't trigger them, so analytics stay clean.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 126 KiB

After

Width:  |  Height:  |  Size: 333 KiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 403 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 284 KiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 998 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 464 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 465 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 237 KiB

Before After
Before After

View file

@ -1,25 +0,0 @@
#!/usr/bin/env pwsh
# Setup script for using pipecat as a git submodule (Windows)
$ErrorActionPreference = 'Stop'
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$BaseDir = Split-Path -Parent $ScriptDir
Set-Location $BaseDir
Write-Host "Setting up pipecat as a git submodule..."
# Initialize and update submodules
Write-Host "Initializing git submodules..."
git submodule update --init --recursive
# Install dograh API requirements first so pipecat's extras win on any
# shared transitive dependencies (matches api/Dockerfile and CI workflow).
Write-Host "Installing dograh API requirements..."
pip install -r api/requirements.txt
# Install pipecat in editable mode with all extras
Write-Host "Installing pipecat dependencies..."
pip install -e './pipecat[cartesia,deepgram,openai,elevenlabs,groq,google,azure,sarvam,soundfile,silero,webrtc,speechmatics,openrouter,camb]'
Write-Host "Setup complete! Pipecat is now available as a git submodule."

View file

@ -1,26 +0,0 @@
#!/bin/bash
# Setup script for using pipecat as a git submodule
# Get the project root directory (parent of scripts)
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
DOGRAH_DIR="$(dirname "$SCRIPT_DIR")"
cd "$DOGRAH_DIR"
echo "Setting up pipecat as a git submodule..."
# Initialize and update submodules
echo "Initializing git submodules..."
git submodule update --init --recursive
# Install dograh API requirements first so pipecat's extras win on any
# shared transitive dependencies (matches api/Dockerfile and CI workflow).
echo "Installing dograh API requirements..."
pip install -r api/requirements.txt
# Install pipecat in editable mode with all extras
echo "Installing pipecat dependencies..."
pip install -e ./pipecat[cartesia,deepgram,openai,elevenlabs,groq,google,azure,sarvam,soundfile,silero,webrtc,speechmatics,openrouter,camb]
echo "Setup complete! Pipecat is now available as a git submodule."

View file

@ -0,0 +1,48 @@
#!/usr/bin/env pwsh
# Setup script for using pipecat as a git submodule (Windows).
#
# Usage:
# ./scripts/setup_requirements.ps1 # default: install runtime deps
# ./scripts/setup_requirements.ps1 -Dev # also install pipecat dev deps;
# # skips git submodule update (CI
# # already checks out submodules).
[CmdletBinding()]
param(
[switch]$Dev
)
$ErrorActionPreference = 'Stop'
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$BaseDir = Split-Path -Parent $ScriptDir
Set-Location $BaseDir
Write-Host "Setting up pipecat as a git submodule..."
if (-not $Dev) {
Write-Host "Initializing git submodules..."
git submodule update --init --recursive
}
# Install dograh API requirements first so pipecat's extras win on any
# shared transitive dependencies (matches api/Dockerfile and CI workflow).
Write-Host "Installing dograh API requirements..."
pip install -r api/requirements.txt
if ($Dev) {
Write-Host "Installing dograh API dev requirements..."
pip install -r api/requirements.dev.txt
}
# Install pipecat in editable mode with all extras
Write-Host "Installing pipecat dependencies..."
pip install -e './pipecat[cartesia,deepgram,openai,elevenlabs,groq,google,azure,sarvam,soundfile,silero,webrtc,speechmatics,openrouter,camb]'
if ($Dev) {
Write-Host "Installing pipecat dev dependencies..."
pip install --upgrade pip
pip install --group pipecat/pyproject.toml:dev
}
Write-Host "Setup complete! Requirements are installed."

62
scripts/setup_requirements.sh Executable file
View file

@ -0,0 +1,62 @@
#!/bin/bash
# Setup script for using pipecat as a git submodule.
#
# Usage:
# ./scripts/setup_requirements.sh # default: install runtime deps
# ./scripts/setup_requirements.sh --dev # also install pipecat dev deps;
# # skips git submodule update (CI
# # already checks out submodules).
set -euo pipefail
DEV_MODE=0
while [[ $# -gt 0 ]]; do
case "$1" in
--dev)
DEV_MODE=1
shift
;;
*)
echo "Unknown argument: $1" >&2
echo "Usage: $0 [--dev]" >&2
exit 1
;;
esac
done
# Get the project root directory (parent of scripts)
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
DOGRAH_DIR="$(dirname "$SCRIPT_DIR")"
cd "$DOGRAH_DIR"
echo "Setting up pipecat as a git submodule..."
if [ "$DEV_MODE" -eq 0 ]; then
echo "Initializing git submodules..."
git submodule update --init --recursive
fi
# Install dograh API requirements first so pipecat's extras win on any
# shared transitive dependencies (matches api/Dockerfile and CI workflow).
echo "Installing dograh API requirements..."
pip install -r api/requirements.txt
if [ "$DEV_MODE" -eq 1 ]; then
echo "Installing dograh API dev requirements..."
pip install -r api/requirements.dev.txt
fi
# Install pipecat in editable mode with all extras
echo "Installing pipecat dependencies..."
pip install -e ./pipecat[cartesia,deepgram,openai,elevenlabs,groq,google,azure,sarvam,soundfile,silero,webrtc,speechmatics,openrouter,camb]
if [ "$DEV_MODE" -eq 1 ]; then
echo "Installing pipecat dev dependencies..."
pip install --upgrade pip
pip install --group pipecat/pyproject.toml:dev
fi
echo "Setup complete! Requirements are installed."

View file

@ -33,6 +33,8 @@
callbacks: {
onReady: null,
onCallStart: null,
onCallConnected: null,
onCallDisconnected: null,
onCallEnd: null,
onError: null,
onStatusChange: null
@ -114,7 +116,7 @@
containerId: configData.settings?.containerId || 'dograh-inline-container',
position: configData.position || DEFAULT_CONFIG.position,
buttonColor: configData.settings?.buttonColor || '#10b981',
buttonText: configData.settings?.buttonText || 'Start Call',
buttonText: configData.settings?.buttonText || 'Talk to Agent',
callToActionText: configData.settings?.callToActionText || 'Click to start voice conversation',
autoStart: configData.auto_start || false
};
@ -125,13 +127,14 @@
state.isInitialized = true;
// Load styles
injectStyles();
// Create widget UI based on mode
if (state.config.embedMode === 'inline') {
injectStyles();
createInlineWidget();
} else if (state.config.embedMode === 'headless') {
createHeadlessWidget();
} else {
injectStyles();
createFloatingWidget();
}
@ -192,68 +195,43 @@
left: 20px;
}
.dograh-widget-button {
color: white;
border: none;
border-radius: 50%;
width: 60px;
height: 60px;
cursor: pointer;
display: flex;
.dograh-widget-cta {
display: inline-flex;
align-items: center;
justify-content: center;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
transition: all 0.3s ease;
gap: 8px;
padding: 12px 20px;
border: none;
border-radius: 9999px;
color: #ffffff;
font-size: 14px;
font-weight: 600;
cursor: pointer;
white-space: nowrap;
max-width: calc(100vw - 40px);
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.2);
transition: filter 150ms ease, transform 100ms ease, box-shadow 200ms ease;
animation: dograh-cta-in 220ms ease-out;
}
.dograh-widget-button:hover {
transform: scale(1.1);
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.25);
.dograh-widget-cta:hover {
filter: brightness(1.08);
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.28);
}
.dograh-widget-cta:active { transform: scale(0.98); }
.dograh-widget-cta.dograh-state-connecting { background: #f59e0b !important; animation: dograh-pulse 1.6s infinite; }
.dograh-widget-cta.dograh-state-connected { background: #ef4444 !important; }
.dograh-widget-cta.dograh-state-failed { background: #ef4444 !important; opacity: 0.85; }
@keyframes dograh-pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.6; }
}
.dograh-widget-button:active {
transform: scale(0.95);
@keyframes dograh-cta-in {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}
/* Green button for idle/ready state */
.dograh-widget-button-idle {
background: #10b981;
}
.dograh-widget-button-idle:hover {
background: #059669;
}
/* Orange button for connecting state */
.dograh-widget-button-connecting {
background: #f59e0b;
animation: pulse 2s infinite;
}
/* Red button for connected state (to end call) */
.dograh-widget-button-connected {
background: #ef4444;
}
.dograh-widget-button-connected:hover {
background: #dc2626;
}
/* Red button for failed state */
.dograh-widget-button-failed {
background: #ef4444;
opacity: 0.8;
}
@keyframes pulse {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.6;
}
}
`;
const styleSheet = document.createElement('style');
@ -262,39 +240,81 @@
document.head.appendChild(styleSheet);
}
function ctaLabelForStatus(status) {
switch (status) {
case 'connecting': return 'Connecting…';
case 'connected': return 'End Call';
case 'failed': return 'Retry';
default: return state.config.buttonText || 'Talk to Agent';
}
}
/**
* Create floating widget UI (simplified - no modal)
* Create floating widget UI a single CTA pill button anchored to the
* configured corner of the viewport.
*/
function createFloatingWidget() {
// Create container
const container = document.createElement('div');
container.className = `dograh-widget-container ${state.config.position}`;
container.id = 'dograh-widget';
container.id = 'dograh-widget-root';
// Create button (configured color to start, red to end)
const button = document.createElement('button');
button.className = 'dograh-widget-button dograh-widget-button-idle';
button.id = 'dograh-widget-button';
button.style.backgroundColor = state.config.buttonColor;
button.innerHTML = `
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"/>
</svg>
`;
button.onclick = toggleCall;
// Create hidden audio element
const audio = document.createElement('audio');
audio.id = 'dograh-widget-audio';
audio.autoplay = true;
audio.style.display = 'none';
// Append elements
container.appendChild(button);
container.appendChild(audio);
document.body.appendChild(container);
state.audioElement = audio;
// Store audio element reference
document.body.appendChild(container);
renderFloating();
}
/**
* Render the floating CTA pill. Re-renders preserve the hidden audio
* element so an in-progress call is not interrupted on status changes.
*/
function renderFloating() {
const container = document.getElementById('dograh-widget-root');
if (!container) return;
Array.from(container.children).forEach((child) => {
if (child !== state.audioElement) container.removeChild(child);
});
const status = state.connectionStatus || 'idle';
const button = document.createElement('button');
button.id = 'dograh-widget-cta';
button.type = 'button';
button.className = `dograh-widget-cta dograh-state-${status}`;
// Idle uses configured color; status states use CSS-defined colors.
if (status === 'idle') {
button.style.backgroundColor = state.config.buttonColor;
}
button.innerHTML = `
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3z"/>
<path d="M19 10v2a7 7 0 0 1-14 0v-2"/>
<line x1="12" y1="19" x2="12" y2="23"/>
<line x1="8" y1="23" x2="16" y2="23"/>
</svg>
<span></span>
`;
button.querySelector('span').textContent = ctaLabelForStatus(status);
button.onclick = toggleCall;
container.appendChild(button);
}
/**
* Create headless widget (no UI host page drives everything via window.DograhWidget API)
*/
function createHeadlessWidget() {
const audio = document.createElement('audio');
audio.id = 'dograh-widget-audio';
audio.autoplay = true;
audio.style.display = 'none';
document.body.appendChild(audio);
state.audioElement = audio;
}
@ -309,30 +329,9 @@
}
}
/**
* Update floating widget button appearance
*/
function updateFloatingButton(status) {
const button = document.getElementById('dograh-widget-button');
if (!button) return;
// Remove all status classes
button.classList.remove('dograh-widget-button-idle', 'dograh-widget-button-connecting', 'dograh-widget-button-connected', 'dograh-widget-button-failed');
// Add current status class
button.classList.add(`dograh-widget-button-${status}`);
// Apply configured color only for idle state, let CSS handle other states
button.style.backgroundColor = status === 'idle' ? state.config.buttonColor : '';
// Update title attribute for tooltip
const titles = {
idle: 'Start Call',
connecting: 'Connecting...',
connected: 'End Call',
failed: 'Retry Call'
};
button.title = titles[status] || 'Voice Call';
state.connectionStatus = status;
renderFloating();
}
/**
@ -582,6 +581,10 @@
// Use appropriate update function based on mode
if (state.config.embedMode === 'inline') {
updateInlineStatus(status, text, subtext);
} else if (state.config.embedMode === 'headless') {
if (state.callbacks.onStatusChange) {
state.callbacks.onStatusChange(status, text, subtext);
}
} else {
updateFloatingButton(status);
}
@ -610,7 +613,6 @@
async function startCall() {
updateStatus('connecting', 'Connecting...', 'Please wait while we establish the connection');
// Trigger call start callback
if (state.callbacks.onCallStart) {
state.callbacks.onCallStart();
}
@ -768,9 +770,18 @@
console.log('ICE connection state:', state.pc.iceConnectionState);
if (state.pc.iceConnectionState === 'connected' || state.pc.iceConnectionState === 'completed') {
const wasAlreadyConnected = state.callStartedAt !== null;
updateStatus('connected', 'Connected', 'Your voice call is now active');
state.callStartedAt = Date.now();
emitMessage('dograh:call_started', {});
if (!wasAlreadyConnected) {
state.callStartedAt = Date.now();
if (state.callbacks.onCallConnected) {
state.callbacks.onCallConnected({
agentId: state.config.workflowId || null,
token: state.config.token || null,
workflowRunId: state.workflowRunId || null
});
}
}
} else if (state.pc.iceConnectionState === 'failed' || state.pc.iceConnectionState === 'disconnected') {
updateStatus('failed', 'Connection lost', 'The call has been disconnected');
stopCall();
@ -903,16 +914,21 @@
* Stop voice call
*/
function stopCall() {
// Emit end message before clearing state so identifiers are still available
const durationSeconds = state.callStartedAt
? Math.round((Date.now() - state.callStartedAt) / 1000)
: 0;
emitMessage('dograh:call_ended', { durationSeconds });
// Fire onCallDisconnected only if the call had actually connected, with
// identifiers and duration. Must run before we clear callStartedAt.
if (state.callStartedAt && state.callbacks.onCallDisconnected) {
const durationSeconds = Math.round((Date.now() - state.callStartedAt) / 1000);
state.callbacks.onCallDisconnected({
agentId: state.config.workflowId || null,
token: state.config.token || null,
workflowRunId: state.workflowRunId || null,
durationSeconds
});
}
state.callStartedAt = null;
updateStatus('idle', 'Call ended', 'Click below to start a new call');
// Trigger call end callback
if (state.callbacks.onCallEnd) {
state.callbacks.onCallEnd();
}
@ -949,22 +965,6 @@
setTimeout(() => startCall(), 500);
}
/**
* Emit a postMessage event to the host window
* Allows the embedding website to listen for agent lifecycle events via:
* window.addEventListener('message', (event) => { ... })
*/
function emitMessage(eventType, detail) {
const message = {
type: eventType,
agentId: state.config.workflowId || null,
token: state.config.token || null,
workflowRunId: state.workflowRunId || null,
...detail
};
window.postMessage(message, '*');
}
/**
* Generate unique peer ID
*/
@ -993,6 +993,8 @@
getState: () => state,
onReady: (callback) => { state.callbacks.onReady = callback; },
onCallStart: (callback) => { state.callbacks.onCallStart = callback; },
onCallConnected: (callback) => { state.callbacks.onCallConnected = callback; },
onCallDisconnected: (callback) => { state.callbacks.onCallDisconnected = callback; },
onCallEnd: (callback) => { state.callbacks.onCallEnd = callback; },
onError: (callback) => { state.callbacks.onError = callback; },
onStatusChange: (callback) => { state.callbacks.onStatusChange = callback; },
@ -1018,6 +1020,8 @@
// Set callbacks if provided
if (options.onReady) state.callbacks.onReady = options.onReady;
if (options.onCallStart) state.callbacks.onCallStart = options.onCallStart;
if (options.onCallConnected) state.callbacks.onCallConnected = options.onCallConnected;
if (options.onCallDisconnected) state.callbacks.onCallDisconnected = options.onCallDisconnected;
if (options.onCallEnd) state.callbacks.onCallEnd = options.onCallEnd;
if (options.onError) state.callbacks.onError = options.onError;
if (options.onStatusChange) state.callbacks.onStatusChange = options.onStatusChange;

View file

@ -1,4 +1,4 @@
import { Check, Copy, Loader2, Plus, Rocket, Trash2 } from "lucide-react";
import { Check, Copy, Loader2, Mic, Plus, Rocket, Trash2 } from "lucide-react";
import { useCallback, useEffect, useState } from "react";
import {
@ -61,9 +61,9 @@ export function EmbedDialog({
const [isEnabled, setIsEnabled] = useState(false);
const [domains, setDomains] = useState<string[]>([]);
const [newDomain, setNewDomain] = useState("");
const [embedMode, setEmbedMode] = useState<"floating" | "inline">("floating");
const [embedMode, setEmbedMode] = useState<"floating" | "inline" | "headless">("floating");
const [position, setPosition] = useState("bottom-right");
const [buttonText, setButtonText] = useState("Start Call");
const [buttonText, setButtonText] = useState("Talk to Agent");
const [buttonColor, setButtonColor] = useState("#10b981");
const [callToActionText, setCallToActionText] = useState("Click to start voice conversation");
@ -81,9 +81,9 @@ export function EmbedDialog({
// Load settings
if (response.data.settings) {
const settings = response.data.settings as Record<string, string>;
setEmbedMode((settings.embedMode as "floating" | "inline") || "floating");
setEmbedMode((settings.embedMode as "floating" | "inline" | "headless") || "floating");
setPosition(settings.position || "bottom-right");
setButtonText(settings.buttonText || "Start Call");
setButtonText(settings.buttonText || "Talk to Agent");
setButtonColor(settings.buttonColor || "#10b981");
setCallToActionText(settings.callToActionText || "Click to start voice conversation");
}
@ -266,7 +266,7 @@ export function EmbedDialog({
{/* Embed Mode Selection */}
<div className="space-y-4">
<Label>Embed Mode</Label>
<div className="grid grid-cols-2 gap-4">
<div className="grid grid-cols-3 gap-4">
<button
type="button"
onClick={() => setEmbedMode("floating")}
@ -299,6 +299,22 @@ export function EmbedDialog({
</div>
</div>
</button>
<button
type="button"
onClick={() => setEmbedMode("headless")}
className={`p-4 rounded-lg border-2 transition-all ${
embedMode === "headless"
? "border-primary bg-primary/5"
: "border-muted hover:border-muted-foreground/20"
}`}
>
<div className="space-y-2">
<div className="font-medium">Headless (Bring Your Own UI)</div>
<div className="text-xs text-muted-foreground">
No UI drive calls from your own buttons via the JS API
</div>
</div>
</button>
</div>
</div>
@ -306,26 +322,40 @@ export function EmbedDialog({
<div className="space-y-4">
<Label>Configuration</Label>
{/* Shared: Button Color */}
<div className="space-y-2">
<Label htmlFor="button-color" className="text-sm">Button Color</Label>
<div className="flex gap-2">
<Input
id="button-color-picker"
type="color"
value={buttonColor}
onChange={(e) => setButtonColor(e.target.value)}
className="w-14 h-10 cursor-pointer"
/>
<Input
id="button-color"
value={buttonColor}
onChange={(e) => setButtonColor(e.target.value)}
placeholder="#10b981"
className="flex-1"
/>
{/* Shared: Button Text + Button Color (skipped in headless — host renders its own UI) */}
{embedMode !== "headless" && (
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="button-text" className="text-sm">Button Text</Label>
<Input
id="button-text"
value={buttonText}
onChange={(e) => setButtonText(e.target.value)}
placeholder="Talk to Agent"
maxLength={40}
/>
</div>
<div className="space-y-2">
<Label htmlFor="button-color" className="text-sm">Button Color</Label>
<div className="flex gap-2">
<Input
id="button-color-picker"
type="color"
value={buttonColor}
onChange={(e) => setButtonColor(e.target.value)}
className="w-14 h-10 cursor-pointer"
/>
<Input
id="button-color"
value={buttonColor}
onChange={(e) => setButtonColor(e.target.value)}
placeholder="#10b981"
className="flex-1"
/>
</div>
</div>
</div>
</div>
)}
{/* Floating mode: Position */}
{embedMode === "floating" && (
@ -345,52 +375,29 @@ export function EmbedDialog({
</div>
)}
{/* Inline mode: Button Text, CTA Text */}
{/* Inline mode: Call to Action Text */}
{embedMode === "inline" && (
<>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="button-text" className="text-sm">Button Text</Label>
<Input
id="button-text"
value={buttonText}
onChange={(e) => setButtonText(e.target.value)}
placeholder="Start Call"
/>
</div>
<div className="space-y-2">
<Label htmlFor="cta-text" className="text-sm">Call to Action Text</Label>
<Input
id="cta-text"
value={callToActionText}
onChange={(e) => setCallToActionText(e.target.value)}
placeholder="Click to start voice conversation"
/>
</div>
</div>
</>
<div className="space-y-2">
<Label htmlFor="cta-text" className="text-sm">Call to Action Text</Label>
<Input
id="cta-text"
value={callToActionText}
onChange={(e) => setCallToActionText(e.target.value)}
placeholder="Click to start voice conversation"
/>
</div>
)}
{/* Preview */}
{embedMode === "floating" ? (
<div className="rounded-lg border bg-background p-4 flex items-center justify-center">
<div
className="w-[60px] h-[60px] rounded-full flex items-center justify-center shadow-lg"
style={{
backgroundColor: buttonColor,
}}
{/* Preview (skipped for headless — host renders its own UI) */}
{embedMode === "headless" ? null : embedMode === "floating" ? (
<div className="rounded-lg border bg-muted/30 p-6 flex items-center justify-center">
<button
className="inline-flex items-center gap-2 rounded-full px-5 py-3 font-medium text-white shadow-lg whitespace-nowrap"
style={{ backgroundColor: buttonColor }}
>
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="white"
strokeWidth="2"
>
<path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z" />
</svg>
</div>
<Mic className="h-4 w-4" />
{buttonText || "Talk to Agent"}
</button>
</div>
) : (
<div className="rounded-lg border bg-background p-6 flex items-center justify-center">
@ -410,6 +417,64 @@ export function EmbedDialog({
</div>
)}
{/* Headless mode: Integration Instructions */}
{embedMode === "headless" && (
<div className="space-y-3">
<div className="rounded-lg bg-muted/50 p-4">
<h4 className="font-medium mb-2">Integration Instructions</h4>
<ul className="text-sm space-y-2 text-muted-foreground">
<li> Add the embed script tag to your page (see below).</li>
<li> The widget renders no UI render your own buttons.</li>
<li> Call <code className="text-xs">window.DograhWidget.start()</code> to begin a call.</li>
<li> Call <code className="text-xs">window.DograhWidget.end()</code> to end it.</li>
<li> Subscribe to <code className="text-xs">onCallStart</code>, <code className="text-xs">onCallEnd</code>, <code className="text-xs">onStatusChange</code>, <code className="text-xs">onError</code> to drive your UI.</li>
<li> <code className="text-xs">start()</code> must run inside a user-gesture handler (click) so the browser grants microphone access.</li>
</ul>
</div>
<div className="rounded-lg bg-blue-50 dark:bg-blue-950/20 p-4 border border-blue-200 dark:border-blue-800">
<h4 className="font-medium mb-2 text-blue-900 dark:text-blue-100">Example track status in your own state</h4>
<p className="text-xs text-blue-900/80 dark:text-blue-100/80 mb-2">
Mirror the call status into a variable you control, then render whatever UI you like from it. The status values are <code className="text-xs">idle</code>, <code className="text-xs">connecting</code>, <code className="text-xs">connected</code>, <code className="text-xs">failed</code>.
</p>
<pre className="text-xs overflow-x-auto">
<code className="text-blue-800 dark:text-blue-200">{`// Vanilla JS — keep your own state, render however you want
let callStatus = 'idle';
window.DograhWidget?.onStatusChange((status) => {
callStatus = status;
// ...trigger your render here (re-paint DOM, dispatch event, etc.)
});
document.getElementById('talk-btn').addEventListener('click', () => {
if (callStatus === 'connected' || callStatus === 'connecting') {
window.DograhWidget.end();
} else {
window.DograhWidget.start();
}
});`}</code>
</pre>
<p className="text-xs text-blue-900/80 dark:text-blue-100/80 mt-3 mb-2">React:</p>
<pre className="text-xs overflow-x-auto">
<code className="text-blue-800 dark:text-blue-200">{`function TalkButton() {
const [status, setStatus] = useState('idle');
useEffect(() => {
window.DograhWidget?.onStatusChange(setStatus);
}, []);
const isLive = status === 'connected' || status === 'connecting';
return (
<button onClick={() => isLive ? window.DograhWidget.end() : window.DograhWidget.start()}>
{/* render anything you want from \`status\` */}
</button>
);
}`}</code>
</pre>
</div>
</div>
)}
{/* Inline mode: Integration Instructions */}
{embedMode === "inline" && (
<div className="space-y-3">