Skip to content

Commit 5c003ca

Browse files
author
ci-bot
committed
add audio prompt
1 parent a6ba75b commit 5c003ca

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>
@@ -68,6 +70,8 @@ export const PromptArea: React.FC<PromptAreaProps> = ({
6870
handleSetModel,
6971
handleModelSelection,
7072
handleGenerateWorkspace,
73+
handleRecord,
74+
isRecording,
7175
dispatchActivity,
7276
contextBtnRef,
7377
modelBtnRef,
@@ -199,6 +203,15 @@ export const PromptArea: React.FC<PromptAreaProps> = ({
199203
</button>
200204
)}
201205
</div>
206+
<button
207+
data-id="remix-ai-record-audio"
208+
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'}`}
209+
onClick={handleRecord}
210+
title={isRecording ? 'Stop recording' : 'Record audio'}
211+
>
212+
<i className={`fa ${isRecording ? 'fa-stop' : 'fa-microphone'} me-1`}></i>
213+
{isRecording ? 'Stop' : 'Audio Prompt'}
214+
</button>
202215
<button
203216
data-id="remix-ai-workspace-generate"
204217
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

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

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

708+
const handleRecord = useCallback(async () => {
709+
await toggleRecording()
710+
if (!isRecording) {
711+
_paq.push(['trackEvent', 'remixAI', 'StartAudioRecording'])
712+
}
713+
}, [toggleRecording, isRecording])
714+
649715
const handleGenerateWorkspace = useCallback(async () => {
650716
dispatchActivity('button', 'generateWorkspace')
651717
try {
@@ -800,6 +866,8 @@ export const RemixUiRemixAiAssistant = React.forwardRef<
800866
handleSetModel={handleSetModel}
801867
handleModelSelection={handleModelSelection}
802868
handleGenerateWorkspace={handleGenerateWorkspace}
869+
handleRecord={handleRecord}
870+
isRecording={isRecording}
803871
dispatchActivity={dispatchActivity}
804872
contextBtnRef={contextBtnRef}
805873
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)