diff --git a/web/oss/src/components/AgentChatSlice/assets/constants.test.ts b/web/oss/src/components/AgentChatSlice/assets/constants.test.ts new file mode 100644 index 0000000000..dcfe05cff4 --- /dev/null +++ b/web/oss/src/components/AgentChatSlice/assets/constants.test.ts @@ -0,0 +1,29 @@ +import {afterEach, describe, expect, it, vi} from "vitest" + +import {getEnv} from "@/oss/lib/helpers/dynamicEnv" + +import {isAgentVoiceInputAvailable} from "./constants" + +vi.mock("@/oss/lib/helpers/dynamicEnv", () => ({getEnv: vi.fn(() => "")})) + +const mockedGetEnv = vi.mocked(getEnv) + +describe("voice input gating", () => { + afterEach(() => { + mockedGetEnv.mockReset() + mockedGetEnv.mockReturnValue("") + }) + + it("stays off when neither the setting nor the env flag is on", () => { + expect(isAgentVoiceInputAvailable(false)).toBe(false) + }) + + it("turns on from the per-user setting alone", () => { + expect(isAgentVoiceInputAvailable(true)).toBe(true) + }) + + it("turns on from the env flag alone, so dev stacks keep working", () => { + mockedGetEnv.mockReturnValue("true") + expect(isAgentVoiceInputAvailable(false)).toBe(true) + }) +}) diff --git a/web/oss/src/components/AgentChatSlice/assets/constants.ts b/web/oss/src/components/AgentChatSlice/assets/constants.ts index e240df95ba..fa256c64a7 100644 --- a/web/oss/src/components/AgentChatSlice/assets/constants.ts +++ b/web/oss/src/components/AgentChatSlice/assets/constants.ts @@ -32,6 +32,13 @@ export const isAgentChatSteerEnabled = (): boolean => export const isAgentVoiceInputEnabled = (): boolean => (getEnv("NEXT_PUBLIC_AGENT_VOICE_INPUT") || "").toLowerCase() === "true" +/** + * Voice input shows when the per-user experimental setting is on, or when the deployment forces it + * on with the env flag (so dev stacks that set it keep the UI without touching their settings). + */ +export const isAgentVoiceInputAvailable = (settingEnabled: boolean): boolean => + settingEnabled || isAgentVoiceInputEnabled() + /** * File uploads and attachments — the composer attach button + attachment preview, and every drive * upload entry point (upload button, drop-to-upload in the Files drawer, drop-to-stage on a recents diff --git a/web/oss/src/components/AgentChatSlice/hooks/useVoiceComposer.ts b/web/oss/src/components/AgentChatSlice/hooks/useVoiceComposer.ts index 509645fead..5a9690966f 100644 --- a/web/oss/src/components/AgentChatSlice/hooks/useVoiceComposer.ts +++ b/web/oss/src/components/AgentChatSlice/hooks/useVoiceComposer.ts @@ -1,8 +1,11 @@ import {type RefObject, useState} from "react" import {type RichChatInputHandle} from "@agenta/ui/rich-chat-input" +import {useAtomValue} from "jotai" -import {isAgentVoiceInputEnabled} from "../assets/constants" +import {agentVoiceInputEnabledAtom} from "@/oss/state/settings/featureFlags" + +import {isAgentVoiceInputAvailable} from "../assets/constants" import {useAudioRecorder} from "./useAudioRecorder" @@ -36,8 +39,10 @@ export const useVoiceComposer = ({ * Decided when recording STARTS: the composer is covered by the recording bar and drops are * blocked while capturing, so neither the text nor the tray can change in between. */ - // Flagged off until the agent service accepts audio parts (`NEXT_PUBLIC_AGENT_VOICE_INPUT`). - const voiceEnabled = isAgentVoiceInputEnabled() + // Experimental: off until the person turns it on in Settings, or the deployment forces it on + // with `NEXT_PUBLIC_AGENT_VOICE_INPUT`. + const voiceSettingEnabled = useAtomValue(agentVoiceInputEnabledAtom) + const voiceEnabled = isAgentVoiceInputAvailable(voiceSettingEnabled) const [voiceWillSend, setVoiceWillSend] = useState(false) const voiceRecorder = useAudioRecorder((file) => { if (voiceWillSend) onSendVoiceMessage(file) diff --git a/web/oss/src/components/pages/settings/FeatureFlags/FeatureFlags.tsx b/web/oss/src/components/pages/settings/FeatureFlags/FeatureFlags.tsx index c8bac14134..90c3ac6645 100644 --- a/web/oss/src/components/pages/settings/FeatureFlags/FeatureFlags.tsx +++ b/web/oss/src/components/pages/settings/FeatureFlags/FeatureFlags.tsx @@ -4,7 +4,10 @@ import {useAtom, useAtomValue, useSetAtom} from "jotai" import {navSimplifiedOverrideAtom} from "@/oss/lib/onboarding/atoms" import {advancedNavHiddenAtom} from "@/oss/state/onboarding/selectors" -import {playgroundInspectorEnabledAtom} from "@/oss/state/settings/featureFlags" +import { + agentVoiceInputEnabledAtom, + playgroundInspectorEnabledAtom, +} from "@/oss/state/settings/featureFlags" interface FlagRowProps { title: string @@ -36,6 +39,7 @@ const FeatureFlags = () => { const [playgroundInspectorEnabled, setPlaygroundInspectorEnabled] = useAtom( playgroundInspectorEnabledAtom, ) + const [agentVoiceInputEnabled, setAgentVoiceInputEnabled] = useAtom(agentVoiceInputEnabledAtom) return (
@@ -49,6 +53,12 @@ const FeatureFlags = () => { enabled={!advancedNavHidden} onChange={(enabled) => setNavSimplifiedOverride(!enabled)} /> +
diff --git a/web/oss/src/state/settings/featureFlags.ts b/web/oss/src/state/settings/featureFlags.ts index 3408822189..c7d898a02e 100644 --- a/web/oss/src/state/settings/featureFlags.ts +++ b/web/oss/src/state/settings/featureFlags.ts @@ -7,6 +7,24 @@ const playgroundInspectorAtomFamily = atomFamily((userId: string) => atomWithStorage(`agenta:settings:${userId}:playground-inspector`, false), ) +const agentVoiceInputAtomFamily = atomFamily((userId: string) => + atomWithStorage(`agenta:settings:${userId}:agent-voice-input`, false), +) + +/** Experimental switch for the agent chat composer's dictation + voice-message controls. */ +export const agentVoiceInputEnabledAtom = atom( + (get) => { + const userId = get(onboardingStorageUserIdAtom) + if (!userId) return false + return get(agentVoiceInputAtomFamily(userId)) + }, + (get, set, next: boolean) => { + const userId = get(onboardingStorageUserIdAtom) + if (!userId) return + set(agentVoiceInputAtomFamily(userId), next) + }, +) + export const playgroundInspectorEnabledAtom = atom( (get) => { const userId = get(onboardingStorageUserIdAtom)