diff --git a/ui/src/app/api-keys/page.tsx b/ui/src/app/api-keys/page.tsx index 80d4d17..57cf27a 100644 --- a/ui/src/app/api-keys/page.tsx +++ b/ui/src/app/api-keys/page.tsx @@ -22,6 +22,7 @@ import { Label } from '@/components/ui/label'; import { Skeleton } from '@/components/ui/skeleton'; import { useAuth } from '@/lib/auth'; import logger from '@/lib/logger'; +import { isOSSMode } from '@/lib/utils'; export default function APIKeysPage() { const { user, getAccessToken, redirectToLogin, loading } = useAuth(); @@ -310,6 +311,11 @@ export default function APIKeysPage() { ); } + // In OSS mode, check if there's already an active service key + const activeServiceKeys = serviceKeys.filter(key => !key.archived_at); + const canCreateServiceKey = !isOSSMode() || activeServiceKeys.length === 0; + const showServiceKeyArchiveControls = !isOSSMode(); + return (
@@ -444,21 +450,25 @@ export default function APIKeysPage() {
- - + {showServiceKeyArchiveControls && ( + + )} + {canCreateServiceKey && ( + + )}
@@ -479,9 +489,11 @@ export default function APIKeysPage() {

No service keys found

- + {canCreateServiceKey && ( + + )}
) : (
@@ -520,7 +532,7 @@ export default function APIKeysPage() {
- {!key.archived_at && ( + {!key.archived_at && showServiceKeyArchiveControls && (
- - {!currentUsage.quota_enabled && ( -
-

- Quota enforcement is not enabled for your organization. -

-
- )} ) : (

Unable to load usage data

diff --git a/ui/src/app/workflow/[workflowId]/run/[runId]/BrowserCall.tsx b/ui/src/app/workflow/[workflowId]/run/[runId]/BrowserCall.tsx index e4aa47d..f753172 100644 --- a/ui/src/app/workflow/[workflowId]/run/[runId]/BrowserCall.tsx +++ b/ui/src/app/workflow/[workflowId]/run/[runId]/BrowserCall.tsx @@ -39,7 +39,8 @@ const BrowserCall = ({ workflowId, workflowRunId, accessToken, initialContextVar connectionStatus, start, stop, - isStarting + isStarting, + getAudioInputDevices } = useWebSocketRTC({ workflowId, workflowRunId, accessToken, initialContextVariables }); // Poll for recording availability after call ends @@ -118,6 +119,7 @@ const BrowserCall = ({ workflowId, workflowRunId, accessToken, initialContextVar start={start} stop={stop} isStarting={isStarting} + getAudioInputDevices={getAudioInputDevices} /> Promise; stop: () => void; isStarting: boolean; + getAudioInputDevices: () => Promise; } export const AudioControls = ({ @@ -23,28 +25,35 @@ export const AudioControls = ({ permissionError, start, stop, - isStarting + isStarting, + getAudioInputDevices }: AudioControlsProps) => { // Check if we have valid audio devices (permissions granted) - const hasValidDevices = audioInputs.length > 0 && audioInputs.some(device => device.deviceId && device.deviceId.trim() !== ''); + // Browsers only provide device labels after permission is granted + const hasValidDevices = audioInputs.length > 0 && audioInputs.some(device => device.label && device.label.trim() !== ''); const requestAudioPermissions = async () => { try { - await navigator.mediaDevices.getUserMedia({ audio: true }); - // This will trigger the parent component to refresh the device list - window.location.reload(); + // Request audio permissions - this triggers the browser permission prompt + const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); + // Stop the stream immediately - we just needed to trigger the permission prompt + stream.getTracks().forEach(track => track.stop()); + // Refresh the device list now that we have permissions + await getAudioInputDevices(); } catch (error) { console.error('Failed to request audio permissions:', error); } }; // Handle auto-selection of first device if none selected - if (hasValidDevices && !selectedAudioInput) { - const firstValidDevice = audioInputs.find(device => device.deviceId && device.deviceId.trim() !== ''); - if (firstValidDevice) { - setSelectedAudioInput(firstValidDevice.deviceId); + useEffect(() => { + if (hasValidDevices && !selectedAudioInput) { + const firstValidDevice = audioInputs.find(device => device.label && device.label.trim() !== ''); + if (firstValidDevice) { + setSelectedAudioInput(firstValidDevice.deviceId); + } } - } + }, [hasValidDevices, selectedAudioInput, audioInputs, setSelectedAudioInput]); if (isCompleted) { return null; // The parent component will handle showing the loading state diff --git a/ui/src/app/workflow/[workflowId]/run/[runId]/hooks/useDeviceInputs.tsx b/ui/src/app/workflow/[workflowId]/run/[runId]/hooks/useDeviceInputs.tsx index a082a7b..c3dfd4e 100644 --- a/ui/src/app/workflow/[workflowId]/run/[runId]/hooks/useDeviceInputs.tsx +++ b/ui/src/app/workflow/[workflowId]/run/[runId]/hooks/useDeviceInputs.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from "react"; +import { useCallback, useEffect, useState } from "react"; import logger from '@/lib/logger'; @@ -7,30 +7,32 @@ export const useDeviceInputs = () => { const [selectedAudioInput, setSelectedAudioInput] = useState(''); const [permissionError, setPermissionError] = useState(null); - useEffect(() => { - const getAudioInputs = async () => { - try { - const devices = await navigator.mediaDevices.enumerateDevices(); - const audioDevices = devices.filter(device => device.kind === 'audioinput'); - setAudioInputs(audioDevices); + const getAudioInputDevices = useCallback(async () => { + try { + const devices = await navigator.mediaDevices.enumerateDevices(); + const audioDevices = devices.filter(device => device.kind === 'audioinput'); + setAudioInputs(audioDevices); - const defaultAudioInput = audioDevices.find(device => device.deviceId === 'default'); - if (defaultAudioInput) { - setSelectedAudioInput(defaultAudioInput.deviceId); - } - } catch (error) { - setPermissionError('Could not enumerate devices'); - logger.error(`Error enumerating devices: ${error}`); + const defaultAudioInput = audioDevices.find(device => device.deviceId === 'default'); + if (defaultAudioInput) { + setSelectedAudioInput(defaultAudioInput.deviceId); } - }; - getAudioInputs(); + } catch (error) { + setPermissionError('Could not enumerate devices'); + logger.error(`Error enumerating devices: ${error}`); + } }, []); + useEffect(() => { + getAudioInputDevices(); + }, [getAudioInputDevices]); + return { audioInputs, selectedAudioInput, setSelectedAudioInput, permissionError, - setPermissionError + setPermissionError, + getAudioInputDevices }; }; diff --git a/ui/src/app/workflow/[workflowId]/run/[runId]/hooks/useWebSocketRTC.tsx b/ui/src/app/workflow/[workflowId]/run/[runId]/hooks/useWebSocketRTC.tsx index 64947ed..27414b6 100644 --- a/ui/src/app/workflow/[workflowId]/run/[runId]/hooks/useWebSocketRTC.tsx +++ b/ui/src/app/workflow/[workflowId]/run/[runId]/hooks/useWebSocketRTC.tsx @@ -31,7 +31,8 @@ export const useWebSocketRTC = ({ workflowId, workflowRunId, accessToken, initia selectedAudioInput, setSelectedAudioInput, permissionError, - setPermissionError + setPermissionError, + getAudioInputDevices } = useDeviceInputs(); const useStun = true; @@ -435,6 +436,7 @@ export const useWebSocketRTC = ({ workflowId, workflowRunId, accessToken, initia start, stop, isStarting, - initialContext + initialContext, + getAudioInputDevices }; }; diff --git a/ui/src/components/flow/nodes/StartCall.tsx b/ui/src/components/flow/nodes/StartCall.tsx index 7053fed..b09d207 100644 --- a/ui/src/components/flow/nodes/StartCall.tsx +++ b/ui/src/components/flow/nodes/StartCall.tsx @@ -9,6 +9,7 @@ import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Switch } from "@/components/ui/switch"; import { Textarea } from "@/components/ui/textarea"; +import { isOSSMode } from "@/lib/utils"; import { NodeContent } from "./common/NodeContent"; import { NodeEditDialog } from "./common/NodeEditDialog"; @@ -170,8 +171,6 @@ const StartCallEditForm = ({ delayedStartDuration, setDelayedStartDuration }: StartCallEditFormProps) => { - const isOSS = process.env.NEXT_PUBLIC_DEPLOYMENT_MODE === 'oss'; - return (
@@ -241,7 +240,7 @@ const StartCallEditForm = ({
- {!isOSS && ( + {!isOSSMode() && (