profileShare

rasmusjy / voicetask

Read-only snapshot

No repository description.

main default branch 105 files Expires Sep 13, 2026, 9:06 AM

Commit

Add animated bot avatar component

commit 2bce30f

1 changed file with +57 and −0

added client/src/components/VoiceOrb.tsx +57 −0
@@ -0,0 +1,57 @@
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 +}