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";
import { motion, AnimatePresence } from "framer-motion";

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 (message: string) => {
    if (!message.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 }),
      });

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

      setMessages((prev) => [...prev, { user: message, 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, alpha: 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

          // Add pulsating effect
          particlesRef.current.scale.setScalar(
            1 + Math.sin(Date.now() * 0.005) * 0.1
          );
        } else {
          particlesRef.current.rotation.y += 0.002;
          (particlesRef.current.material as THREE.PointsMaterial).color.set(
            0x1d92c4
          ); // Default color
          particlesRef.current.scale.setScalar(1);
        }
      }

      // 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 = false; // Set to false to stop automatically
      recognitionRef.current.interimResults = false;
      recognitionRef.current.lang = "ar-SA"; // Change to your desired language

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

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

        // Automatically send the recognized speech
        handleSendMessage(transcript);
      };

      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 relative">
      {/* Audio Visualizer */}
      <div
        ref={visualizerRef}
        className="w-full flex-grow bg-background overflow-hidden"
      ></div>

      {/* Chat Interface with Animations */}
      <AnimatePresence>
        {chatVisible && (
          <motion.div
            initial={{ y: "100%", opacity: 0 }}
            animate={{ y: 0, opacity: 1 }}
            exit={{ y: "100%", opacity: 0 }}
            transition={{ duration: 0.5 }}
            className="fixed bottom-20 w-full max-w-lg mx-auto z-20" // Added bottom-20 and z-20
          >
            <Card className="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 animate-fadeIn"
                      // Using Tailwind CSS for a simple fade-in animation
                    >
                      <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(input)
                    }
                    disabled={isListening} // Disable manual input while listening
                  />
                  <Button
                    onClick={() => handleSendMessage(input)}
                    className="ml-2 text-foreground"
                    disabled={isListening} // Disable manual send while listening
                  >
                    Send
                  </Button>
                </div>
              </CardContent>
            </Card>
          </motion.div>
        )}
      </AnimatePresence>

      {/* Control Buttons with Animations */}
      <div className="fixed bottom-4 w-full flex justify-center space-x-4 z-10">
        {/* Added z-10 to ensure buttons are below the chat (z-20) */}
        <motion.div
          whileHover={{ scale: 1.1 }}
          whileTap={{ scale: 0.95 }}
          transition={{ type: "spring", stiffness: 300 }}
        >
          <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>
        </motion.div>

        <motion.div
          whileHover={{ scale: 1.1 }}
          whileTap={{ scale: 0.95 }}
          transition={{ type: "spring", stiffness: 300 }}
        >
          <Button
            onClick={toggleChat}
            variant="secondary"
            className="flex items-center"
          >
            {chatVisible ? (
              <>
                <XIcon className="mr-2" /> Hide Chat
              </>
            ) : (
              <>
                <MessageCircle className="mr-2" /> Show Chat
              </>
            )}
          </Button>
        </motion.div>
      </div>

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

export default VoiceAssistant;

The AudioContext was not allowed to start. It must be resumed (or created) after a user gesture on the page. https://goo.gl/7K7WLu