diff --git a/api/services/mps_service_key_client.py b/api/services/mps_service_key_client.py index 3bbc3f4..8f49ad5 100644 --- a/api/services/mps_service_key_client.py +++ b/api/services/mps_service_key_client.py @@ -65,12 +65,12 @@ class MPSServiceKeyClient: data = response.json() # Transform the response to match our expected format return { - "id": data.get("id"), - "name": data.get("name"), - "service_key": data.get("service_key"), # Only returned on creation - "key_prefix": data.get("service_key", "")[:8] + "id": data.get("id"), + "name": data.get("name") or name, + "service_key": data.get("service_key"), + "key_prefix": data.get("key_prefix") or (data.get("service_key", "")[:8] if data.get("service_key") - else "", + else ""), "expires_at": data.get("expires_at"), "created_at": data.get("created_at"), "is_active": data.get("is_active", True), diff --git a/api/services/workflow/pipecat_engine.py b/api/services/workflow/pipecat_engine.py index 8a714cc..23db1fa 100644 --- a/api/services/workflow/pipecat_engine.py +++ b/api/services/workflow/pipecat_engine.py @@ -15,7 +15,7 @@ from pipecat.services.openai.llm import OpenAILLMContext from pipecat.transports.base_transport import BaseTransport from pipecat.utils.enums import EndTaskReason -from api.constants import VOICEMAIL_RECORDING_DURATION +from api.constants import DEPLOYMENT_MODE, ENABLE_TRACING, VOICEMAIL_RECORDING_DURATION from api.services.gender.gender_service import GenderService from api.services.workflow.disposition_mapper import ( apply_disposition_mapping, @@ -480,7 +480,9 @@ class PipecatEngine: async def _handle_start_node(self, node: Node) -> None: """Handle start node execution.""" # Handle voicemail detection setup (before any returns) - if node.detect_voicemail: + # Lets check ENABLE_TRACING to make sure we have prompt access from + # langfuse + if node.detect_voicemail and DEPLOYMENT_MODE == 'saas' and ENABLE_TRACING: if not self._audio_buffer: logger.warning( "Voicemail detection enabled but no audio buffer available - skipping detection" diff --git a/docker-compose-local.yaml b/docker-compose-local.yaml new file mode 100644 index 0000000..1145451 --- /dev/null +++ b/docker-compose-local.yaml @@ -0,0 +1,47 @@ +services: + postgres: + image: postgres:17 + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: postgres + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 3s + timeout: 3s + retries: 10 + networks: + - app-network + + redis: + image: redis:7 + ports: + - "6379:6379" + command: > + --requirepass redissecret + volumes: + - redis_data:/data + healthcheck: + test: ["CMD", "redis-cli", "-a", "redissecret", "ping"] + interval: 3s + timeout: 10s + retries: 10 + networks: + - app-network + +volumes: + postgres_data: + redis_data: + +networks: + app-network: + driver: bridge diff --git a/docker-compose.yaml b/docker-compose.yaml index e66c522..2182153 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -83,8 +83,6 @@ services: MINIO_BUCKET: "voice-audio" MINIO_SECURE: "false" - MPS_API_URL: "https://dograh.a.pinggy.link" - # Langfuse ENABLE_TRACING: "false" # LANGFUSE_SECRET_KEY: "" @@ -169,6 +167,9 @@ services: # Server-side URL (SSR, internal Docker network) BACKEND_URL: "http://api:8000" + # Controls some feature flags like voicemail detection + NEXT_PUBLIC_DEPLOYMENT_MODE: "oss" + # Posthog NEXT_PUBLIC_ENABLE_POSTHOG: "true" NEXT_PUBLIC_POSTHOG_KEY: "phc_st6dverimoydkpM5m0a9aeAr8znUYWznEaQa8v80E2D" diff --git a/ui/src/app/after-sign-in/page.tsx b/ui/src/app/after-sign-in/page.tsx index 10ce7ea..eabad50 100644 --- a/ui/src/app/after-sign-in/page.tsx +++ b/ui/src/app/after-sign-in/page.tsx @@ -1,20 +1,32 @@ import { redirect } from "next/navigation"; import { getServerAuthProvider, getServerUser } from "@/lib/auth/server"; +import logger from '@/lib/logger'; import { getRedirectUrl } from "@/lib/utils"; +export const dynamic = 'force-dynamic'; + export default async function AfterSignInPage() { + logger.debug('[AfterSignInPage] Starting after-sign-in page'); const authProvider = getServerAuthProvider(); + logger.debug('[AfterSignInPage] Auth provider:', authProvider); + logger.debug('[AfterSignInPage] Getting server user...'); const user = await getServerUser(); + logger.debug('[AfterSignInPage] Got user:', { hasUser: !!user, userId: user?.id }); if (authProvider === 'stack' && user && 'getAuthJson' in user) { + logger.debug('[AfterSignInPage] Stack user detected, getting auth token...'); const token = await user.getAuthJson(); + logger.debug('[AfterSignInPage] Got token:', { hasToken: !!token?.accessToken }); const permissions = 'listPermissions' in user && 'selectedTeam' in user ? await user.listPermissions(user.selectedTeam!) ?? [] : []; + logger.debug('[AfterSignInPage] Got permissions:', { count: permissions.length }); const redirectUrl = await getRedirectUrl(token?.accessToken ?? "", permissions); + logger.debug('[AfterSignInPage] Redirecting to:', redirectUrl); redirect(redirectUrl); } // For local provider or if user is not available, redirect to create-workflow + logger.debug('[AfterSignInPage] Fallback redirect to /create-workflow'); redirect('/create-workflow'); } diff --git a/ui/src/app/api-keys/page.tsx b/ui/src/app/api-keys/page.tsx index 9991d12..80d4d17 100644 --- a/ui/src/app/api-keys/page.tsx +++ b/ui/src/app/api-keys/page.tsx @@ -21,10 +21,18 @@ import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Skeleton } from '@/components/ui/skeleton'; import { useAuth } from '@/lib/auth'; +import logger from '@/lib/logger'; export default function APIKeysPage() { const { user, getAccessToken, redirectToLogin, loading } = useAuth(); + logger.debug('[APIKeysPage] Component render', { + loading, + hasUser: !!user, + userId: user?.id, + timestamp: new Date().toISOString() + }); + const [apiKeys, setApiKeys] = useState([]); const [serviceKeys, setServiceKeys] = useState([]); const [isLoading, setIsLoading] = useState(true); @@ -49,12 +57,24 @@ export default function APIKeysPage() { }, [loading, user, redirectToLogin]); const fetchApiKeys = useCallback(async () => { - if (!user) return; + logger.debug('[APIKeysPage] fetchApiKeys called', { + loading, + hasUser: !!user, + userId: user?.id + }); + + // Follow the pattern from UserConfigContext - check both loading and user + if (loading || !user) { + logger.debug('[APIKeysPage] fetchApiKeys - skipping due to loading or no user'); + return; + } try { setIsLoading(true); setError(null); + logger.debug('[APIKeysPage] fetchApiKeys - calling getAccessToken...'); const accessToken = await getAccessToken(); + logger.debug('[APIKeysPage] fetchApiKeys - got access token'); const response = await getApiKeysApiV1UserApiKeysGet({ query: { @@ -76,15 +96,27 @@ export default function APIKeysPage() { } finally { setIsLoading(false); } - }, [user, getAccessToken, showArchived]); + }, [loading, user, getAccessToken, showArchived]); const fetchServiceKeys = useCallback(async () => { - if (!user) return; + logger.debug('[APIKeysPage] fetchServiceKeys called', { + loading, + hasUser: !!user, + userId: user?.id + }); + + // Follow the pattern from UserConfigContext - check both loading and user + if (loading || !user) { + logger.debug('[APIKeysPage] fetchServiceKeys - skipping due to loading or no user'); + return; + } try { setIsServiceKeysLoading(true); setError(null); + logger.debug('[APIKeysPage] fetchServiceKeys - calling getAccessToken...'); const accessToken = await getAccessToken(); + logger.debug('[APIKeysPage] fetchServiceKeys - got access token'); const response = await getServiceKeysApiV1UserServiceKeysGet({ query: { @@ -104,13 +136,15 @@ export default function APIKeysPage() { } finally { setIsServiceKeysLoading(false); } - }, [user, getAccessToken, showServiceArchived]); + }, [loading, user, getAccessToken, showServiceArchived]); useEffect(() => { + logger.debug('[APIKeysPage] useEffect for fetchApiKeys triggered'); fetchApiKeys(); }, [fetchApiKeys]); useEffect(() => { + logger.debug('[APIKeysPage] useEffect for fetchServiceKeys triggered'); fetchServiceKeys(); }, [fetchServiceKeys]); diff --git a/ui/src/app/create-workflow/page.tsx b/ui/src/app/create-workflow/page.tsx index ed5b53e..e043459 100644 --- a/ui/src/app/create-workflow/page.tsx +++ b/ui/src/app/create-workflow/page.tsx @@ -6,6 +6,14 @@ import { useState } from 'react'; import { createWorkflowFromTemplateApiV1WorkflowCreateTemplatePost } from '@/client/sdk.gen'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardDescription, CardHeader } from '@/components/ui/card'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; import { Input } from '@/components/ui/input'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { useAuth } from '@/lib/auth'; @@ -16,6 +24,8 @@ export default function CreateWorkflowPage() { const { user, getAccessToken } = useAuth(); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); + const [showSuccessModal, setShowSuccessModal] = useState(false); + const [workflowId, setWorkflowId] = useState(null); const [callType, setCallType] = useState<'INBOUND' | 'OUTBOUND'>('INBOUND'); const [useCase, setUseCase] = useState(''); @@ -51,7 +61,8 @@ export default function CreateWorkflowPage() { }); if (response.data?.id) { - router.push(`/workflow/${response.data.id}`); + setWorkflowId(String(response.data.id)); + setShowSuccessModal(true); } } catch (err) { setError('Failed to create workflow. Please try again.'); @@ -61,6 +72,12 @@ export default function CreateWorkflowPage() { } }; + const handleModalContinue = () => { + if (workflowId) { + router.push(`/workflow/${workflowId}`); + } + }; + return (
@@ -165,6 +182,79 @@ export default function CreateWorkflowPage() {
+ + {/* Loading Overlay */} + {isLoading && ( +
+ +
+ {/* Animated spinner */} +
+
+
+
+
+ +
+

+ Creating Your Workflow +

+

+ We're setting up your voice agent with your specifications. This will just take a moment... +

+
+ + {/* Animated dots */} +
+
+
+
+
+
+
+
+ )} + + {/* Success Modal */} + + + + + + + + Workflow Created Successfully! + + +
+

+ A starter template has been generated for your use case, with some randomised data and sample actions. +

+

+ The voice bot is pre-set to communicate in English with an American accent. +

+

+ You're encouraged to first test the bot and then modify it to your specific requirements and location (currency/accent etc). +

+

+ Feel free to join our Slack channel for any queries and star us on GitHub. +

+
+
+
+ + + +
+
); } diff --git a/ui/src/app/globals.css b/ui/src/app/globals.css index cbce9b1..679436e 100644 --- a/ui/src/app/globals.css +++ b/ui/src/app/globals.css @@ -123,3 +123,16 @@ } } +@keyframes spin-slow { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +.animate-spin-slow { + animation: spin-slow 3s linear infinite; +} + diff --git a/ui/src/app/integrations/page.tsx b/ui/src/app/integrations/page.tsx index 2f35244..349bb1b 100644 --- a/ui/src/app/integrations/page.tsx +++ b/ui/src/app/integrations/page.tsx @@ -6,6 +6,8 @@ import logger from '@/lib/logger'; import CreateIntegrationButton from "./CreateIntegrationButton"; +export const dynamic = 'force-dynamic'; + // Server component for integration list async function IntegrationList() { const authProvider = getServerAuthProvider(); diff --git a/ui/src/app/looptalk/page.tsx b/ui/src/app/looptalk/page.tsx index d4bc3aa..b5d143b 100644 --- a/ui/src/app/looptalk/page.tsx +++ b/ui/src/app/looptalk/page.tsx @@ -6,6 +6,8 @@ import { getServerAuthProvider, isServerAuthenticated } from '@/lib/auth/server' import LoopTalkLayout from "./LoopTalkLayout"; +export const dynamic = 'force-dynamic'; + async function PageContent() { const authProvider = getServerAuthProvider(); const isAuthenticated = await isServerAuthenticated(); diff --git a/ui/src/app/page.tsx b/ui/src/app/page.tsx index bd59e22..c7338f9 100644 --- a/ui/src/app/page.tsx +++ b/ui/src/app/page.tsx @@ -5,29 +5,42 @@ import { getServerAuthProvider,getServerUser } from "@/lib/auth/server"; import logger from '@/lib/logger'; import { getRedirectUrl } from "@/lib/utils"; +export const dynamic = 'force-dynamic'; + export default async function Home() { + logger.debug('[HomePage] Starting Home page render'); const authProvider = getServerAuthProvider(); + logger.debug('[HomePage] Auth provider:', authProvider); // For local/OSS provider, always redirect to workflow page if (authProvider === 'local') { - logger.debug('Redirecting to workflow page for local provider'); + logger.debug('[HomePage] Redirecting to workflow page for local provider'); redirect('/create-workflow'); } + logger.debug('[HomePage] Getting server user...'); const user = await getServerUser(); - logger.debug(`authProvider: ${authProvider}, user: ${JSON.stringify(user)}`); + logger.debug('[HomePage] Server user result:', { + hasUser: !!user, + userId: user?.id, + authProvider + }); if (user) { try { // For Stack provider, get the token and permissions if (authProvider === 'stack' && 'getAuthJson' in user) { + logger.debug('[HomePage] Getting auth token from Stack user...'); const token = await user.getAuthJson(); + logger.debug('[HomePage] Got auth token:', { hasToken: !!token?.accessToken }); const permissions = 'listPermissions' in user && 'selectedTeam' in user ? await user.listPermissions(user.selectedTeam!) ?? [] : []; + logger.debug('[HomePage] Got permissions:', { count: permissions.length }); + logger.debug('[HomePage] Getting redirect URL...'); const redirectUrl = await getRedirectUrl(token?.accessToken ?? "", permissions); - logger.debug(`redirectUrl: ${redirectUrl}`); + logger.debug('[HomePage] Redirecting to:', redirectUrl); redirect(redirectUrl); } } catch (error) { diff --git a/ui/src/app/workflow/[workflowId]/components/WorkflowHeader.tsx b/ui/src/app/workflow/[workflowId]/components/WorkflowHeader.tsx index 7320e63..b766f5e 100644 --- a/ui/src/app/workflow/[workflowId]/components/WorkflowHeader.tsx +++ b/ui/src/app/workflow/[workflowId]/components/WorkflowHeader.tsx @@ -67,6 +67,7 @@ const WorkflowHeader = ({ isDirty, workflowName, rfInstance, onRun, workflowId, const { user, getAccessToken } = useAuth(); const hasValidationErrors = workflowValidationErrors.length > 0; + const isOSSDeployment = process.env.NEXT_PUBLIC_DEPLOYMENT_MODE === 'oss'; // Reset call-related state whenever the dialog is closed so that a new call can be placed useEffect(() => { @@ -195,15 +196,17 @@ const WorkflowHeader = ({ isDirty, workflowName, rfInstance, onRun, workflowId, Web Call - + {!isOSSDeployment && ( + + )} {isDirty ? (