Spaces:
Running
Running
File size: 4,606 Bytes
c0ddd13 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 | 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>
);
}
|