diff --git a/src/backend/base/langflow/api/v1/schemas.py b/src/backend/base/langflow/api/v1/schemas.py index 619f572febfd..6101cad32b40 100644 --- a/src/backend/base/langflow/api/v1/schemas.py +++ b/src/backend/base/langflow/api/v1/schemas.py @@ -407,6 +407,7 @@ class ConfigResponse(BaseModel): public_flow_cleanup_interval: int public_flow_expiration: int event_delivery: Literal["polling", "streaming", "direct"] + voice_mode_available: bool @classmethod def from_settings(cls, settings: Settings) -> "ConfigResponse": @@ -431,6 +432,7 @@ def from_settings(cls, settings: Settings) -> "ConfigResponse": public_flow_cleanup_interval=settings.public_flow_cleanup_interval, public_flow_expiration=settings.public_flow_expiration, event_delivery=settings.event_delivery, + voice_mode_available=settings.voice_mode_available, ) diff --git a/src/backend/base/langflow/services/settings/base.py b/src/backend/base/langflow/services/settings/base.py index 3fea44d8d2ef..b10de52ddd34 100644 --- a/src/backend/base/langflow/services/settings/base.py +++ b/src/backend/base/langflow/services/settings/base.py @@ -507,6 +507,16 @@ def update_settings(self, **kwargs) -> None: logger.debug(f"Updated {key}") logger.debug(f"{key}: {getattr(self, key)}") + @property + def voice_mode_available(self) -> bool: + """Check if voice mode is available by testing webrtcvad import.""" + try: + import webrtcvad # noqa: F401 + except ImportError: + return False + else: + return True + @classmethod @override def settings_customise_sources( # type: ignore[misc] diff --git a/src/frontend/src/controllers/API/queries/config/use-get-config.ts b/src/frontend/src/controllers/API/queries/config/use-get-config.ts index baa072e044d2..b249ba55f047 100644 --- a/src/frontend/src/controllers/API/queries/config/use-get-config.ts +++ b/src/frontend/src/controllers/API/queries/config/use-get-config.ts @@ -21,6 +21,7 @@ export interface ConfigResponse { webhook_polling_interval: number; serialization_max_items_length: number; event_delivery: EventDeliveryType; + voice_mode_available: boolean; } export const useGetConfig: useQueryFunctionType = ( diff --git a/src/frontend/src/modals/IOModal/components/chatView/chatInput/components/input-wrapper.tsx b/src/frontend/src/modals/IOModal/components/chatView/chatInput/components/input-wrapper.tsx index 202a1381b36b..192e9d758538 100644 --- a/src/frontend/src/modals/IOModal/components/chatView/chatInput/components/input-wrapper.tsx +++ b/src/frontend/src/modals/IOModal/components/chatView/chatInput/components/input-wrapper.tsx @@ -1,4 +1,5 @@ import type React from "react"; +import { useGetConfig } from "@/controllers/API/queries/config/use-get-config"; import { ENABLE_IMAGE_ON_PLAYGROUND, ENABLE_VOICE_ASSISTANT, @@ -51,6 +52,9 @@ const InputWrapper: React.FC = ({ }) => { const classNameFilePreview = `flex w-full items-center gap-2 py-2 overflow-auto custom-scroll`; + // Check if voice mode is available + const { data: config } = useGetConfig(); + return (
= ({ )}
- {ENABLE_VOICE_ASSISTANT && ( + {ENABLE_VOICE_ASSISTANT && config?.voice_mode_available && ( setShowAudioInput(true)} /> )} diff --git a/src/frontend/tests/core/features/voice-assistant.spec.ts b/src/frontend/tests/core/features/voice-assistant.spec.ts index 513a7644de4c..3af2e61800da 100644 --- a/src/frontend/tests/core/features/voice-assistant.spec.ts +++ b/src/frontend/tests/core/features/voice-assistant.spec.ts @@ -11,6 +11,20 @@ test( "OPENAI_API_KEY required to run this test", ); + await page.route("**/api/v1/config", (route) => { + route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify({ + voice_mode_available: true, + }), + headers: { + "content-type": "application/json", + ...route.request().headers(), + }, + }); + }); + await awaitBootstrapTest(page); await page.getByTestId("side_nav_options_all-templates").click(); @@ -58,3 +72,61 @@ test( await expect(page.getByTestId("input-wrapper")).toBeVisible(); }, ); + +test( + "user should not be able to see voice button if voice mode is not available", + { tag: ["@release", "@workspace", "@api"] }, + async ({ page, request }) => { + await page.route("**/api/v1/config", (route) => { + route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify({ + voice_mode_available: false, + }), + headers: { + "content-type": "application/json", + ...route.request().headers(), + }, + }); + }); + + await awaitBootstrapTest(page); + + await page.getByTestId("side_nav_options_all-templates").click(); + await page.getByRole("heading", { name: "Basic Prompting" }).click(); + await page.getByTestId("playground-btn-flow-io").click(); + + await expect(page.getByTestId("voice-button")).not.toBeVisible(); + }, +); + +test( + "user should be able to see voice button if voice mode is available", + { tag: ["@release", "@workspace", "@api"] }, + async ({ page, request }) => { + await page.route("**/api/v1/config", (route) => { + route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify({ + voice_mode_available: true, + }), + headers: { + "content-type": "application/json", + ...route.request().headers(), + }, + }); + }); + + await awaitBootstrapTest(page); + + await page.getByTestId("side_nav_options_all-templates").click(); + await page.getByRole("heading", { name: "Basic Prompting" }).click(); + await page.getByTestId("playground-btn-flow-io").click(); + + await expect(page.getByTestId("voice-button")).toBeVisible(); + + await page.getByTestId("voice-button").click(); + }, +);