From 09608e12d9bfe2629063f8bdb2a9933d9b07ae06 Mon Sep 17 00:00:00 2001 From: Sabiha Khan Date: Wed, 6 May 2026 09:44:56 +0530 Subject: [PATCH] feat: update floating widget to have a button with text --- ui/package-lock.json | 4 +- ui/public/embed/dograh-widget.js | 211 ++++++++---------- .../[workflowId]/components/EmbedDialog.tsx | 107 ++++----- 3 files changed, 140 insertions(+), 182 deletions(-) diff --git a/ui/package-lock.json b/ui/package-lock.json index 982ae57..2edb193 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -1,12 +1,12 @@ { "name": "ui", - "version": "1.26.0", + "version": "1.27.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ui", - "version": "1.26.0", + "version": "1.27.0", "dependencies": { "@dagrejs/dagre": "^1.1.4", "@nangohq/frontend": "^0.69.47", diff --git a/ui/public/embed/dograh-widget.js b/ui/public/embed/dograh-widget.js index 647a4be..36fb119 100644 --- a/ui/public/embed/dograh-widget.js +++ b/ui/public/embed/dograh-widget.js @@ -114,7 +114,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 }; @@ -172,88 +172,48 @@ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; } - .dograh-widget-container.bottom-right { - bottom: 20px; - right: 20px; - } + .dograh-widget-container.bottom-right { bottom: 20px; right: 20px; } + .dograh-widget-container.bottom-left { bottom: 20px; left: 20px; } + .dograh-widget-container.top-right { top: 20px; right: 20px; } + .dograh-widget-container.top-left { top: 20px; left: 20px; } - .dograh-widget-container.bottom-left { - bottom: 20px; - left: 20px; - } - - .dograh-widget-container.top-right { - top: 20px; - right: 20px; - } - - .dograh-widget-container.top-left { - top: 20px; - 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,40 +222,70 @@ 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 = ` - - - - `; - 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); - - // Store audio element reference state.audioElement = audio; + + 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 = ` + + + + + + + + `; + button.querySelector('span').textContent = ctaLabelForStatus(status); + button.onclick = toggleCall; + + container.appendChild(button); } /** @@ -309,30 +299,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(); } /** diff --git a/ui/src/app/workflow/[workflowId]/components/EmbedDialog.tsx b/ui/src/app/workflow/[workflowId]/components/EmbedDialog.tsx index ace9e02..53d2df3 100644 --- a/ui/src/app/workflow/[workflowId]/components/EmbedDialog.tsx +++ b/ui/src/app/workflow/[workflowId]/components/EmbedDialog.tsx @@ -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 { @@ -63,7 +63,7 @@ export function EmbedDialog({ const [newDomain, setNewDomain] = useState(""); const [embedMode, setEmbedMode] = useState<"floating" | "inline">("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"); @@ -83,7 +83,7 @@ export function EmbedDialog({ const settings = response.data.settings as Record; setEmbedMode((settings.embedMode as "floating" | "inline") || "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"); } @@ -306,25 +306,37 @@ export function EmbedDialog({
- {/* Shared: Button Color */} -
- -
+ {/* Shared: Button Text + Button Color */} +
+
+ setButtonColor(e.target.value)} - className="w-14 h-10 cursor-pointer" - /> - setButtonColor(e.target.value)} - placeholder="#10b981" - className="flex-1" + id="button-text" + value={buttonText} + onChange={(e) => setButtonText(e.target.value)} + placeholder="Talk to Agent" + maxLength={40} />
+
+ +
+ setButtonColor(e.target.value)} + className="w-14 h-10 cursor-pointer" + /> + setButtonColor(e.target.value)} + placeholder="#10b981" + className="flex-1" + /> +
+
{/* Floating mode: Position */} @@ -345,52 +357,29 @@ export function EmbedDialog({
)} - {/* Inline mode: Button Text, CTA Text */} + {/* Inline mode: Call to Action Text */} {embedMode === "inline" && ( - <> -
-
- - setButtonText(e.target.value)} - placeholder="Start Call" - /> -
-
- - setCallToActionText(e.target.value)} - placeholder="Click to start voice conversation" - /> -
-
- +
+ + setCallToActionText(e.target.value)} + placeholder="Click to start voice conversation" + /> +
)} {/* Preview */} {embedMode === "floating" ? ( -
-
+
+ + {buttonText || "Talk to Agent"} +
) : (