From 7cfe55a3603d5e8a96c15ba7a00aebd4a8013d57 Mon Sep 17 00:00:00 2001 From: Felarof Date: Mon, 12 Jan 2026 15:03:13 -0800 Subject: [PATCH] feat: jtbd agent add transcription support (#212) * feat: v0.1 of voice transcription for JTBD survey Add voice input capability to the JTBD Product Survey chat: - useVoiceInput hook for audio recording and transcription - VoiceInputButton component for mic/stop/loading states - Waveform visualization during recording - Integration with BrowserOS gateway transcription endpoint Co-Authored-By: Claude Opus 4.5 * style: make voice button orange like send button Co-Authored-By: Claude Opus 4.5 * chore: refactor jtbd agent * chore: udpate text * fix: clean up stop recording if stopped midway --------- Co-authored-by: Claude Opus 4.5 --- apps/agent/entrypoints/options/App.tsx | 4 +- .../{jtbd-agent-chat.tsx => chat.tsx} | 95 +++++++- .../{jtbd-agent-header.tsx => header.tsx} | 4 +- .../{jtbd-agent-page.tsx => index.tsx} | 18 +- .../{use-jtbd-agent-chat.ts => use-chat.ts} | 2 +- .../options/jtbd-agent/use-voice-input.ts | 208 ++++++++++++++++++ .../options/jtbd-agent/voice-input-button.tsx | 53 +++++ .../{jtbd-agent-welcome.tsx => welcome.tsx} | 2 +- 8 files changed, 360 insertions(+), 26 deletions(-) rename apps/agent/entrypoints/options/jtbd-agent/{jtbd-agent-chat.tsx => chat.tsx} (50%) rename apps/agent/entrypoints/options/jtbd-agent/{jtbd-agent-header.tsx => header.tsx} (86%) rename apps/agent/entrypoints/options/jtbd-agent/{jtbd-agent-page.tsx => index.tsx} (83%) rename apps/agent/entrypoints/options/jtbd-agent/{use-jtbd-agent-chat.ts => use-chat.ts} (99%) create mode 100644 apps/agent/entrypoints/options/jtbd-agent/use-voice-input.ts create mode 100644 apps/agent/entrypoints/options/jtbd-agent/voice-input-button.tsx rename apps/agent/entrypoints/options/jtbd-agent/{jtbd-agent-welcome.tsx => welcome.tsx} (92%) diff --git a/apps/agent/entrypoints/options/App.tsx b/apps/agent/entrypoints/options/App.tsx index aba8ee18..c0647505 100644 --- a/apps/agent/entrypoints/options/App.tsx +++ b/apps/agent/entrypoints/options/App.tsx @@ -3,7 +3,7 @@ import { HashRouter, Navigate, Route, Routes } from 'react-router' import { AISettingsPage } from './ai-settings/AISettingsPage' import { ConnectMCP } from './connect-mcp/ConnectMCP' import { CustomizationPage } from './customization/CustomizationPage' -import { JTBDAgentPage } from './jtbd-agent/jtbd-agent-page' +import { SurveyPage } from './jtbd-agent' import { DashboardLayout } from './layout/DashboardLayout' import { LlmHubPage } from './llm-hub/LlmHubPage' import { MCPSettingsPage } from './mcp-settings/MCPSettingsPage' @@ -36,7 +36,7 @@ export const App: FC = () => { element={} /> } /> - } /> + } /> diff --git a/apps/agent/entrypoints/options/jtbd-agent/jtbd-agent-chat.tsx b/apps/agent/entrypoints/options/jtbd-agent/chat.tsx similarity index 50% rename from apps/agent/entrypoints/options/jtbd-agent/jtbd-agent-chat.tsx rename to apps/agent/entrypoints/options/jtbd-agent/chat.tsx index b095bdf8..57e5aee2 100644 --- a/apps/agent/entrypoints/options/jtbd-agent/jtbd-agent-chat.tsx +++ b/apps/agent/entrypoints/options/jtbd-agent/chat.tsx @@ -4,7 +4,9 @@ import { MessageResponse } from '@/components/ai-elements/message' import { Button } from '@/components/ui/button' import { Textarea } from '@/components/ui/textarea' import { cn } from '@/lib/utils' -import type { Message } from './use-jtbd-agent-chat' +import type { Message } from './use-chat' +import { useVoiceInput } from './use-voice-input' +import { VoiceInputButton } from './voice-input-button' interface Props { messages: Message[] @@ -39,7 +41,29 @@ const MessageBubble: FC<{ message: Message }> = ({ message }) => { ) } -export const JTBDAgentChat: FC = ({ +const WAVEFORM_BARS = [0, 1, 2, 3, 4] as const + +const WaveformIndicator: FC<{ level: number }> = ({ level }) => { + return ( +
+ {WAVEFORM_BARS.map((barIndex) => { + const barLevel = Math.max( + 0.2, + Math.sin((barIndex / WAVEFORM_BARS.length) * Math.PI) * (level / 100), + ) + return ( +
+ ) + })} +
+ ) +} + +export const Chat: FC = ({ messages, isStreaming, onSendMessage, @@ -48,15 +72,28 @@ export const JTBDAgentChat: FC = ({ const [input, setInput] = useState('') const messagesEndRef = useRef(null) + const voice = useVoiceInput() + const messagesLength = messages.length // biome-ignore lint/correctness/useExhaustiveDependencies: intentionally scroll on message count change useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }) }, [messagesLength]) + // Insert transcript into input when transcription completes + useEffect(() => { + if (voice.transcript && !voice.isTranscribing) { + setInput((prev) => { + const separator = prev.trim() ? ' ' : '' + return prev + separator + voice.transcript + }) + voice.clearTranscript() + } + }, [voice.transcript, voice.isTranscribing, voice.clearTranscript]) + const handleSubmit = (e: FormEvent) => { e.preventDefault() - if (!input.trim() || isStreaming) return + if (!input.trim() || isStreaming || voice.isRecording) return onSendMessage(input.trim()) setInput('') } @@ -68,6 +105,9 @@ export const JTBDAgentChat: FC = ({ } } + const isInputDisabled = + isStreaming || voice.isRecording || voice.isTranscribing + return (
@@ -78,16 +118,43 @@ export const JTBDAgentChat: FC = ({
+ {voice.error && ( +
{voice.error}
+ )} +
-