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() && (