have messages working but streaming still not fixed yet

Signed-off-by: Francisco Javier Arceo <farceo@redhat.com>
This commit is contained in:
Francisco Javier Arceo 2025-07-20 23:50:34 -04:00
parent fc3286be7e
commit f7c9651ca7
23 changed files with 4749 additions and 184 deletions

View file

@ -0,0 +1,93 @@
import { useEffect, useRef, useState } from "react"
import { recordAudio } from "@/lib/audio-utils"
interface UseAudioRecordingOptions {
transcribeAudio?: (blob: Blob) => Promise<string>
onTranscriptionComplete?: (text: string) => void
}
export function useAudioRecording({
transcribeAudio,
onTranscriptionComplete,
}: UseAudioRecordingOptions) {
const [isListening, setIsListening] = useState(false)
const [isSpeechSupported, setIsSpeechSupported] = useState(!!transcribeAudio)
const [isRecording, setIsRecording] = useState(false)
const [isTranscribing, setIsTranscribing] = useState(false)
const [audioStream, setAudioStream] = useState<MediaStream | null>(null)
const activeRecordingRef = useRef<any>(null)
useEffect(() => {
const checkSpeechSupport = async () => {
const hasMediaDevices = !!(
navigator.mediaDevices && navigator.mediaDevices.getUserMedia
)
setIsSpeechSupported(hasMediaDevices && !!transcribeAudio)
}
checkSpeechSupport()
}, [transcribeAudio])
const stopRecording = async () => {
setIsRecording(false)
setIsTranscribing(true)
try {
// First stop the recording to get the final blob
recordAudio.stop()
// Wait for the recording promise to resolve with the final blob
const recording = await activeRecordingRef.current
if (transcribeAudio) {
const text = await transcribeAudio(recording)
onTranscriptionComplete?.(text)
}
} catch (error) {
console.error("Error transcribing audio:", error)
} finally {
setIsTranscribing(false)
setIsListening(false)
if (audioStream) {
audioStream.getTracks().forEach((track) => track.stop())
setAudioStream(null)
}
activeRecordingRef.current = null
}
}
const toggleListening = async () => {
if (!isListening) {
try {
setIsListening(true)
setIsRecording(true)
// Get audio stream first
const stream = await navigator.mediaDevices.getUserMedia({
audio: true,
})
setAudioStream(stream)
// Start recording with the stream
activeRecordingRef.current = recordAudio(stream)
} catch (error) {
console.error("Error recording audio:", error)
setIsListening(false)
setIsRecording(false)
if (audioStream) {
audioStream.getTracks().forEach((track) => track.stop())
setAudioStream(null)
}
}
} else {
await stopRecording()
}
}
return {
isListening,
isSpeechSupported,
isRecording,
isTranscribing,
audioStream,
toggleListening,
stopRecording,
}
}

View file

@ -0,0 +1,73 @@
import { useEffect, useRef, useState } from "react"
// How many pixels from the bottom of the container to enable auto-scroll
const ACTIVATION_THRESHOLD = 50
// Minimum pixels of scroll-up movement required to disable auto-scroll
const MIN_SCROLL_UP_THRESHOLD = 10
export function useAutoScroll(dependencies: React.DependencyList) {
const containerRef = useRef<HTMLDivElement | null>(null)
const previousScrollTop = useRef<number | null>(null)
const [shouldAutoScroll, setShouldAutoScroll] = useState(true)
const scrollToBottom = () => {
if (containerRef.current) {
containerRef.current.scrollTop = containerRef.current.scrollHeight
}
}
const handleScroll = () => {
if (containerRef.current) {
const { scrollTop, scrollHeight, clientHeight } = containerRef.current
const distanceFromBottom = Math.abs(
scrollHeight - scrollTop - clientHeight
)
const isScrollingUp = previousScrollTop.current
? scrollTop < previousScrollTop.current
: false
const scrollUpDistance = previousScrollTop.current
? previousScrollTop.current - scrollTop
: 0
const isDeliberateScrollUp =
isScrollingUp && scrollUpDistance > MIN_SCROLL_UP_THRESHOLD
if (isDeliberateScrollUp) {
setShouldAutoScroll(false)
} else {
const isScrolledToBottom = distanceFromBottom < ACTIVATION_THRESHOLD
setShouldAutoScroll(isScrolledToBottom)
}
previousScrollTop.current = scrollTop
}
}
const handleTouchStart = () => {
setShouldAutoScroll(false)
}
useEffect(() => {
if (containerRef.current) {
previousScrollTop.current = containerRef.current.scrollTop
}
}, [])
useEffect(() => {
if (shouldAutoScroll) {
scrollToBottom()
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, dependencies)
return {
containerRef,
scrollToBottom,
handleScroll,
shouldAutoScroll,
handleTouchStart,
}
}

View file

@ -0,0 +1,39 @@
import { useLayoutEffect, useRef } from "react"
interface UseAutosizeTextAreaProps {
ref: React.RefObject<HTMLTextAreaElement | null>
maxHeight?: number
borderWidth?: number
dependencies: React.DependencyList
}
export function useAutosizeTextArea({
ref,
maxHeight = Number.MAX_SAFE_INTEGER,
borderWidth = 0,
dependencies,
}: UseAutosizeTextAreaProps) {
const originalHeight = useRef<number | null>(null)
useLayoutEffect(() => {
if (!ref.current) return
const currentRef = ref.current
const borderAdjustment = borderWidth * 2
if (originalHeight.current === null) {
originalHeight.current = currentRef.scrollHeight - borderAdjustment
}
currentRef.style.removeProperty("height")
const scrollHeight = currentRef.scrollHeight
// Make sure we don't go over maxHeight
const clampedToMax = Math.min(scrollHeight, maxHeight)
// Make sure we don't go less than the original height
const clampedToMin = Math.max(clampedToMax, originalHeight.current)
currentRef.style.height = `${clampedToMin + borderAdjustment}px`
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [maxHeight, ref, ...dependencies])
}

View file

@ -0,0 +1,36 @@
import { useCallback, useRef, useState } from "react"
import { toast } from "sonner"
type UseCopyToClipboardProps = {
text: string
copyMessage?: string
}
export function useCopyToClipboard({
text,
copyMessage = "Copied to clipboard!",
}: UseCopyToClipboardProps) {
const [isCopied, setIsCopied] = useState(false)
const timeoutRef = useRef<NodeJS.Timeout | null>(null)
const handleCopy = useCallback(() => {
navigator.clipboard
.writeText(text)
.then(() => {
toast.success(copyMessage)
setIsCopied(true)
if (timeoutRef.current) {
clearTimeout(timeoutRef.current)
timeoutRef.current = null
}
timeoutRef.current = setTimeout(() => {
setIsCopied(false)
}, 2000)
})
.catch(() => {
toast.error("Failed to copy to clipboard.")
})
}, [text, copyMessage])
return { isCopied, handleCopy }
}