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>
  );
}