Spaces:
Running
Running
| import { useState, useRef } from "react"; | |
| import { ThumbsUp, ThumbsDown, Copy, Check, Loader2, Volume2, VolumeX } from "lucide-react"; | |
| import { replayAudio } from "../../../audio/AudioPlayer"; | |
| import { textToSpeech } from "../../../services/voiceApi"; | |
| interface FeedbackWidgetProps { | |
| messageId: string; | |
| content: string; | |
| audioText?: string; | |
| audioChunks?: ArrayBuffer[]; | |
| audioSampleRate?: number; | |
| } | |
| export default function FeedbackWidget({ | |
| content, | |
| audioText, | |
| audioChunks, | |
| audioSampleRate, | |
| }: FeedbackWidgetProps) { | |
| const [thumbsUp, setThumbsUp] = useState(false); | |
| const [thumbsDown, setThumbsDown] = useState(false); | |
| const [copyState, setCopyState] = useState<"idle" | "loading" | "success">("idle"); | |
| const [speakerState, setSpeakerState] = useState<"idle" | "loading" | "playing">("idle"); | |
| const cancelPlayRef = useRef<(() => void) | null>(null); | |
| const handleThumbsUp = () => { | |
| setThumbsUp((v) => !v); | |
| if (thumbsDown) setThumbsDown(false); | |
| }; | |
| const handleThumbsDown = () => { | |
| setThumbsDown((v) => !v); | |
| if (thumbsUp) setThumbsUp(false); | |
| }; | |
| const handleCopy = async () => { | |
| if (copyState !== "idle") return; | |
| setCopyState("loading"); | |
| try { | |
| await navigator.clipboard.writeText(content); | |
| setCopyState("success"); | |
| setTimeout(() => setCopyState("idle"), 2000); | |
| } catch { | |
| setCopyState("idle"); | |
| } | |
| }; | |
| const handleSpeaker = async () => { | |
| if (speakerState === "playing") { | |
| cancelPlayRef.current?.(); | |
| cancelPlayRef.current = null; | |
| setSpeakerState("idle"); | |
| return; | |
| } | |
| if (speakerState === "loading") return; | |
| if (audioChunks && audioChunks.length > 0) { | |
| // Voice mode: replay pre-stored audio immediately | |
| setSpeakerState("playing"); | |
| const cancel = replayAudio(audioChunks, audioSampleRate ?? 24000); | |
| cancelPlayRef.current = cancel; | |
| // Calculate approx duration to reset state | |
| const totalSamples = audioChunks.reduce((acc, c) => acc + c.byteLength / 2, 0); | |
| const duration = (totalSamples / (audioSampleRate ?? 24000)) * 1000; | |
| setTimeout(() => { | |
| cancelPlayRef.current = null; | |
| setSpeakerState("idle"); | |
| }, duration + 200); | |
| } else { | |
| // Text mode: request TTS now | |
| setSpeakerState("loading"); | |
| try { | |
| const { pcm, sampleRate } = await textToSpeech(audioText || content); | |
| setSpeakerState("playing"); | |
| const cancel = replayAudio([pcm], sampleRate); | |
| cancelPlayRef.current = cancel; | |
| const duration = (pcm.byteLength / 2 / sampleRate) * 1000; | |
| setTimeout(() => { | |
| cancelPlayRef.current = null; | |
| setSpeakerState("idle"); | |
| }, duration + 200); | |
| } catch { | |
| setSpeakerState("idle"); | |
| } | |
| } | |
| }; | |
| return ( | |
| <div className="flex items-center gap-1 ml-10 mt-1 px-4"> | |
| <button | |
| onClick={handleThumbsUp} | |
| className={`p-1 rounded transition-colors ${ | |
| thumbsUp ? "text-brand-green" : "text-neutral-300 hover:text-neutral-500" | |
| }`} | |
| aria-label="Thumbs up" | |
| > | |
| <ThumbsUp className="h-3.5 w-3.5" /> | |
| </button> | |
| <button | |
| onClick={handleThumbsDown} | |
| className={`p-1 rounded transition-colors ${ | |
| thumbsDown ? "text-red-400" : "text-neutral-300 hover:text-neutral-500" | |
| }`} | |
| aria-label="Thumbs down" | |
| > | |
| <ThumbsDown className="h-3.5 w-3.5" /> | |
| </button> | |
| <button | |
| onClick={handleCopy} | |
| className={`p-1 rounded transition-colors ${ | |
| copyState === "success" | |
| ? "text-brand-green" | |
| : "text-neutral-300 hover:text-neutral-500" | |
| }`} | |
| aria-label="Copy message" | |
| > | |
| {copyState === "loading" ? ( | |
| <Loader2 className="h-3.5 w-3.5 animate-spin text-neutral-400" /> | |
| ) : copyState === "success" ? ( | |
| <Check className="h-3.5 w-3.5" /> | |
| ) : ( | |
| <Copy className="h-3.5 w-3.5" /> | |
| )} | |
| </button> | |
| <button | |
| onClick={handleSpeaker} | |
| className={`p-1 rounded transition-colors ${ | |
| speakerState === "playing" | |
| ? "text-brand-green" | |
| : "text-neutral-300 hover:text-neutral-500" | |
| }`} | |
| aria-label="Play audio" | |
| > | |
| {speakerState === "loading" ? ( | |
| <Loader2 className="h-3.5 w-3.5 animate-spin text-neutral-400" /> | |
| ) : speakerState === "playing" ? ( | |
| <VolumeX className="h-3.5 w-3.5" /> | |
| ) : ( | |
| <Volume2 className="h-3.5 w-3.5" /> | |
| )} | |
| </button> | |
| </div> | |
| ); | |
| } | |