feat: update floating widget to have a button with text

This commit is contained in:
Sabiha Khan 2026-05-06 09:44:56 +05:30
parent 0e12c41fc7
commit 09608e12d9
3 changed files with 140 additions and 182 deletions

4
ui/package-lock.json generated
View file

@ -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",

View file

@ -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 = `
<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);
// 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 = `
<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);
}
/**
@ -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();
}
/**

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 {
@ -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<string, string>;
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({
<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">
{/* Shared: Button Text + Button Color */}
<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-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"
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>
{/* Floating mode: Position */}
@ -345,52 +357,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,
}}
<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">