diff --git a/docs/deployment/introduction.mdx b/docs/deployment/introduction.mdx index f8a328c..23d11fc 100644 --- a/docs/deployment/introduction.mdx +++ b/docs/deployment/introduction.mdx @@ -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) diff --git a/docs/deployment/web-widget.mdx b/docs/deployment/web-widget.mdx index 5cd7fab..60d770f 100644 --- a/docs/deployment/web-widget.mdx +++ b/docs/deployment/web-widget.mdx @@ -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 `
` 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 + + + +``` + +#### 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 ( + + ); +} +``` + + +`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. + diff --git a/docs/images/copy-deployment-code.png b/docs/images/copy-deployment-code.png index 5178390..5dff882 100644 Binary files a/docs/images/copy-deployment-code.png and b/docs/images/copy-deployment-code.png differ diff --git a/docs/images/save-configurations.png b/docs/images/save-configurations.png index 12feda6..3cc5fe5 100644 Binary files a/docs/images/save-configurations.png and b/docs/images/save-configurations.png differ diff --git a/ui/public/embed/dograh-widget.js b/ui/public/embed/dograh-widget.js index 647a4be..d6b5bc3 100644 --- a/ui/public/embed/dograh-widget.js +++ b/ui/public/embed/dograh-widget.js @@ -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); } diff --git a/ui/src/app/workflow/[workflowId]/components/EmbedDialog.tsx b/ui/src/app/workflow/[workflowId]/components/EmbedDialog.tsx index ace9e02..55e2421 100644 --- a/ui/src/app/workflow/[workflowId]/components/EmbedDialog.tsx +++ b/ui/src/app/workflow/[workflowId]/components/EmbedDialog.tsx @@ -61,7 +61,7 @@ export function EmbedDialog({ const [isEnabled, setIsEnabled] = useState(false); const [domains, setDomains] = useState([]); 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; - 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 */}
-
+
+
@@ -306,26 +322,28 @@ export function EmbedDialog({
- {/* Shared: Button Color */} -
- -
- setButtonColor(e.target.value)} - className="w-14 h-10 cursor-pointer" - /> - setButtonColor(e.target.value)} - placeholder="#10b981" - className="flex-1" - /> + {/* Shared: Button Color (skipped in headless — host renders its own UI) */} + {embedMode !== "headless" && ( +
+ +
+ setButtonColor(e.target.value)} + className="w-14 h-10 cursor-pointer" + /> + setButtonColor(e.target.value)} + placeholder="#10b981" + className="flex-1" + /> +
-
+ )} {/* 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" ? (
)} + {/* Headless mode: Integration Instructions */} + {embedMode === "headless" && ( +
+
+

Integration Instructions

+
    +
  • • Add the embed script tag to your page (see below).
  • +
  • • The widget renders no UI — render your own buttons.
  • +
  • • Call window.DograhWidget.start() to begin a call.
  • +
  • • Call window.DograhWidget.end() to end it.
  • +
  • • Subscribe to onCallStart, onCallEnd, onStatusChange, onError to drive your UI.
  • +
  • start() must run inside a user-gesture handler (click) so the browser grants microphone access.
  • +
+
+ +
+

Example — track status in your own state

+

+ Mirror the call status into a variable you control, then render whatever UI you like from it. The status values are idle, connecting, connected, failed. +

+
+                                                    {`// 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();
+  }
+});`}
+                                                
+

React:

+
+                                                    {`function TalkButton() {
+  const [status, setStatus] = useState('idle');
+
+  useEffect(() => {
+    window.DograhWidget?.onStatusChange(setStatus);
+  }, []);
+
+  const isLive = status === 'connected' || status === 'connecting';
+  return (
+    
+  );
+}`}
+                                                
+
+
+ )} + {/* Inline mode: Integration Instructions */} {embedMode === "inline" && (