Skip to content

Commit 52fc6d8

Browse files
author
ci-bot
committed
add audio prompt
1 parent 0729e76 commit 52fc6d8

File tree

6 files changed

+475
-0
lines changed

6 files changed

+475
-0
lines changed

libs/remix-ui/remix-ai-assistant/src/components/prompt.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ export interface PromptAreaProps {
3030
handleSetModel: () => void
3131
handleModelSelection: (modelName: string) => void
3232
handleGenerateWorkspace: () => void
33+
handleRecord: () => void
34+
isRecording: boolean
3335
dispatchActivity: (type: ActivityType, payload?: any) => void
3436
contextBtnRef: React.RefObject<HTMLButtonElement>
3537
modelBtnRef: React.RefObject<HTMLButtonElement>
@@ -70,6 +72,8 @@ export const PromptArea: React.FC<PromptAreaProps> = ({
7072
handleSetModel,
7173
handleModelSelection,
7274
handleGenerateWorkspace,
75+
handleRecord,
76+
isRecording,
7377
dispatchActivity,
7478
contextBtnRef,
7579
modelBtnRef,
@@ -205,6 +209,15 @@ export const PromptArea: React.FC<PromptAreaProps> = ({
205209
</button>
206210
)}
207211
</div>
212+
<button
213+
data-id="remix-ai-record-audio"
214+
className={`btn btn-text btn-sm small fw-light mt-2 align-self-end border border-text rounded ${isRecording ? 'btn-danger text-white' : 'text-secondary'}`}
215+
onClick={handleRecord}
216+
title={isRecording ? 'Stop recording' : 'Record audio'}
217+
>
218+
<i className={`fa ${isRecording ? 'fa-stop' : 'fa-microphone'} me-1`}></i>
219+
{isRecording ? 'Stop' : 'Audio Prompt'}
220+
</button>
208221
<button
209222
data-id="remix-ai-workspace-generate"
210223
className="btn btn-text btn-sm small fw-light text-secondary mt-2 align-self-end border border-text rounded"

libs/remix-ui/remix-ai-assistant/src/components/remix-ui-remix-ai-assistant.tsx

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { ActivityType, ChatMessage } from '../lib/types'
1212
import { groupListType } from '../types/componentTypes'
1313
import GroupListMenu from './contextOptMenu'
1414
import { useOnClickOutside } from './onClickOutsideHook'
15+
import { useAudioTranscription } from '../hooks/useAudioTranscription'
1516

1617
const _paq = (window._paq = window._paq || [])
1718

@@ -63,6 +64,58 @@ export const RemixUiRemixAiAssistant = React.forwardRef<
6364
const textareaRef = useRef<HTMLTextAreaElement>(null)
6465
const aiChatRef = useRef<HTMLDivElement>(null)
6566

67+
// Ref to hold the sendPrompt function for audio transcription callback
68+
const sendPromptRef = useRef<((prompt: string) => Promise<void>) | null>(null)
69+
70+
// Audio transcription hook
71+
const {
72+
isRecording,
73+
isTranscribing,
74+
error: transcriptionError,
75+
toggleRecording
76+
} = useAudioTranscription({
77+
apiKey: 'fw_3ZZeKZ67JHvZKahmHUvo8XTR',
78+
model: 'whisper-v3',
79+
onTranscriptionComplete: async (text) => {
80+
if (sendPromptRef.current) {
81+
await sendPromptRef.current(text)
82+
_paq.push(['trackEvent', 'remixAI', 'SpeechToTextPrompt', text])
83+
}
84+
},
85+
onError: (error) => {
86+
console.error('Audio transcription error:', error)
87+
setMessages(prev => [...prev, {
88+
id: crypto.randomUUID(),
89+
role: 'assistant',
90+
content: `**Audio transcription failed.**\n\nError: ${error.message}`,
91+
timestamp: Date.now(),
92+
sentiment: 'none'
93+
}])
94+
}
95+
})
96+
97+
// Show transcribing status
98+
useEffect(() => {
99+
if (isTranscribing) {
100+
setMessages(prev => [...prev, {
101+
id: crypto.randomUUID(),
102+
role: 'assistant',
103+
content: '***Transcribing audio...***',
104+
timestamp: Date.now(),
105+
sentiment: 'none'
106+
}])
107+
} else {
108+
// Remove transcribing message when done
109+
setMessages(prev => {
110+
const last = prev[prev.length - 1]
111+
if (last?.content === '***Transcribing audio...***') {
112+
return prev.slice(0, -1)
113+
}
114+
return prev
115+
})
116+
}
117+
}, [isTranscribing])
118+
66119
useOnClickOutside([modelBtnRef, contextBtnRef], () => setShowAssistantOptions(false))
67120
useOnClickOutside([modelBtnRef, contextBtnRef], () => setShowContextOptions(false))
68121
useOnClickOutside([modelSelectorBtnRef], () => setShowModelOptions(false))
@@ -429,6 +482,12 @@ export const RemixUiRemixAiAssistant = React.forwardRef<
429482
},
430483
[isStreaming, props.plugin]
431484
)
485+
486+
// Update ref for audio transcription callback
487+
useEffect(() => {
488+
sendPromptRef.current = sendPrompt
489+
}, [sendPrompt])
490+
432491
const handleGenerateWorkspaceWithPrompt = useCallback(async (prompt: string) => {
433492
dispatchActivity('button', 'generateWorkspace')
434493
if (prompt && prompt.trim()) {
@@ -647,6 +706,13 @@ export const RemixUiRemixAiAssistant = React.forwardRef<
647706
)
648707
}
649708

709+
const handleRecord = useCallback(async () => {
710+
await toggleRecording()
711+
if (!isRecording) {
712+
_paq.push(['trackEvent', 'remixAI', 'StartAudioRecording'])
713+
}
714+
}, [toggleRecording, isRecording])
715+
650716
const handleGenerateWorkspace = useCallback(async () => {
651717
dispatchActivity('button', 'generateWorkspace')
652718
try {
@@ -802,6 +868,8 @@ export const RemixUiRemixAiAssistant = React.forwardRef<
802868
handleSetModel={handleSetModel}
803869
handleModelSelection={handleModelSelection}
804870
handleGenerateWorkspace={handleGenerateWorkspace}
871+
handleRecord={handleRecord}
872+
isRecording={isRecording}
805873
dispatchActivity={dispatchActivity}
806874
contextBtnRef={contextBtnRef}
807875
modelBtnRef={modelBtnRef}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
/**
2+
* React hook for audio recording and transcription
3+
*/
4+
5+
import { useState, useRef, useCallback } from 'react'
6+
import { AudioRecorder } from '../utils/audioRecorder'
7+
import { transcribeAudio, FireworksTranscriptionError } from '../services/fireworksTranscription'
8+
9+
export interface UseAudioTranscriptionOptions {
10+
apiKey: string
11+
model?: string
12+
onTranscriptionComplete?: (text: string) => void
13+
onError?: (error: Error) => void
14+
}
15+
16+
export interface UseAudioTranscriptionResult {
17+
isRecording: boolean
18+
isTranscribing: boolean
19+
error: Error | null
20+
startRecording: () => Promise<void>
21+
stopRecording: () => void
22+
toggleRecording: () => Promise<void>
23+
}
24+
25+
export function useAudioTranscription(
26+
options: UseAudioTranscriptionOptions
27+
): UseAudioTranscriptionResult {
28+
const [isRecording, setIsRecording] = useState(false)
29+
const [isTranscribing, setIsTranscribing] = useState(false)
30+
const [error, setError] = useState<Error | null>(null)
31+
const audioRecorderRef = useRef<AudioRecorder | null>(null)
32+
33+
const startRecording = useCallback(async () => {
34+
try {
35+
setError(null)
36+
37+
// Create new recorder instance
38+
if (!audioRecorderRef.current) {
39+
audioRecorderRef.current = new AudioRecorder()
40+
}
41+
42+
await audioRecorderRef.current.startRecording({
43+
onStop: async (audioBlob) => {
44+
setIsRecording(false)
45+
setIsTranscribing(true)
46+
47+
try {
48+
// Transcribe the audio
49+
const result = await transcribeAudio(audioBlob, {
50+
apiKey: options.apiKey,
51+
model: options.model
52+
})
53+
54+
setIsTranscribing(false)
55+
56+
// Call completion callback with transcribed text
57+
if (result.text && result.text.trim()) {
58+
options.onTranscriptionComplete?.(result.text.trim())
59+
}
60+
} catch (err) {
61+
setIsTranscribing(false)
62+
const error = err instanceof Error ? err : new Error('Transcription failed')
63+
setError(error)
64+
options.onError?.(error)
65+
}
66+
},
67+
onError: (err) => {
68+
setIsRecording(false)
69+
setError(err)
70+
options.onError?.(err)
71+
}
72+
})
73+
74+
setIsRecording(true)
75+
} catch (err) {
76+
const error = err instanceof Error ? err : new Error('Failed to start recording')
77+
setError(error)
78+
setIsRecording(false)
79+
options.onError?.(error)
80+
}
81+
}, [options])
82+
83+
const stopRecording = useCallback(() => {
84+
if (audioRecorderRef.current) {
85+
audioRecorderRef.current.stopRecording()
86+
}
87+
}, [])
88+
89+
const toggleRecording = useCallback(async () => {
90+
if (isRecording) {
91+
stopRecording()
92+
} else {
93+
await startRecording()
94+
}
95+
}, [isRecording, startRecording, stopRecording])
96+
97+
return {
98+
isRecording,
99+
isTranscribing,
100+
error,
101+
startRecording,
102+
stopRecording,
103+
toggleRecording
104+
}
105+
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
/**
2+
* Fireworks AI Transcription Service
3+
* Provides speech-to-text transcription using Fireworks API
4+
*/
5+
6+
export interface TranscriptionOptions {
7+
model?: string
8+
language?: string
9+
apiKey: string
10+
}
11+
12+
export interface TranscriptionResult {
13+
text: string
14+
duration?: number
15+
language?: string
16+
}
17+
18+
export class FireworksTranscriptionError extends Error {
19+
constructor(
20+
message: string,
21+
public statusCode?: number,
22+
public response?: any
23+
) {
24+
super(message)
25+
this.name = 'FireworksTranscriptionError'
26+
}
27+
}
28+
29+
/**
30+
* Transcribe audio using Fireworks AI Whisper API
31+
*/
32+
export async function transcribeAudio(
33+
audioBlob: Blob,
34+
options: TranscriptionOptions
35+
): Promise<TranscriptionResult> {
36+
const { model = 'whisper-v3', apiKey } = options
37+
38+
if (!apiKey) {
39+
throw new FireworksTranscriptionError('Fireworks API key is required')
40+
}
41+
42+
// Create form data
43+
const formData = new FormData()
44+
formData.append('file', audioBlob, 'recording.webm')
45+
formData.append('model', model)
46+
47+
try {
48+
const response = await fetch('https://api.fireworks.ai/inference/v1/audio/transcriptions', {
49+
method: 'POST',
50+
headers: {
51+
'Authorization': `Bearer ${apiKey}`
52+
},
53+
body: formData
54+
})
55+
56+
if (!response.ok) {
57+
const errorText = await response.text().catch(() => response.statusText)
58+
throw new FireworksTranscriptionError(
59+
`Transcription failed: ${errorText}`,
60+
response.status,
61+
errorText
62+
)
63+
}
64+
65+
const result = await response.json()
66+
67+
if (!result.text) {
68+
throw new FireworksTranscriptionError('No transcription text in response', undefined, result)
69+
}
70+
71+
return {
72+
text: result.text,
73+
duration: result.duration,
74+
language: result.language
75+
}
76+
} catch (error) {
77+
if (error instanceof FireworksTranscriptionError) {
78+
throw error
79+
}
80+
81+
// Handle network or other errors
82+
const message = error instanceof Error ? error.message : 'Unknown error occurred'
83+
throw new FireworksTranscriptionError(`Network error: ${message}`)
84+
}
85+
}
86+
87+
/**
88+
* Check if Fireworks API key is configured
89+
*/
90+
export function hasApiKey(apiKey: string | null | undefined): apiKey is string {
91+
return typeof apiKey === 'string' && apiKey.length > 0
92+
}

0 commit comments

Comments
 (0)