Skip to content

Commit b5207dd

Browse files
committed
chore(chat): remove deployed-chat voice mode
Removes the voice-first interface and TTS playback from the deployed chat, keeping workspace dictation, which is a separate feature. - Deletes the VoiceInterface UI and its particles canvas, the chat mic input, the TTS audio-streaming hook, the /api/proxy/tts/stream relay and its contract, and the voice-settings query hook. - Unpicks the voice wiring in chat.tsx and use-chat-streaming: the audio stream handler, sentence-splitting for speech, voice-first mode state, and the isVoiceInput plumbing through ChatInput. - Drops the now-dead chatId branch from /api/speech/token. It was the anonymous public-chat path; with no caller left it would have stayed an unauthenticated relay spending the platform key. That leaves resolveDeployedChatCaller unused, so it goes too. - Removes code the above orphaned: MAX_CHAT_SESSION_MS, the noop util, the audio/position refs in use-chat-streaming that were only ever written, and the chatId field on the speech contract. Keeps /api/settings/voice, lib/speech and use-speech-to-text: the workspace home input still uses them. Keeps the voice-output usage source, enum and label — Postgres cannot drop an enum value, and historical usage_log rows still need a label to render.
1 parent 0bc4fb4 commit b5207dd

23 files changed

Lines changed: 82 additions & 2601 deletions

File tree

apps/sim/app/(interfaces)/chat/[identifier]/chat.tsx

Lines changed: 4 additions & 128 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@
33
import { type RefObject, useCallback, useEffect, useMemo, useRef, useState } from 'react'
44
import { createLogger } from '@sim/logger'
55
import { generateId } from '@sim/utils/id'
6-
import { DEFAULT_TTS_VOICE_ID } from '@/lib/api/contracts/media/tts-stream'
7-
import { noop } from '@/lib/core/utils/request'
86
import {
97
AGENT_STREAM_PROTOCOL_HEADER,
108
AGENT_STREAM_PROTOCOL_V1,
@@ -19,23 +17,15 @@ import {
1917
ChatMessageContainer,
2018
EmailAuth,
2119
PasswordAuth,
22-
VoiceInterface,
2320
} from '@/app/(interfaces)/chat/components'
2421
import { CHAT_ERROR_MESSAGES, CHAT_REQUEST_TIMEOUT_MS } from '@/app/(interfaces)/chat/constants'
25-
import { useAudioStreaming, useChatStreaming } from '@/app/(interfaces)/chat/hooks'
22+
import { useChatStreaming } from '@/app/(interfaces)/chat/hooks'
2623
import SSOAuth from '@/ee/sso/components/sso-auth'
2724
import { useDeployedChatConfig } from '@/hooks/queries/chats'
2825
import { useGitHubStars } from '@/hooks/queries/github-stars'
29-
import { useVoiceSettings } from '@/hooks/queries/voice-settings'
3026

3127
const logger = createLogger('ChatClient')
3228

33-
interface AudioStreamingOptions {
34-
voiceId: string
35-
chatId: string
36-
onError: (error: Error) => void
37-
}
38-
3929
interface ChatRequestFile {
4030
name: string
4131
size: number
@@ -49,10 +39,6 @@ interface ChatRequestPayload {
4939
files?: ChatRequestFile[]
5040
}
5141

52-
const DEFAULT_VOICE_SETTINGS = {
53-
voiceId: DEFAULT_TTS_VOICE_ID,
54-
}
55-
5642
/**
5743
* Converts a File object to a base64 data URL
5844
*/
@@ -65,33 +51,6 @@ function fileToBase64(file: File): Promise<string> {
6551
})
6652
}
6753

68-
/**
69-
* Creates an audio stream handler for text-to-speech conversion
70-
* @param streamTextToAudio - Function to stream text to audio
71-
* @param voiceId - The voice ID to use for TTS
72-
* @param chatId - Optional chat ID for deployed chat authentication
73-
* @returns Audio stream handler function or undefined
74-
*/
75-
function createAudioStreamHandler(
76-
streamTextToAudio: (text: string, options: AudioStreamingOptions) => Promise<void>,
77-
voiceId: string,
78-
chatId: string
79-
) {
80-
return async (text: string) => {
81-
try {
82-
await streamTextToAudio(text, {
83-
voiceId,
84-
chatId,
85-
onError: (error: Error) => {
86-
logger.error('Audio streaming error:', error)
87-
},
88-
})
89-
} catch (error) {
90-
logger.error('TTS error:', error)
91-
}
92-
}
93-
}
94-
9554
export default function ChatClient({ identifier }: { identifier: string }) {
9655
const [messages, setMessages] = useState<ChatMessage[]>([])
9756
const [inputValue, setInputValue] = useState('')
@@ -105,13 +64,9 @@ export default function ChatClient({ identifier }: { identifier: string }) {
10564
const stickToBottomRef = useRef(true)
10665
const ignoreScrollRef = useRef(false)
10766

108-
const [isVoiceFirstMode, setIsVoiceFirstMode] = useState(false)
109-
11067
const { data: chatConfigResult, error: chatConfigError } = useDeployedChatConfig(identifier)
111-
const { data: voiceSettings } = useVoiceSettings()
11268
const { data: starCount } = useGitHubStars()
11369

114-
const sttAvailable = voiceSettings?.sttAvailable === true
11570
const authRequired = chatConfigResult?.kind === 'auth' ? chatConfigResult.authType : null
11671
const chatConfig = chatConfigResult?.kind === 'config' ? chatConfigResult.config : null
11772

@@ -135,8 +90,6 @@ export default function ChatClient({ identifier }: { identifier: string }) {
13590

13691
const { isStreamingResponse, abortControllerRef, stopStreaming, handleStreamedResponse } =
13792
useChatStreaming()
138-
const audioContextRef = useRef<AudioContext | null>(null)
139-
const { isPlayingAudio, streamTextToAudio, stopAudio } = useAudioStreaming(audioContextRef)
14093

14194
const NEAR_BOTTOM_THRESHOLD_PX = 100
14295

@@ -208,11 +161,10 @@ export default function ChatClient({ identifier }: { identifier: string }) {
208161

209162
container.addEventListener('scroll', handleScroll, { passive: true })
210163
return () => container.removeEventListener('scroll', handleScroll)
211-
}, [chatConfig, isVoiceFirstMode, authRequired])
164+
}, [chatConfig, authRequired])
212165

213166
const handleSendMessage = async (
214167
messageParam?: string,
215-
isVoiceInput = false,
216168
files?: Array<{
217169
id: string
218170
name: string
@@ -227,7 +179,6 @@ export default function ChatClient({ identifier }: { identifier: string }) {
227179

228180
logger.info('Sending message:', {
229181
messageToSend,
230-
isVoiceInput,
231182
conversationId,
232183
filesCount: files?.length,
233184
})
@@ -316,30 +267,12 @@ export default function ChatClient({ identifier }: { identifier: string }) {
316267
throw new Error('Response body is missing')
317268
}
318269

319-
const shouldPlayAudio = isVoiceInput || isVoiceFirstMode
320-
const audioHandler =
321-
shouldPlayAudio && chatConfig?.id
322-
? createAudioStreamHandler(
323-
streamTextToAudio,
324-
DEFAULT_VOICE_SETTINGS.voiceId,
325-
chatConfig.id
326-
)
327-
: undefined
328-
329-
logger.info('Starting to handle streamed response:', { shouldPlayAudio })
330-
331270
await handleStreamedResponse(
332271
response,
333272
setMessages,
334273
setIsLoading,
335274
() => scrollToBottom({ behavior: 'auto' }),
336275
{
337-
voiceSettings: {
338-
isVoiceEnabled: shouldPlayAudio,
339-
voiceId: DEFAULT_VOICE_SETTINGS.voiceId,
340-
autoPlayResponses: shouldPlayAudio,
341-
},
342-
audioStreamHandler: audioHandler,
343276
outputConfigs: chatConfig?.outputConfigs,
344277
abortController,
345278
}
@@ -365,41 +298,6 @@ export default function ChatClient({ identifier }: { identifier: string }) {
365298
}
366299
}
367300

368-
useEffect(() => {
369-
return () => {
370-
stopAudio()
371-
if (audioContextRef.current && audioContextRef.current.state !== 'closed') {
372-
audioContextRef.current.close()
373-
}
374-
}
375-
}, [stopAudio])
376-
377-
const handleVoiceInterruption = useCallback(() => {
378-
stopAudio()
379-
380-
if (isStreamingResponse) {
381-
stopStreaming(setMessages)
382-
}
383-
}, [isStreamingResponse, stopStreaming, setMessages, stopAudio])
384-
385-
const handleVoiceStart = useCallback(() => {
386-
if (!sttAvailable) return
387-
setIsVoiceFirstMode(true)
388-
}, [sttAvailable])
389-
390-
const handleExitVoiceMode = useCallback(() => {
391-
setIsVoiceFirstMode(false)
392-
stopAudio()
393-
}, [stopAudio])
394-
395-
const handleVoiceTranscript = useCallback(
396-
(transcript: string) => {
397-
logger.info('Received voice transcript:', transcript)
398-
handleSendMessage(transcript, true)
399-
},
400-
[handleSendMessage]
401-
)
402-
403301
if (chatConfigError) {
404302
logger.error('Error fetching chat config:', chatConfigError)
405303
return <ChatErrorState error={CHAT_ERROR_MESSAGES.CHAT_UNAVAILABLE} />
@@ -421,26 +319,6 @@ export default function ChatClient({ identifier }: { identifier: string }) {
421319
return <ChatLoadingState />
422320
}
423321

424-
if (isVoiceFirstMode) {
425-
return (
426-
<VoiceInterface
427-
onCallEnd={handleExitVoiceMode}
428-
onVoiceTranscript={handleVoiceTranscript}
429-
onVoiceStart={noop}
430-
onVoiceEnd={noop}
431-
onInterrupt={handleVoiceInterruption}
432-
isStreaming={isStreamingResponse}
433-
isPlayingAudio={isPlayingAudio}
434-
audioContextRef={audioContextRef}
435-
chatId={chatConfig?.id}
436-
messages={displayMessages.map((msg) => ({
437-
content: typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content),
438-
type: msg.type,
439-
}))}
440-
/>
441-
)
442-
}
443-
444322
return (
445323
<div className='light desktop-title-bar-page fixed inset-0 z-[100] flex flex-col bg-[var(--bg)] text-[var(--text-primary)]'>
446324
<DesktopTitleBarLane />
@@ -463,13 +341,11 @@ export default function ChatClient({ identifier }: { identifier: string }) {
463341
<div className='relative p-3 pb-4 md:p-4 md:pb-6'>
464342
<div className='relative mx-auto max-w-3xl md:max-w-[748px]'>
465343
<ChatInput
466-
onSubmit={(value, isVoiceInput, files) => {
467-
void handleSendMessage(value, isVoiceInput, files)
344+
onSubmit={(value, files) => {
345+
void handleSendMessage(value, files)
468346
}}
469347
isStreaming={isStreamingResponse}
470348
onStopStreaming={() => stopStreaming(setMessages)}
471-
onVoiceStart={handleVoiceStart}
472-
sttAvailable={sttAvailable}
473349
/>
474350
</div>
475351
</div>

apps/sim/app/(interfaces)/chat/components/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,3 @@ export { ChatInput } from './input/input'
66
export { ChatLoadingState } from './loading-state/loading-state'
77
export type { ChatMessage } from './message/message'
88
export { ChatMessageContainer } from './message-container/message-container'
9-
export { VoiceInterface } from './voice-interface/voice-interface'

apps/sim/app/(interfaces)/chat/components/input/input.tsx

Lines changed: 5 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,8 @@ import { useCallback, useLayoutEffect, useRef, useState } from 'react'
55
import { Badge, Button, cn, handleKeyboardActivation, Tooltip } from '@sim/emcn'
66
import { createLogger } from '@sim/logger'
77
import { generateId } from '@sim/utils/id'
8-
import { ArrowUp, Mic, Paperclip, X } from 'lucide-react'
8+
import { ArrowUp, Paperclip, X } from 'lucide-react'
99
import { CHAT_ACCEPT_ATTRIBUTE } from '@/lib/uploads/utils/validation'
10-
import { VoiceInput } from '@/app/(interfaces)/chat/components/input/voice-input'
1110

1211
const logger = createLogger('ChatInput')
1312

@@ -23,20 +22,10 @@ interface AttachedFile {
2322
}
2423

2524
export const ChatInput: React.FC<{
26-
onSubmit?: (value: string, isVoiceInput?: boolean, files?: AttachedFile[]) => void
25+
onSubmit?: (value: string, files?: AttachedFile[]) => void
2726
isStreaming?: boolean
2827
onStopStreaming?: () => void
29-
onVoiceStart?: () => void
30-
voiceOnly?: boolean
31-
sttAvailable?: boolean
32-
}> = ({
33-
onSubmit,
34-
isStreaming = false,
35-
onStopStreaming,
36-
onVoiceStart,
37-
voiceOnly = false,
38-
sttAvailable = false,
39-
}) => {
28+
}> = ({ onSubmit, isStreaming = false, onStopStreaming }) => {
4029
const fileInputRef = useRef<HTMLInputElement>(null)
4130
const textareaRef = useRef<HTMLTextAreaElement>(null)
4231
const [inputValue, setInputValue] = useState('')
@@ -114,7 +103,7 @@ export const ChatInput: React.FC<{
114103
const handleSubmit = useCallback(() => {
115104
if (isStreaming) return
116105
if (!inputValue.trim() && attachedFiles.length === 0) return
117-
onSubmit?.(inputValue.trim(), false, attachedFiles)
106+
onSubmit?.(inputValue.trim(), attachedFiles)
118107
setInputValue('')
119108
setAttachedFiles([])
120109
setUploadErrors([])
@@ -141,31 +130,6 @@ export const ChatInput: React.FC<{
141130

142131
const canSubmit = (inputValue.trim().length > 0 || attachedFiles.length > 0) && !isStreaming
143132

144-
if (voiceOnly) {
145-
return (
146-
<Tooltip.Provider>
147-
<div className='flex items-center justify-center'>
148-
{sttAvailable && (
149-
<Tooltip.Root>
150-
<Tooltip.Trigger asChild>
151-
<div>
152-
<VoiceInput
153-
onVoiceStart={onVoiceStart ?? (() => {})}
154-
disabled={isStreaming}
155-
large={true}
156-
/>
157-
</div>
158-
</Tooltip.Trigger>
159-
<Tooltip.Content side='top'>
160-
<p>Start voice conversation</p>
161-
</Tooltip.Content>
162-
</Tooltip.Root>
163-
)}
164-
</div>
165-
</Tooltip.Provider>
166-
)
167-
}
168-
169133
return (
170134
<Tooltip.Provider>
171135
<div className='fixed right-0 bottom-0 left-0 flex w-full items-center justify-center bg-gradient-to-t from-[var(--bg)] to-transparent px-4 pb-4 md:px-0 md:pb-4'>
@@ -302,26 +266,8 @@ export const ChatInput: React.FC<{
302266
/>
303267
</div>
304268

305-
{/* Right: mic + send */}
269+
{/* Right: send */}
306270
<div className='flex items-center gap-1.5'>
307-
{sttAvailable && (
308-
<Tooltip.Root>
309-
<Tooltip.Trigger asChild>
310-
<Button
311-
variant='quiet'
312-
onClick={onVoiceStart}
313-
disabled={isStreaming}
314-
className='size-[28px] rounded-full p-0'
315-
>
316-
<Mic className='size-[16px]' strokeWidth={2} />
317-
</Button>
318-
</Tooltip.Trigger>
319-
<Tooltip.Content side='top'>
320-
<p>Start voice conversation</p>
321-
</Tooltip.Content>
322-
</Tooltip.Root>
323-
)}
324-
325271
{isStreaming ? (
326272
<Button
327273
variant='primary'

0 commit comments

Comments
 (0)