Frontend-Data-Eyond / src /app /components /chat /FeedbackWidget.tsx
ishaq101's picture
[NOTICKET] Major update, re-stylign and upgrade using maintiva demo setup
c0ddd13
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>
);
}