diff --git a/api/services/pipecat/audio_config.py b/api/services/pipecat/audio_config.py index 3c5aa80..966e8e0 100644 --- a/api/services/pipecat/audio_config.py +++ b/api/services/pipecat/audio_config.py @@ -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. diff --git a/api/services/pipecat/pipeline_builder.py b/api/services/pipecat/pipeline_builder.py index 40d6619..b22cbae 100644 --- a/api/services/pipecat/pipeline_builder.py +++ b/api/services/pipecat/pipeline_builder.py @@ -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) diff --git a/pipecat b/pipecat index 5e95754..30c6d1e 160000 --- a/pipecat +++ b/pipecat @@ -1 +1 @@ -Subproject commit 5e95754902332ee4a7ff7ebcee3cca7e70fce825 +Subproject commit 30c6d1edb93c144a52adc6a3aa1aa618b1ee85fc diff --git a/scripts/start_services.sh b/scripts/start_services.sh index 055fde4..98d7239 100755 --- a/scripts/start_services.sh +++ b/scripts/start_services.sh @@ -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 diff --git a/ui/src/app/workflow/[workflowId]/components/PhoneCallDialog.tsx b/ui/src/app/workflow/[workflowId]/components/PhoneCallDialog.tsx index d1f8f23..114d852 100644 --- a/ui/src/app/workflow/[workflowId]/components/PhoneCallDialog.tsx +++ b/ui/src/app/workflow/[workflowId]/components/PhoneCallDialog.tsx @@ -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(null); const [callSuccessMsg, setCallSuccessMsg] = useState(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(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 */} - - - - Phone Call - - Enter the phone number to call. The number will be saved automatically. - - - - - -
- - - - {!callSuccessMsg ? ( - - ) : ( - - )} -
-
- {callError &&
{callError}
} - {callSuccessMsg &&
{callSuccessMsg}
} -
-
- - {/* Configure Telephony Dialog */} - - - - Configure Telephony - - You need to configure your telephony settings before making phone calls. - You will be redirected to the telephony configuration page. - - - - - - - - + + Phone Call + +
+ +
); + + // Render configuration needed state + const renderConfigurationNeeded = () => ( + <> + + Configure Telephony + + You need to configure your telephony settings before making phone calls. + You will be redirected to the telephony configuration page. + + + + + + + + ); + + // Render phone call form + const renderPhoneCallForm = () => ( + <> + + Phone Call + + Enter the phone number to call. The number will be saved automatically. + + + + + +
+ + + + {!callSuccessMsg ? ( + + ) : ( + + )} +
+
+ {callError &&
{callError}
} + {callSuccessMsg &&
{callSuccessMsg}
} + + ); + + return ( + + + {checkingConfig || needsConfiguration === null + ? renderLoading() + : needsConfiguration + ? renderConfigurationNeeded() + : renderPhoneCallForm() + } + + + ); }; diff --git a/ui/src/app/workflow/[workflowId]/components/WorkflowEditorHeader.tsx b/ui/src/app/workflow/[workflowId]/components/WorkflowEditorHeader.tsx index 03bb488..aec495b 100644 --- a/ui/src/app/workflow/[workflowId]/components/WorkflowEditorHeader.tsx +++ b/ui/src/app/workflow/[workflowId]/components/WorkflowEditorHeader.tsx @@ -125,7 +125,12 @@ export const WorkflowEditorHeader = ({ Web Call { + // 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" > diff --git a/ui/src/app/workflow/[workflowId]/run/[runId]/page.tsx b/ui/src/app/workflow/[workflowId]/run/[runId]/page.tsx index e9e86c3..0832e4e 100644 --- a/ui/src/app/workflow/[workflowId]/run/[runId]/page.tsx +++ b/ui/src/app/workflow/[workflowId]/run/[runId]/page.tsx @@ -121,7 +121,6 @@ export default function WorkflowRunPage() { diff --git a/ui/src/components/layout/AppSidebar.tsx b/ui/src/components/layout/AppSidebar.tsx index 680bd0b..6b9781b 100644 --- a/ui/src/components/layout/AppSidebar.tsx +++ b/ui/src/components/layout/AppSidebar.tsx @@ -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 */} - {/*
@@ -431,7 +432,7 @@ export function AppSidebar() { className="hover:bg-accent hover:text-accent-foreground" /> )} -
*/} +