text.aicmadi Computer Traning
import { useState, useEffect } from 'react';
import { createRoot } from 'react-dom/client';

// Main application component
const App = () => {
  const [textToType, setTextToType] = useState("");
  const [userInput, setUserInput] = useState("");
  const [currentIndex, setCurrentIndex] = useState(0);
  const [isLoading, setIsLoading] = useState(true);
  const [timer, setTimer] = useState(60); // 60-second test
  const [testActive, setTestActive] = useState(false);
  const [results, setResults] = useState(null);

  // Gemini API key - will be filled by the environment
  const apiKey = "";
  const apiUrl = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-05-20:generateContent?key=${apiKey}`;

  // Function to fetch new text from the Gemini API
  const generateNewText = async () => {
    setIsLoading(true);
    const prompt = "Generate a paragraph of creative, engaging prose for a typing test. It should be at least 100 words long. Do not include any titles or special formatting, just the paragraph.";

    // API payload configuration
    const payload = {
      contents: [{
        role: "user",
        parts: [{ text: prompt }]
      }]
    };

    // Implement exponential backoff for API calls
    let retryCount = 0;
    const maxRetries = 5;
    const baseDelay = 1000;

    while (retryCount < maxRetries) {
      try {
        const response = await fetch(apiUrl, {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
          },
          body: JSON.stringify(payload),
        });

        if (!response.ok) {
          if (response.status === 429) {
            const delay = baseDelay * Math.pow(2, retryCount);
            console.log(`Rate limit exceeded. Retrying in ${delay / 1000}s...`);
            await new Promise(res => setTimeout(res, delay));
            retryCount++;
            continue;
          } else {
            throw new Error(`API call failed with status: ${response.status}`);
          }
        }

        const result = await response.json();
        const generatedText = result?.candidates?.[0]?.content?.parts?.[0]?.text;

        if (generatedText) {
          setTextToType(generatedText.trim());
          setIsLoading(false);
          return;
        } else {
          throw new Error("API response is missing generated text.");
        }
      } catch (error) {
        console.error("Error fetching new text:", error);
        setTextToType("Failed to load text. Please try again.");
        setIsLoading(false);
        return;
      }
    }

    console.error("Failed to fetch new text after multiple retries.");
    setTextToType("Failed to load text. Please try again.");
    setIsLoading(false);
  };

  // Function to start the typing test
  const startTest = () => {
    setUserInput("");
    setCurrentIndex(0);
    setTimer(60);
    setResults(null);
    setTestActive(true);
  };

  // Effect to load initial text and handle timer
  useEffect(() => {
    generateNewText();
    let interval;
    if (testActive) {
      interval = setInterval(() => {
        setTimer(prevTimer => {
          if (prevTimer <= 1) {
            clearInterval(interval);
            setTestActive(false);
            return 0;
          }
          return prevTimer - 1;
        });
      }, 1000);
    }
    return () => clearInterval(interval);
  }, [testActive]);

  // Effect to handle keyboard events
  useEffect(() => {
    if (testActive && !isLoading) {
      const handleKeyDown = (event) => {
        const key = event.key;
        
        // Prevent default browser behavior for space and backspace
        if (key === ' ' || key === 'Backspace') {
          event.preventDefault();
        }

        if (key === 'Backspace') {
          setUserInput(prevInput => prevInput.slice(0, -1));
          setCurrentIndex(prevIndex => Math.max(0, prevIndex - 1));
        } else if (key.length === 1 && currentIndex < textToType.length) {
          setUserInput(prevInput => prevInput + key);
          setCurrentIndex(prevIndex => prevIndex + 1);
        }
      };
      
      window.addEventListener('keydown', handleKeyDown);
      return () => {
        window.removeEventListener('keydown', handleKeyDown);
      };
    }
  }, [testActive, isLoading, currentIndex, textToType]);

  // Effect to calculate results when the test ends
  useEffect(() => {
    if (!testActive && timer === 0 && !isLoading) {
      let correctChars = 0;
      for (let i = 0; i < userInput.length; i++) {
        if (userInput[i] === textToType[i]) {
          correctChars++;
        }
      }

      const wordsTyped = userInput.split(/\s+/).filter(word => word !== "").length;
      const wpm = (correctChars / 5) * (60 / 60); // (chars / 5) / minutes
      const accuracy = userInput.length > 0 ? (correctChars / userInput.length) * 100 : 0;

      setResults({
        wpm: Math.round(wpm),
        accuracy: accuracy.toFixed(2)
      });
    }
  }, [testActive, timer, isLoading, userInput, textToType]);

  // Helper function to render each character with appropriate styling
  const renderCharacters = () => {
    return textToType.split('').map((char, index) => {
      let charClass = "transition-colors duration-100 ";
      if (index < userInput.length) {
        if (userInput[index] === char) {
          charClass += "text-green-500 font-bold";
        } else {
          charClass += "text-red-500 font-bold underline decoration-red-500";
        }
      } else if (index === userInput.length && testActive) {
        charClass += "text-blue-500 underline font-extrabold";
      } else {
        charClass += "text-gray-400";
      }
      return (
        <span key={index} className={charClass}>
          {char}
        </span>
      );
    });
  };

  return (
    <div className="flex flex-col items-center justify-center min-h-screen bg-gray-900 text-white p-4 font-inter">
      <div className="bg-gray-800 p-8 rounded-xl shadow-lg w-full max-w-4xl text-center">
        <h1 className="text-4xl font-bold mb-6 text-indigo-400">Typing Test</h1>
        <div className="flex justify-between items-center mb-4">
          <div className="text-lg font-bold">
            Time: <span className="text-yellow-400">{timer}s</span>
          </div>
          <button
            onClick={startTest}
            disabled={isLoading || testActive}
            className="bg-indigo-600 hover:bg-indigo-700 disabled:opacity-50 text-white font-bold py-2 px-4 rounded-lg transition-colors duration-200 shadow-md transform hover:scale-105 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 focus:ring-offset-gray-900"
          >
            {isLoading ? "Loading..." : testActive ? "Typing..." : "Start Test"}
          </button>
        </div>
        <div className="mb-6 h-40 overflow-y-auto p-4 border border-gray-700 rounded-lg text-left text-xl leading-relaxed">
          {isLoading ? (
            <p className="text-gray-400 animate-pulse">Loading new text...</p>
          ) : (
            renderCharacters()
          )}
        </div>
        {results && (
          <div className="mt-6 p-4 bg-gray-700 rounded-lg shadow-md">
            <h2 className="text-2xl font-bold mb-2">Results</h2>
            <div className="flex justify-around">
              <div className="flex flex-col items-center">
                <span className="text-4xl font-extrabold text-green-400">{results.wpm}</span>
                <span className="text-gray-400">WPM</span>
              </div>
              <div className="flex flex-col items-center">
                <span className="text-4xl font-extrabold text-teal-400">{results.accuracy}%</span>
                <span className="text-gray-400">Accuracy</span>
              </div>
            </div>
          </div>
        )}
      </div>
    </div>
  );
};

export default App;

Scroll to Top