feat: Enable telephony for OSS (#21)

* fix: fix tooltip bug

* feat: add Twilio with CloudFlare configuration

* chore: update Tella Video
This commit is contained in:
Abhishek 2025-10-04 12:22:50 +05:30 committed by GitHub
parent d39a8111a6
commit 8e2e5c9327
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 891 additions and 191 deletions

View file

@ -2,10 +2,11 @@ import 'react-international-phone/style.css';
import { ReactFlowInstance, ReactFlowJsonObject } from "@xyflow/react";
import { AlertTriangle, CheckCheck, Download, LoaderCircle, Phone, ShieldCheck } from "lucide-react";
import { useRouter } from "next/navigation";
import { useEffect, useRef, useState } from "react";
import { PhoneInput } from 'react-international-phone';
import { initiateCallApiV1TwilioInitiateCallPost } from '@/client/sdk.gen';
import { getTelephonyConfigurationApiV1OrganizationsTelephonyConfigGet, initiateCallApiV1TwilioInitiateCallPost } from '@/client/sdk.gen';
import { WorkflowError } from '@/client/types.gen';
import { FlowEdge, FlowNode } from "@/components/flow/types";
import { OnboardingTooltip } from '@/components/onboarding/OnboardingTooltip';
@ -16,7 +17,6 @@ import { WORKFLOW_RUN_MODES } from '@/constants/workflowRunModes';
import { useOnboarding } from '@/context/OnboardingContext';
import { useUserConfig } from "@/context/UserConfigContext";
import { useAuth } from '@/lib/auth';
import logger from '@/lib/logger';
interface WorkflowHeaderProps {
isDirty: boolean;
@ -58,24 +58,22 @@ const handleExport = (workflow_name: string, workflow_definition: ReactFlowJsonO
};
const WorkflowHeader = ({ isDirty, workflowName, rfInstance, onRun, workflowId, workflowValidationErrors, saveWorkflow }: WorkflowHeaderProps) => {
const router = useRouter();
const { userConfig, saveUserConfig } = useUserConfig();
const { hasSeenTooltip, markTooltipSeen } = useOnboarding();
const [dialogOpen, setDialogOpen] = useState(false);
const [phoneNumber, setPhoneNumber] = useState(userConfig?.test_phone_number || "");
const [saving, setSaving] = useState(false);
const [savingWorkflow, setSavingWorkflow] = useState(false);
const [callLoading, setCallLoading] = useState(false);
const [callError, setCallError] = useState<string | null>(null);
const [callSuccessMsg, setCallSuccessMsg] = useState<string | null>(null);
const [phoneChanged, setPhoneChanged] = useState(false);
const [validationDialogOpen, setValidationDialogOpen] = useState(false);
const [configureDialogOpen, setConfigureDialogOpen] = useState(false);
const { user, getAccessToken } = useAuth();
const webCallButtonRef = useRef<HTMLButtonElement>(null);
const hasValidationErrors = workflowValidationErrors.length > 0;
const isOSSDeployment = process.env.NEXT_PUBLIC_DEPLOYMENT_MODE === 'oss';
logger.info(`isOSSDeployment: ${isOSSDeployment}`);
// Reset call-related state whenever the dialog is closed so that a new call can be placed
useEffect(() => {
@ -95,7 +93,6 @@ const WorkflowHeader = ({ isDirty, workflowName, rfInstance, onRun, workflowId,
setCallError(null);
setCallSuccessMsg(null);
setCallLoading(false);
setSaving(false);
}
};
@ -111,31 +108,54 @@ const WorkflowHeader = ({ isDirty, workflowName, rfInstance, onRun, workflowId,
setCallSuccessMsg(null);
};
const handleSavePhone = async () => {
if (!userConfig) return;
setSaving(true);
const handlePhoneCallClick = async () => {
// Check telephony configuration before opening dialog
try {
await saveUserConfig({ ...userConfig, test_phone_number: phoneNumber });
setPhoneChanged(false);
const accessToken = await getAccessToken();
const configResponse = await getTelephonyConfigurationApiV1OrganizationsTelephonyConfigGet({
headers: { 'Authorization': `Bearer ${accessToken}` },
});
// If no configuration exists, show configure dialog
if (configResponse.error || !configResponse.data?.twilio) {
setConfigureDialogOpen(true);
return;
}
// Configuration exists, open the phone call dialog
setDialogOpen(true);
} catch (err: unknown) {
setCallError(err instanceof Error ? err.message : "Failed to save phone number");
} finally {
setSaving(false);
console.error("Failed to check telephony config:", err);
// Still open dialog to show the error
setDialogOpen(true);
}
};
const handleConfigureContinue = () => {
setConfigureDialogOpen(false);
router.push(`/configure-telephony?returnTo=/workflow/${workflowId}`);
};
const handleStartCall = async () => {
setCallLoading(true);
setCallError(null);
setCallSuccessMsg(null);
try {
if (!user) return;
if (!user || !userConfig) return;
const accessToken = await getAccessToken();
// Save phone number if it has changed
if (phoneChanged) {
await saveUserConfig({ ...userConfig, test_phone_number: phoneNumber });
setPhoneChanged(false);
}
// Configuration exists, proceed with call initiation
const response = await initiateCallApiV1TwilioInitiateCallPost({
body: { workflow_id: workflowId },
headers: { 'Authorization': `Bearer ${accessToken}` },
});
if (response.error) {
let errMsg = "Failed to initiate call";
if (typeof response.error === "string") {
@ -211,17 +231,15 @@ const WorkflowHeader = ({ isDirty, workflowName, rfInstance, onRun, workflowId,
<Phone className="mr-2 h-4 w-4" />
Web Call
</Button>
{!isOSSDeployment && (
<Button
variant="outline"
size="sm"
onClick={() => setDialogOpen(true)}
disabled={hasValidationErrors}
>
<Phone className="mr-2 h-4 w-4" />
Phone Call
</Button>
)}
<Button
variant="outline"
size="sm"
onClick={handlePhoneCallClick}
disabled={hasValidationErrors}
>
<Phone className="mr-2 h-4 w-4" />
Phone Call
</Button>
{isDirty ? (
<Button
@ -293,7 +311,7 @@ const WorkflowHeader = ({ isDirty, workflowName, rfInstance, onRun, workflowId,
<DialogHeader>
<DialogTitle>Phone Call</DialogTitle>
<DialogDescription>
Enter the phone number to call. This will be saved to your user config.
Enter the phone number to call. The number will be saved automatically.
</DialogDescription>
</DialogHeader>
<PhoneInput
@ -301,43 +319,65 @@ const WorkflowHeader = ({ isDirty, workflowName, rfInstance, onRun, workflowId,
value={phoneNumber}
onChange={handlePhoneInputChange}
/>
{phoneChanged && (
<DialogFooter className="flex-col sm:flex-row gap-2">
<Button
variant="outline"
size="sm"
onClick={handleSavePhone}
disabled={saving}
onClick={() => {
setDialogOpen(false);
router.push(`/configure-telephony?returnTo=/workflow/${workflowId}`);
}}
>
{saving ? "Saving..." : "Save Number"}
Configure Telephony
</Button>
)}
<DialogFooter>
{!callSuccessMsg ? (
<Button
onClick={handleStartCall}
disabled={callLoading || phoneChanged || !phoneNumber || saving}
>
{callLoading ? "Calling..." : "Start Call"}
</Button>
) : (
<Button onClick={() => setDialogOpen(false)}>
Close
</Button>
)}
<DialogClose asChild>
<Button variant="ghost">Cancel</Button>
</DialogClose>
<div className="flex gap-2 flex-1 justify-end">
<DialogClose asChild>
<Button variant="outline">Cancel</Button>
</DialogClose>
{!callSuccessMsg ? (
<Button
onClick={handleStartCall}
disabled={callLoading || !phoneNumber}
>
{callLoading ? "Calling..." : "Start Call"}
</Button>
) : (
<Button onClick={() => setDialogOpen(false)}>
Close
</Button>
)}
</div>
</DialogFooter>
{callError && <div className="text-red-500 text-sm mt-2">{callError}</div>}
{callSuccessMsg && <div className="text-green-600 text-sm mt-2">{callSuccessMsg}</div>}
</DialogContent>
</Dialog>
{/* Configure Telephony Dialog */}
<Dialog open={configureDialogOpen} onOpenChange={setConfigureDialogOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>Configure Telephony</DialogTitle>
<DialogDescription>
You need to configure your telephony settings before making phone calls.
You will be redirected to the telephony configuration page.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="ghost" onClick={() => setConfigureDialogOpen(false)}>
Do it Later
</Button>
<Button onClick={handleConfigureContinue}>
Continue
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Onboarding Tooltip */}
<OnboardingTooltip
title='Test your Voice Agent'
targetRef={webCallButtonRef}
message="Test this workflow now in your browser (no phone required)"
message="Test this workflow now in your browser using Web Call"
onDismiss={() => markTooltipSeen('web_call')}
showNext={false}
isVisible={!hasSeenTooltip('web_call') && !hasValidationErrors}