mirror of
https://github.com/dograh-hq/dograh.git
synced 2026-06-10 08:05:22 +02:00
feat: add headless widget for deployment
This commit is contained in:
parent
abfb678b4d
commit
036d82bafa
6 changed files with 206 additions and 33 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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**.
|
|||
|
||||

|
||||
|
||||
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**.
|
||||
|
||||

|
||||
|
||||
Step 4: Copy the generated embed code and paste it into your web page to test your agent.
|
||||
|
||||

|
||||
|
||||
## 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 |
Binary file not shown.
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 83 KiB |
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue