-
ICE gathering state
-
{iceGatheringState}
+export const ConnectionStatus = ({ connectionStatus }: ConnectionStatusProps) => {
+ if (connectionStatus === 'idle') return null;
+
+ if (connectionStatus === 'connecting') {
+ return (
+
+
+ Establishing Connection...
-
-
ICE connection state
-
{iceConnectionState}
+ );
+ }
+
+ if (connectionStatus === 'connected') {
+ return (
+
-
- );
+ );
+ }
+
+ if (connectionStatus === 'failed') {
+ return (
+
+ );
+ }
+
+ return null;
};
diff --git a/ui/src/app/workflow/[workflowId]/run/[runId]/components/index.ts b/ui/src/app/workflow/[workflowId]/run/[runId]/components/index.ts
index c54c923..98f3014 100644
--- a/ui/src/app/workflow/[workflowId]/run/[runId]/components/index.ts
+++ b/ui/src/app/workflow/[workflowId]/run/[runId]/components/index.ts
@@ -2,5 +2,4 @@ export * from './ApiKeyErrorDialog';
export * from './AudioControls';
export * from './ConnectionStatus';
export * from './ContextDisplay';
-export * from './ContextVariablesSection';
export * from './WorkflowConfigErrorDialog'
diff --git a/ui/src/app/workflow/[workflowId]/run/[runId]/hooks/useWebRTC.tsx b/ui/src/app/workflow/[workflowId]/run/[runId]/hooks/useWebRTC.tsx
index 77e3c80..3c21e6a 100644
--- a/ui/src/app/workflow/[workflowId]/run/[runId]/hooks/useWebRTC.tsx
+++ b/ui/src/app/workflow/[workflowId]/run/[runId]/hooks/useWebRTC.tsx
@@ -16,8 +16,7 @@ interface UseWebRTCProps {
}
export const useWebRTC = ({ workflowId, workflowRunId, accessToken, initialContextVariables }: UseWebRTCProps) => {
- const [iceGatheringState, setIceGatheringState] = useState('');
- const [iceConnectionState, setIceConnectionState] = useState('');
+ const [connectionStatus, setConnectionStatus] = useState<'idle' | 'connecting' | 'connected' | 'failed'>('idle');
const [connectionActive, setConnectionActive] = useState(false);
const [isCompleted, setIsCompleted] = useState(false);
const [apiKeyModalOpen, setApiKeyModalOpen] = useState(false);
@@ -25,9 +24,8 @@ export const useWebRTC = ({ workflowId, workflowRunId, accessToken, initialConte
const [workflowConfigModalOpen, setWorkflowConfigModalOpen] = useState(false);
const [workflowConfigError, setWorkflowConfigError] = useState
(null);
const [isStarting, setIsStarting] = useState(false);
- const [initialContext, setInitialContext] = useState>(
- initialContextVariables || {}
- );
+ // Use initial context variables directly, no UI for editing
+ const initialContext = initialContextVariables || {};
const {
audioInputs,
@@ -55,14 +53,16 @@ export const useWebRTC = ({ workflowId, workflowRunId, accessToken, initialConte
pc.addEventListener('icegatheringstatechange', () => {
logger.info(`ICE gathering state changed in createPeerConnection, ${pc.iceGatheringState}`);
- setIceGatheringState(prevState => prevState + ' -> ' + pc.iceGatheringState);
});
- setIceGatheringState(pc.iceGatheringState);
pc.addEventListener('iceconnectionstatechange', () => {
- setIceConnectionState(prevState => prevState + ' -> ' + pc.iceConnectionState);
+ logger.info(`ICE connection state changed: ${pc.iceConnectionState}`);
+ if (pc.iceConnectionState === 'connected' || pc.iceConnectionState === 'completed') {
+ setConnectionStatus('connected');
+ } else if (pc.iceConnectionState === 'failed' || pc.iceConnectionState === 'disconnected') {
+ setConnectionStatus('failed');
+ }
});
- setIceConnectionState(pc.iceConnectionState);
pc.addEventListener('track', (evt) => {
if (evt.track.kind === 'audio' && audioRef.current) {
@@ -142,6 +142,7 @@ export const useWebRTC = ({ workflowId, workflowRunId, accessToken, initialConte
const start = async () => {
if (isStarting || !accessToken) return;
setIsStarting(true);
+ setConnectionStatus('connecting');
try {
const response = await validateUserConfigurationsApiV1UserConfigurationsUserValidateGet({
headers: {
@@ -212,6 +213,7 @@ export const useWebRTC = ({ workflowId, workflowRunId, accessToken, initialConte
} catch (err) {
logger.error(`Could not acquire media: ${err}`);
setPermissionError('Could not acquire media');
+ setConnectionStatus('failed');
}
} else {
await negotiate();
@@ -224,6 +226,7 @@ export const useWebRTC = ({ workflowId, workflowRunId, accessToken, initialConte
const stop = () => {
setConnectionActive(false);
setIsCompleted(true);
+ setConnectionStatus('idle');
const pc = pcRef.current;
if (!pc) return;
@@ -264,12 +267,10 @@ export const useWebRTC = ({ workflowId, workflowRunId, accessToken, initialConte
workflowConfigError,
workflowConfigModalOpen,
setWorkflowConfigModalOpen,
- iceGatheringState,
- iceConnectionState,
+ connectionStatus,
start,
stop,
isStarting,
- initialContext,
- setInitialContext
+ initialContext
};
};
diff --git a/ui/src/app/workflow/[workflowId]/run/[runId]/page.tsx b/ui/src/app/workflow/[workflowId]/run/[runId]/page.tsx
index 06d936c..d495555 100644
--- a/ui/src/app/workflow/[workflowId]/run/[runId]/page.tsx
+++ b/ui/src/app/workflow/[workflowId]/run/[runId]/page.tsx
@@ -5,7 +5,7 @@ import Link from 'next/link';
import { useParams } from 'next/navigation';
import { useEffect, useState } from 'react';
-import Pipecat from '@/app/workflow/[workflowId]/run/[runId]/Pipecat';
+import BrowserCall from '@/app/workflow/[workflowId]/run/[runId]/BrowserCall';
import WorkflowLayout from '@/app/workflow/WorkflowLayout';
import { getWorkflowRunApiV1WorkflowWorkflowIdRunsRunIdGet } from '@/client/sdk.gen';
import { MediaPreviewButtons, MediaPreviewDialog } from '@/components/MediaPreviewDialog';
@@ -15,8 +15,6 @@ import { Skeleton } from '@/components/ui/skeleton';
import { useAuth } from '@/lib/auth';
import { downloadFile } from '@/lib/files';
-import { ContextDisplay } from './components';
-
interface WorkflowRunResponse {
is_completed: boolean;
transcript_url: string | null;
@@ -125,7 +123,7 @@ export default function WorkflowRunPage() {
- Workflow Run Completed
+ Agent Run Completed
- Your workflow run has been completed successfully. You can preview or download the transcript and recording.
+ Your voice agent run has been completed successfully. You can preview or download the transcript and recording.
@@ -171,7 +169,7 @@ export default function WorkflowRunPage() {
-
*/}
);
@@ -188,7 +186,7 @@ export default function WorkflowRunPage() {
else {
returnValue =
diff --git a/ui/src/components/onboarding/OnboardingTooltip.tsx b/ui/src/components/onboarding/OnboardingTooltip.tsx
new file mode 100644
index 0000000..2d709e2
--- /dev/null
+++ b/ui/src/components/onboarding/OnboardingTooltip.tsx
@@ -0,0 +1,139 @@
+'use client';
+
+import { X } from 'lucide-react';
+import { useEffect, useRef, useState } from 'react';
+import { createPortal } from 'react-dom';
+
+interface OnboardingTooltipProps {
+ targetRef: React.RefObject;
+ title?: string;
+ message: string;
+ onDismiss: () => void;
+ onNext?: () => void;
+ showNext?: boolean;
+ isVisible: boolean;
+}
+
+export const OnboardingTooltip = ({
+ targetRef,
+ title = "One more thing...",
+ message,
+ onDismiss,
+ onNext,
+ showNext = true,
+ isVisible
+}: OnboardingTooltipProps) => {
+ const tooltipRef = useRef(null);
+ const [position, setPosition] = useState({ top: 0, left: 0 });
+ const [mounted, setMounted] = useState(false);
+
+ useEffect(() => {
+ setMounted(true);
+ return () => setMounted(false);
+ }, []);
+
+ useEffect(() => {
+ if (!isVisible || !targetRef.current) return;
+
+ const calculatePosition = () => {
+ if (!targetRef.current || !tooltipRef.current) return;
+
+ const targetRect = targetRef.current.getBoundingClientRect();
+ const tooltipRect = tooltipRef.current.getBoundingClientRect();
+
+ // Position tooltip below the target element with some offset for the arrow
+ const top = targetRect.bottom + 8; // 8px gap for arrow
+
+ // Center the tooltip horizontally relative to the target
+ let left = targetRect.left + (targetRect.width / 2) - (tooltipRect.width / 2);
+
+ // Ensure tooltip doesn't go off-screen
+ const padding = 16;
+ if (left < padding) {
+ left = padding;
+ } else if (left + tooltipRect.width > window.innerWidth - padding) {
+ left = window.innerWidth - tooltipRect.width - padding;
+ }
+
+ setPosition({ top, left });
+ };
+
+ // Small delay to ensure tooltip is rendered before calculating position
+ const timer = setTimeout(() => {
+ calculatePosition();
+ }, 10);
+
+ // Recalculate on window resize
+ window.addEventListener('resize', calculatePosition);
+ window.addEventListener('scroll', calculatePosition);
+
+ return () => {
+ clearTimeout(timer);
+ window.removeEventListener('resize', calculatePosition);
+ window.removeEventListener('scroll', calculatePosition);
+ };
+ }, [isVisible, targetRef]);
+
+ if (!mounted || !isVisible) return null;
+
+ const tooltipContent = (
+
+ {/* Arrow pointing up */}
+
+
+ {/* Tooltip content */}
+
+ {/* Close button */}
+
+
+ {/* Title */}
+
{title}
+
+ {/* Message */}
+
+ {message}
+
+
+ {/* Footer actions */}
+
+
+
+ {showNext && (
+
+ )}
+
+
+
+ );
+
+ // Use portal to render tooltip at document root
+ return createPortal(tooltipContent, document.body);
+};
diff --git a/ui/src/context/OnboardingContext.tsx b/ui/src/context/OnboardingContext.tsx
new file mode 100644
index 0000000..d9e469c
--- /dev/null
+++ b/ui/src/context/OnboardingContext.tsx
@@ -0,0 +1,83 @@
+'use client';
+
+import { createContext, useContext, useEffect, useState } from 'react';
+
+export type TooltipKey = 'web_call'; // Add more tooltip keys as needed
+
+interface OnboardingState {
+ seenTooltips: TooltipKey[];
+}
+
+interface OnboardingContextType {
+ hasSeenTooltip: (key: TooltipKey) => boolean;
+ markTooltipSeen: (key: TooltipKey) => void;
+ resetOnboarding: () => void;
+}
+
+const ONBOARDING_STORAGE_KEY = 'dograh_onboarding_state';
+
+const defaultState: OnboardingState = {
+ seenTooltips: [],
+};
+
+const OnboardingContext = createContext(undefined);
+
+export const OnboardingProvider = ({ children }: { children: React.ReactNode }) => {
+ const [onboardingState, setOnboardingState] = useState(defaultState);
+
+ // Load state from localStorage on mount
+ useEffect(() => {
+ const savedState = localStorage.getItem(ONBOARDING_STORAGE_KEY);
+ if (savedState) {
+ try {
+ const parsed = JSON.parse(savedState);
+ setOnboardingState({ ...defaultState, ...parsed });
+ } catch (error) {
+ console.error('Failed to parse onboarding state:', error);
+ }
+ }
+ }, []);
+
+ // Save state to localStorage whenever it changes
+ useEffect(() => {
+ localStorage.setItem(ONBOARDING_STORAGE_KEY, JSON.stringify(onboardingState));
+ }, [onboardingState]);
+
+ const hasSeenTooltip = (key: TooltipKey): boolean => {
+ return onboardingState.seenTooltips.includes(key);
+ };
+
+ const markTooltipSeen = (key: TooltipKey) => {
+ setOnboardingState(prev => ({
+ ...prev,
+ seenTooltips: prev.seenTooltips.includes(key)
+ ? prev.seenTooltips
+ : [...prev.seenTooltips, key]
+ }));
+ };
+
+ const resetOnboarding = () => {
+ setOnboardingState(defaultState);
+ localStorage.removeItem(ONBOARDING_STORAGE_KEY);
+ };
+
+ return (
+
+ {children}
+
+ );
+};
+
+export const useOnboarding = () => {
+ const context = useContext(OnboardingContext);
+ if (!context) {
+ throw new Error('useOnboarding must be used within an OnboardingProvider');
+ }
+ return context;
+};