tsx file for astro shadcnui0
// src/components/VoiceAssistant.tsx
import React, { useState, useRef, useEffect } from "react";
import {
  CardFooter,
  CardDescription,
  CardContent,
  CardHeader,
  Card,
} from "./ui/card";
import { Button } from "./ui/button";
import { Input } from "./ui/input";
import {
  PlayIcon,
  CircleStop,
  MessageCircle,
  XIcon,
} from "lucide-react";
import * as THREE from "three";

const api = "http://localhost:5000"; // Update this if deploying
const bgcolor = new THREE.Color().setHex(0x11161f);
const visualcolor = new THREE.Color().setHex(0x1d92c4);

const VoiceAssistant: React.FC = () => {
  const [chatVisible, setChatVisible] = useState(false);
  const [isListening, setIsListening] = useState(false);
  const [isThinking, setIsThinking] = useState(false);
  const [isTalking, setIsTalking] = useState(false);
  const [messages, setMessages] = useState<
    { user: string; bot: string }[]
  >([]);
  const [input, setInput] = useState("");
  const audioRef = useRef<HTMLAudioElement>(null);
  const [analyser, setAnalyser] = useState<THREE.AudioAnalyser | null>(
    null
  );
  const visualizerRef = useRef<HTMLDivElement>(null);

  const sceneRef = useRef<THREE.Scene | null>(null);
  const cameraRef = useRef<THREE.PerspectiveCamera | null>(null);
  const rendererRef = useRef<THREE.WebGLRenderer | null>(null);
  const sphereRef = useRef<THREE.Mesh | null>(null);
  const particlesRef = useRef<THREE.Points | null>(null);

  const toggleChat = () => setChatVisible(!chatVisible);

  // Handle sending a message to the chat API
  const handleSendMessage = async () => {
    if (!input.trim()) return;

    setIsThinking(true); // Start thinking animation

    try {
      const response = await fetch(`${api}/chat`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ message: input }),
      });

      const data = await response.json();
      const botResponse = data.response;

      setMessages((prev) => [...prev, { user: input, bot: botResponse }]);
      setInput("");
      await playBotResponse(botResponse);
    } catch (error) {
      console.error("Error fetching chat response:", error);
    } finally {
      setIsThinking(false); // Stop thinking animation
    }
  };

  // Play bot response using text-to-speech API
  const playBotResponse = async (text: string) => {
    try {
      const response = await fetch(`${api}/text-to-speech`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ text }),
      });

      const audioBlob = await response.blob();
      const audioUrl = URL.createObjectURL(audioBlob);

      if (audioRef.current) {
        audioRef.current.src = audioUrl;

        audioRef.current.onplay = () => {
          setIsTalking(true);
        };

        audioRef.current.onended = () => {
          setIsTalking(false);
        };

        audioRef.current.play();
      }
    } catch (error) {
      console.error("Error fetching TTS response:", error);
    }
  };

  // Setup Three.js Visualizer
  const initVisualizer = () => {
    if (!visualizerRef.current) return;

    const scene = new THREE.Scene();
    scene.background = bgcolor;

    const camera = new THREE.PerspectiveCamera(
      75,
      window.innerWidth / window.innerHeight,
      0.1,
      1000
    );
    camera.position.z = 5;

    const renderer = new THREE.WebGLRenderer({ antialias: true });
    renderer.setSize(window.innerWidth, window.innerHeight);
    visualizerRef.current.appendChild(renderer.domElement);

    // Add dynamic lighting
    const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
    scene.add(ambientLight);

    const pointLight = new THREE.PointLight(0x1d92c4, 1);
    pointLight.position.set(5, 5, 5);
    scene.add(pointLight);

    // Sphere Geometry
    const geometry = new THREE.SphereGeometry(1.5, 32, 32);
    const material = new THREE.MeshStandardMaterial({ color: visualcolor });
    const sphere = new THREE.Mesh(geometry, material);
    scene.add(sphere);
    sphereRef.current = sphere;

    // Particle System for "Thinking" state
    const particlesGeometry = new THREE.BufferGeometry();
    const particlesCount = 500;
    const positions = new Float32Array(particlesCount * 3);

    for (let i = 0; i < particlesCount; i++) {
      positions[i * 3] = (Math.random() - 0.5) * 10;
      positions[i * 3 + 1] = (Math.random() - 0.5) * 10;
      positions[i * 3 + 2] = (Math.random() - 0.5) * 10;
    }

    particlesGeometry.setAttribute(
      "position",
      new THREE.BufferAttribute(positions, 3)
    );

    const particlesMaterial = new THREE.PointsMaterial({
      color: 0x1d92c4,
      size: 0.05,
    });

    const particles = new THREE.Points(particlesGeometry, particlesMaterial);
    scene.add(particles);
    particlesRef.current = particles;

    // Store references
    sceneRef.current = scene;
    cameraRef.current = camera;
    rendererRef.current = renderer;

    const animate = () => {
      requestAnimationFrame(animate);

      // Rotate the sphere
      if (sphereRef.current) {
        sphereRef.current.rotation.y += 0.003;
        sphereRef.current.rotation.x += 0.005;
      }

      // Update particles if thinking
      if (particlesRef.current) {
        if (isThinking) {
          particlesRef.current.rotation.y += 0.01;
          (particlesRef.current.material as THREE.PointsMaterial).color.set(
            0xff0000
          ); // Change color when thinking
        } else {
          particlesRef.current.rotation.y += 0.002;
          (particlesRef.current.material as THREE.PointsMaterial).color.set(
            0x1d92c4
          ); // Default color
        }
      }

      // Update sphere scale based on audio data when talking
      if (analyser && isTalking && sphereRef.current) {
        const data = analyser.getFrequencyData();
        const scale = 1 + data[0] / 256;
        sphereRef.current.scale.set(scale, scale, scale);
      } else if (sphereRef.current) {
        // Reset scale when not talking
        sphereRef.current.scale.set(1, 1, 1);
      }

      renderer.render(scene, camera);
    };

    animate();

    // Handle window resize
    window.addEventListener("resize", () => {
      if (!cameraRef.current || !rendererRef.current) return;
      cameraRef.current.aspect = window.innerWidth / window.innerHeight;
      cameraRef.current.updateProjectionMatrix();
      rendererRef.current.setSize(window.innerWidth, window.innerHeight);
    });
  };

  useEffect(() => {
    initVisualizer();
  }, [isThinking, isTalking, analyser]);

  useEffect(() => {
    if (audioRef.current && analyser) {
      const listener = new THREE.AudioListener();
      const audio = new THREE.Audio(listener);
      audio.setMediaElementSource(audioRef.current);
      analyser.setFrequencyBinCount(32);
    }
  }, [analyser]);

  return (
    <div className="flex flex-col items-center h-screen">
      {/* Audio Visualizer */}
      <div
        ref={visualizerRef}
        className="w-full h-3/4 bg-background"
      ></div>

      {/* Chat Interface Fixed at the Bottom */}
      {chatVisible && (
        <Card className="fixed bottom-0 w-full max-w-lg mx-auto bg-background shadow-lg border-t border-t-border">
          <CardHeader className="text-xl font-bold p-4">
            AI Chat Assistant
          </CardHeader>
          <CardContent className="p-4">
            <div className="h-40 overflow-y-auto p-2 bg-background rounded-md">
              {messages.map((msg, index) => (
                <div key={index} className="mb-4">
                  <p className="font-semibold text-blue-500">User:</p>
                  <p>{msg.user}</p>
                  <p className="font-semibold text-green-500 mt-2">Bot:</p>
                  <p>{msg.bot}</p>
                </div>
              ))}
            </div>
            <div className="flex mt-4">
              <Input
                placeholder="Type a message..."
                value={input}
                onChange={(e) => setInput(e.target.value)}
                onKeyDown={(e) =>
                  e.key === "Enter" && handleSendMessage()
                }
              />
              <Button
                onClick={handleSendMessage}
                className="ml-2 text-foreground"
              >
                Send
              </Button>
            </div>
          </CardContent>
        </Card>
      )}

      {/* Control Buttons at the Bottom */}
      {!chatVisible && (
        <div className="fixed bottom-4 w-full flex justify-center space-x-4">
          <Button
            onClick={() => setIsListening(!isListening)}
            variant={isListening ? "destructive" : "default"}
            className={isListening ? undefined : "text-foreground"}
          >
            {isListening ? (
              <>
                <CircleStop className="mr-2" /> Stop
              </>
            ) : (
              <>
                <PlayIcon className="mr-2" /> Start
              </>
            )}
          </Button>
        </div>
      )}

      {/* Chat Toggle Button */}
      <div className="fixed bottom-16 w-full flex justify-center">
        <Button onClick={toggleChat} variant="secondary">
          {chatVisible ? (
            <>
              <XIcon className="mr-2" /> Hide Chat
            </>
          ) : (
            <>
              <MessageCircle className="mr-2" /> Show Chat
            </>
          )}
        </Button>
      </div>

      {/* Audio Element for TTS */}
      <audio ref={audioRef} />
    </div>
  );
};

export default VoiceAssistant;

fix issue when animation change i need to scroll to see new animation, and add voice recognition, start button start live chat and stop button stop live chat