feat: add headless widget for deployment

This commit is contained in:
Abhishek Kumar 2026-05-05 18:31:12 +05:30
parent abfb678b4d
commit 036d82bafa
6 changed files with 206 additions and 33 deletions

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,90 @@ 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 circular call 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. |
## Headless mode
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.onStatusChange(cb)` | Fires on every status change. Values: `idle`, `connecting`, `connected`, `failed`. |
| `window.DograhWidget.onCallStart(cb)` | Fires once the call is connected. |
| `window.DograhWidget.onCallEnd(cb)` | Fires when the call ends. |
| `window.DograhWidget.onError(cb)` | Fires on any error (mic permission denied, server error, etc.). |
| `window.DograhWidget.getState()` | Returns the current widget state, including `connectionStatus`. |
### Recommended pattern
Mirror the call status into a state variable that you own, then render whatever UI you like from it.
#### Vanilla JS
```html
<button id="talk-btn">Talk to AI</button>
<script>
let callStatus = 'idle';
window.DograhWidget?.onStatusChange((status) => {
callStatus = status;
document.getElementById('talk-btn').textContent =
status === 'connected' ? 'End Call'
: status === 'connecting' ? 'Connecting…'
: status === 'failed' ? 'Retry'
: 'Talk to AI';
});
document.getElementById('talk-btn').addEventListener('click', () => {
if (callStatus === 'connected' || callStatus === 'connecting') {
window.DograhWidget.end();
} else {
window.DograhWidget.start();
}
});
</script>
```
#### React
```tsx
function TalkButton() {
const [status, setStatus] = useState('idle');
useEffect(() => {
window.DograhWidget?.onStatusChange(setStatus);
}, []);
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>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 126 KiB

After

Width:  |  Height:  |  Size: 64 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 83 KiB

Before After
Before After

View file

@ -125,13 +125,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();
}
@ -298,6 +299,18 @@
state.audioElement = audio;
}
/**
* 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;
}
/**
* Toggle call (start or stop based on current state)
*/
@ -582,6 +595,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);
}

View file

@ -61,7 +61,7 @@ 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 [buttonColor, setButtonColor] = useState("#10b981");
@ -81,7 +81,7 @@ 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");
setButtonColor(settings.buttonColor || "#10b981");
@ -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,28 @@ 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 Color (skipped in headless — host renders its own UI) */}
{embedMode !== "headless" && (
<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>
)}
{/* Floating mode: Position */}
{embedMode === "floating" && (
@ -371,8 +389,8 @@ export function EmbedDialog({
</>
)}
{/* Preview */}
{embedMode === "floating" ? (
{/* Preview (skipped for headless — host renders its own UI) */}
{embedMode === "headless" ? null : 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"
@ -410,6 +428,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">