From 8a0957ec98002d63d13e62f9232773c534ae79e7 Mon Sep 17 00:00:00 2001 From: cristhianzl Date: Fri, 29 Aug 2025 18:16:21 -0300 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20(schemas.py):=20Add=20support=20for?= =?UTF-8?q?=20voice=20mode=20availability=20in=20ConfigResponse=20schema?= =?UTF-8?q?=20=F0=9F=94=A7=20(base.py):=20Implement=20property=20to=20chec?= =?UTF-8?q?k=20if=20voice=20mode=20is=20available=20in=20Settings=20class?= =?UTF-8?q?=20=F0=9F=93=9D=20(use-get-config.tsx):=20Update=20InputWrapper?= =?UTF-8?q?=20component=20to=20conditionally=20render=20voice=20button=20b?= =?UTF-8?q?ased=20on=20voice=20mode=20availability=20=E2=9C=85=20(voice-as?= =?UTF-8?q?sistant.spec.ts):=20Add=20tests=20to=20verify=20visibility=20of?= =?UTF-8?q?=20voice=20button=20based=20on=20voice=20mode=20availability?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/backend/base/langflow/api/v1/schemas.py | 2 + .../base/langflow/services/settings/base.py | 10 +++ .../API/queries/config/use-get-config.ts | 1 + .../chatInput/components/input-wrapper.tsx | 6 +- .../core/features/voice-assistant.spec.ts | 72 +++++++++++++++++++ 5 files changed, 90 insertions(+), 1 deletion(-) 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(); + }, +);