fix: fix pointer events on phone call dialog (#70)

* fix: fix pointer events on phone call dialog

* feat: add max recording limit

* chore: revert docker compose local changes

* chore: revert langfuse changes

* fix: kill descendents before killing main process
This commit is contained in:
Abhishek 2025-12-01 11:32:28 +05:30 committed by GitHub
parent beb0091c01
commit 713c35df64
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 175 additions and 85 deletions

View file

@ -33,7 +33,8 @@ class AudioConfig:
transport_out_sample_rate: int
vad_sample_rate: int = 16000 # VAD typically resamples internally
pipeline_sample_rate: Optional[int] = None # If None, uses transport rates
buffer_size_seconds: float = 1.0 # This is how frequenly we will call merge_auido
buffer_size_seconds: float = 5.0 # This is how frequenly we will call merge_auido
max_recording_duration_seconds: float = 300.0 # 5 minutes max recording duration
def __post_init__(self):
# Validate VAD sample rate
@ -75,6 +76,12 @@ class AudioConfig:
"""Calculate buffer size in samples based on pipeline sample rate."""
return int(self.pipeline_sample_rate * self.buffer_size_seconds)
@property
def max_recording_bytes(self) -> int:
"""Calculate max recording size in bytes based on pipeline sample rate and duration."""
# 2 bytes per sample (16-bit PCM)
return int(self.pipeline_sample_rate * 2 * self.max_recording_duration_seconds)
def create_audio_config(transport_type: str) -> AudioConfig:
"""Create audio configuration based on transport type.

View file

@ -27,6 +27,7 @@ def create_pipeline_components(audio_config: AudioConfig, engine: "PipecatEngine
audio_buffer = AudioBuffer(
sample_rate=audio_config.pipeline_sample_rate,
buffer_size=audio_config.buffer_size_bytes,
max_recording_bytes=audio_config.max_recording_bytes,
)
# Create synchronizer for merged audio (outside pipeline)

@ -1 +1 @@
Subproject commit 5e95754902332ee4a7ff7ebcee3cca7e70fce825
Subproject commit 30c6d1edb93c144a52adc6a3aa1aa618b1ee85fc

View file

@ -76,6 +76,45 @@ fi
###############################################################################
mkdir -p "$RUN_DIR"
# Function to get all descendant PIDs of a process (children, grandchildren, etc.)
get_descendants() {
local parent_pid=$1
local descendants=""
local children
# Get direct children
children=$(pgrep -P "$parent_pid" 2>/dev/null || true)
for child in $children; do
# Recursively get descendants of each child
descendants="$descendants $child $(get_descendants "$child")"
done
echo "$descendants"
}
# Function to kill a process and all its descendants
kill_process_tree() {
local pid=$1
local signal=$2
local descendants
descendants=$(get_descendants "$pid")
# Kill children first (bottom-up), then parent
for desc_pid in $descendants; do
if kill -0 "$desc_pid" 2>/dev/null; then
kill "$signal" "$desc_pid" 2>/dev/null || true
fi
done
# Kill the parent
if kill -0 "$pid" 2>/dev/null; then
kill "$signal" "$pid" 2>/dev/null || true
fi
}
for name in "${SERVICE_NAMES[@]}"; do
pidfile="$RUN_DIR/$name.pid"
@ -83,15 +122,28 @@ for name in "${SERVICE_NAMES[@]}"; do
oldpid=$(<"$pidfile")
if kill -0 "$oldpid" 2>/dev/null; then
echo "Stopping $name (PID $oldpid and its process group)…"
echo "Stopping $name (PID $oldpid and all descendants)…"
# Kill the entire process group (negative PID)
kill -TERM -"$oldpid" 2>/dev/null || kill -TERM "$oldpid" 2>/dev/null || true
# Kill the entire process tree (parent + all descendants)
kill_process_tree "$oldpid" "-TERM"
sleep 4
# Check if parent or any descendants are still alive
still_alive=false
if kill -0 "$oldpid" 2>/dev/null; then
still_alive=true
else
for desc_pid in $(get_descendants "$oldpid"); do
if kill -0 "$desc_pid" 2>/dev/null; then
still_alive=true
break
fi
done
fi
if $still_alive; then
echo "⚠️ $name did not exit cleanly, forcing stop..."
kill -KILL -"$oldpid" 2>/dev/null || kill -KILL "$oldpid" 2>/dev/null || true
kill_process_tree "$oldpid" "-KILL"
sleep 1
fi
fi

View file

@ -2,6 +2,7 @@
import 'react-international-phone/style.css';
import { Loader2 } from "lucide-react";
import { useRouter } from "next/navigation";
import { useEffect, useState } from "react";
import { PhoneInput } from 'react-international-phone';
@ -44,14 +45,15 @@ export const PhoneCallDialog = ({
const [callError, setCallError] = useState<string | null>(null);
const [callSuccessMsg, setCallSuccessMsg] = useState<string | null>(null);
const [phoneChanged, setPhoneChanged] = useState(false);
const [configureDialogOpen, setConfigureDialogOpen] = useState(false);
const [needsConfiguration, setNeedsConfiguration] = useState(false);
const [checkingConfig, setCheckingConfig] = useState(false);
const [needsConfiguration, setNeedsConfiguration] = useState<boolean | null>(null);
// Check telephony configuration when dialog opens
useEffect(() => {
const checkConfig = async () => {
if (!open) return;
setCheckingConfig(true);
try {
const accessToken = await getAccessToken();
const configResponse = await getTelephonyConfigurationApiV1OrganizationsTelephonyConfigGet({
@ -60,18 +62,19 @@ export const PhoneCallDialog = ({
if (configResponse.error || (!configResponse.data?.twilio && !configResponse.data?.vonage && !configResponse.data?.vobiz)) {
setNeedsConfiguration(true);
setConfigureDialogOpen(true);
onOpenChange(false);
} else {
setNeedsConfiguration(false);
}
} catch (err) {
console.error("Failed to check telephony config:", err);
setNeedsConfiguration(false);
} finally {
setCheckingConfig(false);
}
};
checkConfig();
}, [open, getAccessToken, onOpenChange]);
}, [open, getAccessToken]);
// Reset state when dialog closes
useEffect(() => {
@ -79,6 +82,7 @@ export const PhoneCallDialog = ({
setCallError(null);
setCallSuccessMsg(null);
setCallLoading(false);
setNeedsConfiguration(null);
}
}, [open]);
@ -101,7 +105,7 @@ export const PhoneCallDialog = ({
};
const handleConfigureContinue = () => {
setConfigureDialogOpen(false);
onOpenChange(false);
router.push('/telephony-configurations');
};
@ -146,75 +150,96 @@ export const PhoneCallDialog = ({
}
};
return (
// Render loading state
const renderLoading = () => (
<>
{/* Phone Call Dialog */}
<Dialog open={open && !needsConfiguration} onOpenChange={onOpenChange}>
<DialogContent>
<DialogHeader>
<DialogTitle>Phone Call</DialogTitle>
<DialogDescription>
Enter the phone number to call. The number will be saved automatically.
</DialogDescription>
</DialogHeader>
<PhoneInput
defaultCountry="in"
value={phoneNumber}
onChange={handlePhoneInputChange}
/>
<DialogFooter className="flex-col sm:flex-row gap-2">
<Button
variant="outline"
onClick={() => {
onOpenChange(false);
router.push('/telephony-configurations');
}}
>
Configure Telephony
</Button>
<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={() => onOpenChange(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>
<DialogHeader>
<DialogTitle>Phone Call</DialogTitle>
</DialogHeader>
<div className="flex items-center justify-center py-8">
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
</div>
</>
);
// Render configuration needed state
const renderConfigurationNeeded = () => (
<>
<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={() => onOpenChange(false)}>
Do it Later
</Button>
<Button onClick={handleConfigureContinue}>
Continue
</Button>
</DialogFooter>
</>
);
// Render phone call form
const renderPhoneCallForm = () => (
<>
<DialogHeader>
<DialogTitle>Phone Call</DialogTitle>
<DialogDescription>
Enter the phone number to call. The number will be saved automatically.
</DialogDescription>
</DialogHeader>
<PhoneInput
defaultCountry="in"
value={phoneNumber}
onChange={handlePhoneInputChange}
/>
<DialogFooter className="flex-col sm:flex-row gap-2">
<Button
variant="outline"
onClick={() => {
onOpenChange(false);
router.push('/telephony-configurations');
}}
>
Configure Telephony
</Button>
<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={() => onOpenChange(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>}
</>
);
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent>
{checkingConfig || needsConfiguration === null
? renderLoading()
: needsConfiguration
? renderConfigurationNeeded()
: renderPhoneCallForm()
}
</DialogContent>
</Dialog>
);
};

View file

@ -125,7 +125,12 @@ export const WorkflowEditorHeader = ({
Web Call
</DropdownMenuItem>
<DropdownMenuItem
onClick={onPhoneCallClick}
onClick={() => {
// Delay opening dialog to next event cycle to allow DropdownMenu
// to clean up first, preventing pointer-events: none stuck on body
// See: https://github.com/radix-ui/primitives/issues/1241
setTimeout(onPhoneCallClick, 0);
}}
className="text-white hover:bg-[#2a2a2a] cursor-pointer"
>
<Phone className="w-4 h-4 mr-2" />

View file

@ -121,7 +121,6 @@ export default function WorkflowRunPage() {
<Link href={`/workflow/${params.workflowId}`}>
<Button
ref={customizeButtonRef}
variant="outline"
className="gap-2"
onClick={() => {
if (!hasSeenTooltip('customize_workflow')) {
@ -132,7 +131,7 @@ export default function WorkflowRunPage() {
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
Customize Workflow
Back to Agent
</Button>
</Link>
</CardHeader>

View file

@ -22,6 +22,7 @@ import Link from "next/link";
import { usePathname, useRouter } from "next/navigation";
import React from "react";
import ThemeToggle from "@/components/ThemeSwitcher";
import { Button } from "@/components/ui/button";
import {
Sidebar,
@ -405,7 +406,7 @@ export function AppSidebar() {
)}
{/* Theme Toggle - at the very bottom */}
{/* <div className={cn(
<div className={cn(
"mt-2 pt-2 border-t",
state === "collapsed" ? "flex justify-center" : ""
)}>
@ -431,7 +432,7 @@ export function AppSidebar() {
className="hover:bg-accent hover:text-accent-foreground"
/>
)}
</div> */}
</div>
</div>
</SidebarFooter>
<SidebarRail />