this is tsx for astro shadcnui:
// 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 isThinkingRef = useRef(isThinking);
  const isTalkingRef = useRef(isTalking);

  const recognitionRef = useRef<SpeechRecognition | 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;

    // Clear any existing content
    while (visualizerRef.current.firstChild) {
      visualizerRef.current.removeChild(visualizerRef.current.firstChild);
    }

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

    const width = visualizerRef.current.clientWidth;
    const height = visualizerRef.current.clientHeight;

    const camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000);
    camera.position.z = 5;

    const renderer = new THREE.WebGLRenderer({ antialias: true });
    renderer.setSize(width, height);
    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 (isThinkingRef.current) {
          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 && isTalkingRef.current && 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
    const handleResize = () => {
      if (!cameraRef.current || !rendererRef.current || !visualizerRef.current)
        return;
      const width = visualizerRef.current.clientWidth;
      const height = visualizerRef.current.clientHeight;
      cameraRef.current.aspect = width / height;
      cameraRef.current.updateProjectionMatrix();
      rendererRef.current.setSize(width, height);
    };

    window.addEventListener("resize", handleResize);

    // Clean up on unmount
    return () => {
      window.removeEventListener("resize", handleResize);
      renderer.dispose();
    };
  };

  useEffect(() => {
    initVisualizer();
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  // Update refs when isThinking or isTalking changes
  useEffect(() => {
    isThinkingRef.current = isThinking;
  }, [isThinking]);

  useEffect(() => {
    isTalkingRef.current = isTalking;
  }, [isTalking]);

  // Set up the audio analyser
  useEffect(() => {
    if (audioRef.current && !analyser) {
      const listener = new THREE.AudioListener();
      const audio = new THREE.Audio(listener);
      audio.setMediaElementSource(audioRef.current);
      const newAnalyser = new THREE.AudioAnalyser(audio, 32);
      setAnalyser(newAnalyser);
    }
  }, [analyser]);

  // Voice Recognition
  useEffect(() => {
    if (
      !("webkitSpeechRecognition" in window) &&
      !("SpeechRecognition" in window)
    ) {
      console.error("Speech recognition not supported in this browser.");
      return;
    }

    if (!recognitionRef.current) {
      const SpeechRecognitionClass =
        (window as any).SpeechRecognition ||
        (window as any).webkitSpeechRecognition;
      recognitionRef.current = new SpeechRecognitionClass();
      recognitionRef.current.continuous = true;
      recognitionRef.current.interimResults = false;
      recognitionRef.current.lang = "ar-SA";

      recognitionRef.current.onresult = (event: SpeechRecognitionEvent) => {
        const transcript = Array.from(event.results)
          .map((result) => result[0].transcript)
          .join("");

        console.log("Recognized speech:", transcript);

        setInput(transcript);
        handleSendMessage();
      };

      recognitionRef.current.onerror = (event: SpeechRecognitionErrorEvent) => {
        console.error("Speech recognition error:", event.error);
      };

      recognitionRef.current.onend = () => {
        if (isListening) {
          recognitionRef.current?.start();
        }
      };
    }

    if (isListening) {
      recognitionRef.current.start();
    } else {
      recognitionRef.current.stop();
    }

    // Clean up on unmount
    return () => {
      recognitionRef.current?.stop();
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [isListening]);

  return (
    <div className="flex flex-col items-center h-screen overflow-hidden">
      {/* Audio Visualizer */}
      <div
        ref={visualizerRef}
        className="w-full flex-grow bg-background overflow-hidden"
      ></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">ملهم:</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;


add animations and make it send automatcliy what user said