profileShare

rasmusjy / voicetask

Read-only snapshot

No repository description.

main default branch 105 files Expires Sep 13, 2026, 9:06 AM
VoiceOrb.tsx 1,664 bytes
1 import { useEffect, useRef } from 'react'
2
3 export type VoiceState = 'idle' | 'listening' | 'thinking' | 'speaking'
4
5 interface VoiceOrbProps {
6 state: VoiceState
7 levelRef?: React.RefObject<number>
8 }
9
10 export function VoiceOrb({ state, levelRef }: VoiceOrbProps) {
11 const ringRef = useRef<HTMLDivElement>(null)
12
13 useEffect(() => {
14 if (state !== 'listening' || !levelRef) return
15 let rafId = 0
16 const tick = () => {
17 const ring = ringRef.current
18 if (ring) {
19 const level = levelRef.current ?? 0
20 ring.style.transform = `scale(${1 + level * 0.3})`
21 ring.style.opacity = String(0.25 + level * 0.6)
22 }
23 rafId = requestAnimationFrame(tick)
24 }
25 rafId = requestAnimationFrame(tick)
26 return () => {
27 cancelAnimationFrame(rafId)
28 const ring = ringRef.current
29 if (ring) {
30 ring.style.transform = ''
31 ring.style.opacity = ''
32 }
33 }
34 }, [state, levelRef])
35
36 return (
37 <div className={`orb-wrap orb-${state}`} aria-hidden="true">
38 <div ref={ringRef} className="orb-level-ring" />
39 <div className="orb-ripple orb-ripple-1" />
40 <div className="orb-ripple orb-ripple-2" />
41 <div className="orb">
42 <div className="orb-face">
43 <div className="orb-eyes">
44 <span className="orb-eye" />
45 <span className="orb-eye" />
46 </div>
47 <div className="orb-mouth">
48 <span className="orb-mouth-bar" />
49 <span className="orb-mouth-bar" />
50 <span className="orb-mouth-bar" />
51 <span className="orb-mouth-bar" />
52 </div>
53 </div>
54 </div>
55 </div>
56 )
57 }
58