Felix Zieger
		
	commited on
		
		
					Commit 
							
							·
						
						fe9a0c4
	
1
								Parent(s):
							
							1b620c7
								
v2
Browse files- index.html +3 -3
- public/favicon.ico +0 -0
- public/og-image.png +0 -0
- src/components/GameContainer.tsx +65 -32
- src/components/HighScoreBoard.tsx +82 -19
- src/components/game/GameOver.tsx +12 -0
- src/components/game/ThemeSelector.tsx +146 -0
- src/services/themeService.ts +26 -0
- supabase/config.toml +3 -1
- supabase/functions/generate-themed-word/index.ts +70 -0
    	
        index.html
    CHANGED
    
    | @@ -3,9 +3,9 @@ | |
| 3 | 
             
              <head>
         | 
| 4 | 
             
                <meta charset="UTF-8" />
         | 
| 5 | 
             
                <meta name="viewport" content="width=device-width, initial-scale=1.0" />
         | 
| 6 | 
            -
                <title> | 
| 7 | 
            -
                <meta name="description" content=" | 
| 8 | 
            -
                <meta name="author" content=" | 
| 9 | 
             
                <meta property="og:image" content="/og-image.png" />
         | 
| 10 | 
             
              </head>
         | 
| 11 |  | 
|  | |
| 3 | 
             
              <head>
         | 
| 4 | 
             
                <meta charset="UTF-8" />
         | 
| 5 | 
             
                <meta name="viewport" content="width=device-width, initial-scale=1.0" />
         | 
| 6 | 
            +
                <title>Think in Sync</title>
         | 
| 7 | 
            +
                <meta name="description" content="A word puzzle game." />
         | 
| 8 | 
            +
                <meta name="author" content="Team M1X" />
         | 
| 9 | 
             
                <meta property="og:image" content="/og-image.png" />
         | 
| 10 | 
             
              </head>
         | 
| 11 |  | 
    	
        public/favicon.ico
    CHANGED
    
    |  | 
|  | 
    	
        public/og-image.png
    CHANGED
    
    |   | 
|   | 
    	
        src/components/GameContainer.tsx
    CHANGED
    
    | @@ -2,24 +2,27 @@ import { useState, KeyboardEvent, useEffect } from "react"; | |
| 2 | 
             
            import { getRandomWord } from "@/lib/words";
         | 
| 3 | 
             
            import { motion } from "framer-motion";
         | 
| 4 | 
             
            import { generateAIResponse, guessWord } from "@/services/mistralService";
         | 
|  | |
| 5 | 
             
            import { useToast } from "@/components/ui/use-toast";
         | 
| 6 | 
             
            import { WelcomeScreen } from "./game/WelcomeScreen";
         | 
| 7 | 
            -
            import {  | 
| 8 | 
             
            import { SentenceBuilder } from "./game/SentenceBuilder";
         | 
| 9 | 
             
            import { GuessDisplay } from "./game/GuessDisplay";
         | 
| 10 | 
             
            import { GameOver } from "./game/GameOver";
         | 
| 11 |  | 
| 12 | 
            -
            type GameState = "welcome" | " | 
| 13 |  | 
| 14 | 
             
            export const GameContainer = () => {
         | 
| 15 | 
             
              const [gameState, setGameState] = useState<GameState>("welcome");
         | 
| 16 | 
             
              const [currentWord, setCurrentWord] = useState<string>("");
         | 
|  | |
| 17 | 
             
              const [sentence, setSentence] = useState<string[]>([]);
         | 
| 18 | 
             
              const [playerInput, setPlayerInput] = useState<string>("");
         | 
| 19 | 
             
              const [isAiThinking, setIsAiThinking] = useState(false);
         | 
| 20 | 
             
              const [aiGuess, setAiGuess] = useState<string>("");
         | 
| 21 | 
             
              const [successfulRounds, setSuccessfulRounds] = useState<number>(0);
         | 
| 22 | 
             
              const [totalWords, setTotalWords] = useState<number>(0);
         | 
|  | |
| 23 | 
             
              const { toast } = useToast();
         | 
| 24 |  | 
| 25 | 
             
              useEffect(() => {
         | 
| @@ -27,8 +30,6 @@ export const GameContainer = () => { | |
| 27 | 
             
                  if (e.key === 'Enter') {
         | 
| 28 | 
             
                    if (gameState === 'welcome') {
         | 
| 29 | 
             
                      handleStart();
         | 
| 30 | 
            -
                    } else if (gameState === 'showing-word') {
         | 
| 31 | 
            -
                      handleContinue();
         | 
| 32 | 
             
                    } else if (gameState === 'game-over' || gameState === 'showing-guess') {
         | 
| 33 | 
             
                      const correct = isGuessCorrect();
         | 
| 34 | 
             
                      if (correct) {
         | 
| @@ -45,12 +46,27 @@ export const GameContainer = () => { | |
| 45 | 
             
              }, [gameState, aiGuess, currentWord]);
         | 
| 46 |  | 
| 47 | 
             
              const handleStart = () => {
         | 
| 48 | 
            -
                 | 
| 49 | 
            -
             | 
| 50 | 
            -
             | 
| 51 | 
            -
             | 
| 52 | 
            -
                 | 
| 53 | 
            -
                 | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 54 | 
             
              };
         | 
| 55 |  | 
| 56 | 
             
              const handlePlayerWord = async (e: React.FormEvent) => {
         | 
| @@ -82,12 +98,21 @@ export const GameContainer = () => { | |
| 82 | 
             
              };
         | 
| 83 |  | 
| 84 | 
             
              const handleMakeGuess = async () => {
         | 
| 85 | 
            -
                if (sentence.length === 0) return;
         | 
| 86 | 
            -
             | 
| 87 | 
             
                setIsAiThinking(true);
         | 
| 88 | 
             
                try {
         | 
| 89 | 
            -
                   | 
| 90 | 
            -
                   | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 91 | 
             
                  setAiGuess(guess);
         | 
| 92 | 
             
                  setGameState("showing-guess");
         | 
| 93 | 
             
                } catch (error) {
         | 
| @@ -104,12 +129,27 @@ export const GameContainer = () => { | |
| 104 |  | 
| 105 | 
             
              const handleNextRound = () => {
         | 
| 106 | 
             
                if (handleGuessComplete()) {
         | 
| 107 | 
            -
                  const  | 
| 108 | 
            -
             | 
| 109 | 
            -
             | 
| 110 | 
            -
             | 
| 111 | 
            -
             | 
| 112 | 
            -
             | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 113 | 
             
                } else {
         | 
| 114 | 
             
                  setGameState("game-over");
         | 
| 115 | 
             
                }
         | 
| @@ -120,13 +160,10 @@ export const GameContainer = () => { | |
| 120 | 
             
                setSentence([]);
         | 
| 121 | 
             
                setAiGuess("");
         | 
| 122 | 
             
                setCurrentWord("");
         | 
|  | |
| 123 | 
             
                setSuccessfulRounds(0);
         | 
| 124 | 
             
                setTotalWords(0);
         | 
| 125 | 
            -
             | 
| 126 | 
            -
             | 
| 127 | 
            -
              const handleContinue = () => {
         | 
| 128 | 
            -
                setGameState("building-sentence");
         | 
| 129 | 
            -
                setSentence([]);
         | 
| 130 | 
             
              };
         | 
| 131 |  | 
| 132 | 
             
              const isGuessCorrect = () => {
         | 
| @@ -143,7 +180,7 @@ export const GameContainer = () => { | |
| 143 |  | 
| 144 | 
             
              const getAverageWordsPerRound = () => {
         | 
| 145 | 
             
                if (successfulRounds === 0) return 0;
         | 
| 146 | 
            -
                return totalWords / (successfulRounds + 1); | 
| 147 | 
             
              };
         | 
| 148 |  | 
| 149 | 
             
              return (
         | 
| @@ -155,12 +192,8 @@ export const GameContainer = () => { | |
| 155 | 
             
                  >
         | 
| 156 | 
             
                    {gameState === "welcome" ? (
         | 
| 157 | 
             
                      <WelcomeScreen onStart={handleStart} />
         | 
| 158 | 
            -
                    ) : gameState === " | 
| 159 | 
            -
                      < | 
| 160 | 
            -
                        currentWord={currentWord}
         | 
| 161 | 
            -
                        successfulRounds={successfulRounds}
         | 
| 162 | 
            -
                        onContinue={handleContinue}
         | 
| 163 | 
            -
                      />
         | 
| 164 | 
             
                    ) : gameState === "building-sentence" ? (
         | 
| 165 | 
             
                      <SentenceBuilder
         | 
| 166 | 
             
                        currentWord={currentWord}
         | 
|  | |
| 2 | 
             
            import { getRandomWord } from "@/lib/words";
         | 
| 3 | 
             
            import { motion } from "framer-motion";
         | 
| 4 | 
             
            import { generateAIResponse, guessWord } from "@/services/mistralService";
         | 
| 5 | 
            +
            import { getThemedWord } from "@/services/themeService";
         | 
| 6 | 
             
            import { useToast } from "@/components/ui/use-toast";
         | 
| 7 | 
             
            import { WelcomeScreen } from "./game/WelcomeScreen";
         | 
| 8 | 
            +
            import { ThemeSelector } from "./game/ThemeSelector";
         | 
| 9 | 
             
            import { SentenceBuilder } from "./game/SentenceBuilder";
         | 
| 10 | 
             
            import { GuessDisplay } from "./game/GuessDisplay";
         | 
| 11 | 
             
            import { GameOver } from "./game/GameOver";
         | 
| 12 |  | 
| 13 | 
            +
            type GameState = "welcome" | "theme-selection" | "building-sentence" | "showing-guess" | "game-over";
         | 
| 14 |  | 
| 15 | 
             
            export const GameContainer = () => {
         | 
| 16 | 
             
              const [gameState, setGameState] = useState<GameState>("welcome");
         | 
| 17 | 
             
              const [currentWord, setCurrentWord] = useState<string>("");
         | 
| 18 | 
            +
              const [currentTheme, setCurrentTheme] = useState<string>("standard");
         | 
| 19 | 
             
              const [sentence, setSentence] = useState<string[]>([]);
         | 
| 20 | 
             
              const [playerInput, setPlayerInput] = useState<string>("");
         | 
| 21 | 
             
              const [isAiThinking, setIsAiThinking] = useState(false);
         | 
| 22 | 
             
              const [aiGuess, setAiGuess] = useState<string>("");
         | 
| 23 | 
             
              const [successfulRounds, setSuccessfulRounds] = useState<number>(0);
         | 
| 24 | 
             
              const [totalWords, setTotalWords] = useState<number>(0);
         | 
| 25 | 
            +
              const [usedWords, setUsedWords] = useState<string[]>([]);
         | 
| 26 | 
             
              const { toast } = useToast();
         | 
| 27 |  | 
| 28 | 
             
              useEffect(() => {
         | 
|  | |
| 30 | 
             
                  if (e.key === 'Enter') {
         | 
| 31 | 
             
                    if (gameState === 'welcome') {
         | 
| 32 | 
             
                      handleStart();
         | 
|  | |
|  | |
| 33 | 
             
                    } else if (gameState === 'game-over' || gameState === 'showing-guess') {
         | 
| 34 | 
             
                      const correct = isGuessCorrect();
         | 
| 35 | 
             
                      if (correct) {
         | 
|  | |
| 46 | 
             
              }, [gameState, aiGuess, currentWord]);
         | 
| 47 |  | 
| 48 | 
             
              const handleStart = () => {
         | 
| 49 | 
            +
                setGameState("theme-selection");
         | 
| 50 | 
            +
              };
         | 
| 51 | 
            +
             | 
| 52 | 
            +
              const handleThemeSelect = async (theme: string) => {
         | 
| 53 | 
            +
                setCurrentTheme(theme);
         | 
| 54 | 
            +
                try {
         | 
| 55 | 
            +
                  const word = theme === "standard" ? getRandomWord() : await getThemedWord(theme, usedWords);
         | 
| 56 | 
            +
                  setCurrentWord(word);
         | 
| 57 | 
            +
                  setGameState("building-sentence");
         | 
| 58 | 
            +
                  setSuccessfulRounds(0);
         | 
| 59 | 
            +
                  setTotalWords(0);
         | 
| 60 | 
            +
                  setUsedWords([word]); // Initialize used words with the first word
         | 
| 61 | 
            +
                  console.log("Game started with word:", word, "theme:", theme);
         | 
| 62 | 
            +
                } catch (error) {
         | 
| 63 | 
            +
                  console.error('Error getting themed word:', error);
         | 
| 64 | 
            +
                  toast({
         | 
| 65 | 
            +
                    title: "Error",
         | 
| 66 | 
            +
                    description: "Failed to get a word for the selected theme. Please try again.",
         | 
| 67 | 
            +
                    variant: "destructive",
         | 
| 68 | 
            +
                  });
         | 
| 69 | 
            +
                }
         | 
| 70 | 
             
              };
         | 
| 71 |  | 
| 72 | 
             
              const handlePlayerWord = async (e: React.FormEvent) => {
         | 
|  | |
| 98 | 
             
              };
         | 
| 99 |  | 
| 100 | 
             
              const handleMakeGuess = async () => {
         | 
|  | |
|  | |
| 101 | 
             
                setIsAiThinking(true);
         | 
| 102 | 
             
                try {
         | 
| 103 | 
            +
                  // Add the current input to the sentence if it exists
         | 
| 104 | 
            +
                  let finalSentence = sentence;
         | 
| 105 | 
            +
                  if (playerInput.trim()) {
         | 
| 106 | 
            +
                    finalSentence = [...sentence, playerInput.trim()];
         | 
| 107 | 
            +
                    setSentence(finalSentence);
         | 
| 108 | 
            +
                    setPlayerInput("");
         | 
| 109 | 
            +
                    setTotalWords(prev => prev + 1);
         | 
| 110 | 
            +
                  }
         | 
| 111 | 
            +
             | 
| 112 | 
            +
                  if (finalSentence.length === 0) return;
         | 
| 113 | 
            +
             | 
| 114 | 
            +
                  const sentenceString = finalSentence.join(' ');
         | 
| 115 | 
            +
                  const guess = await guessWord(sentenceString);
         | 
| 116 | 
             
                  setAiGuess(guess);
         | 
| 117 | 
             
                  setGameState("showing-guess");
         | 
| 118 | 
             
                } catch (error) {
         | 
|  | |
| 129 |  | 
| 130 | 
             
              const handleNextRound = () => {
         | 
| 131 | 
             
                if (handleGuessComplete()) {
         | 
| 132 | 
            +
                  const getNewWord = async () => {
         | 
| 133 | 
            +
                    try {
         | 
| 134 | 
            +
                      const word = currentTheme === "standard" ? 
         | 
| 135 | 
            +
                        getRandomWord() : 
         | 
| 136 | 
            +
                        await getThemedWord(currentTheme, usedWords);
         | 
| 137 | 
            +
                      setCurrentWord(word);
         | 
| 138 | 
            +
                      setGameState("building-sentence");
         | 
| 139 | 
            +
                      setSentence([]);
         | 
| 140 | 
            +
                      setAiGuess("");
         | 
| 141 | 
            +
                      setUsedWords(prev => [...prev, word]); // Add new word to used words
         | 
| 142 | 
            +
                      console.log("Next round started with word:", word, "theme:", currentTheme);
         | 
| 143 | 
            +
                    } catch (error) {
         | 
| 144 | 
            +
                      console.error('Error getting new word:', error);
         | 
| 145 | 
            +
                      toast({
         | 
| 146 | 
            +
                        title: "Error",
         | 
| 147 | 
            +
                        description: "Failed to get a new word. Please try again.",
         | 
| 148 | 
            +
                        variant: "destructive",
         | 
| 149 | 
            +
                      });
         | 
| 150 | 
            +
                    }
         | 
| 151 | 
            +
                  };
         | 
| 152 | 
            +
                  getNewWord();
         | 
| 153 | 
             
                } else {
         | 
| 154 | 
             
                  setGameState("game-over");
         | 
| 155 | 
             
                }
         | 
|  | |
| 160 | 
             
                setSentence([]);
         | 
| 161 | 
             
                setAiGuess("");
         | 
| 162 | 
             
                setCurrentWord("");
         | 
| 163 | 
            +
                setCurrentTheme("standard");
         | 
| 164 | 
             
                setSuccessfulRounds(0);
         | 
| 165 | 
             
                setTotalWords(0);
         | 
| 166 | 
            +
                setUsedWords([]); // Reset used words when starting over
         | 
|  | |
|  | |
|  | |
|  | |
| 167 | 
             
              };
         | 
| 168 |  | 
| 169 | 
             
              const isGuessCorrect = () => {
         | 
|  | |
| 180 |  | 
| 181 | 
             
              const getAverageWordsPerRound = () => {
         | 
| 182 | 
             
                if (successfulRounds === 0) return 0;
         | 
| 183 | 
            +
                return totalWords / (successfulRounds + 1);
         | 
| 184 | 
             
              };
         | 
| 185 |  | 
| 186 | 
             
              return (
         | 
|  | |
| 192 | 
             
                  >
         | 
| 193 | 
             
                    {gameState === "welcome" ? (
         | 
| 194 | 
             
                      <WelcomeScreen onStart={handleStart} />
         | 
| 195 | 
            +
                    ) : gameState === "theme-selection" ? (
         | 
| 196 | 
            +
                      <ThemeSelector onThemeSelect={handleThemeSelect} />
         | 
|  | |
|  | |
|  | |
|  | |
| 197 | 
             
                    ) : gameState === "building-sentence" ? (
         | 
| 198 | 
             
                      <SentenceBuilder
         | 
| 199 | 
             
                        currentWord={currentWord}
         | 
    	
        src/components/HighScoreBoard.tsx
    CHANGED
    
    | @@ -1,4 +1,4 @@ | |
| 1 | 
            -
            import { useState } from "react";
         | 
| 2 | 
             
            import { Button } from "@/components/ui/button";
         | 
| 3 | 
             
            import { Input } from "@/components/ui/input";
         | 
| 4 | 
             
            import { supabase } from "@/integrations/supabase/client";
         | 
| @@ -36,7 +36,8 @@ interface HighScoreBoardProps { | |
| 36 | 
             
              onPlayAgain: () => void;
         | 
| 37 | 
             
            }
         | 
| 38 |  | 
| 39 | 
            -
            const ITEMS_PER_PAGE =  | 
|  | |
| 40 |  | 
| 41 | 
             
            const getRankMedal = (rank: number) => {
         | 
| 42 | 
             
              switch (rank) {
         | 
| @@ -55,7 +56,6 @@ export const HighScoreBoard = ({ | |
| 55 | 
             
              currentScore,
         | 
| 56 | 
             
              avgWordsPerRound,
         | 
| 57 | 
             
              onClose,
         | 
| 58 | 
            -
              onPlayAgain,
         | 
| 59 | 
             
            }: HighScoreBoardProps) => {
         | 
| 60 | 
             
              const [playerName, setPlayerName] = useState("");
         | 
| 61 | 
             
              const [isSubmitting, setIsSubmitting] = useState(false);
         | 
| @@ -107,18 +107,55 @@ export const HighScoreBoard = ({ | |
| 107 |  | 
| 108 | 
             
                setIsSubmitting(true);
         | 
| 109 | 
             
                try {
         | 
| 110 | 
            -
                   | 
| 111 | 
            -
             | 
| 112 | 
            -
                     | 
| 113 | 
            -
                     | 
| 114 | 
            -
             | 
| 115 |  | 
| 116 | 
            -
                   | 
| 117 |  | 
| 118 | 
            -
                   | 
| 119 | 
            -
                     | 
| 120 | 
            -
                     | 
| 121 | 
            -
             | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 122 |  | 
| 123 | 
             
                  setHasSubmitted(true);
         | 
| 124 | 
             
                  await refetch();
         | 
| @@ -135,7 +172,14 @@ export const HighScoreBoard = ({ | |
| 135 | 
             
                }
         | 
| 136 | 
             
              };
         | 
| 137 |  | 
| 138 | 
            -
              const  | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 139 | 
             
              const startIndex = (currentPage - 1) * ITEMS_PER_PAGE;
         | 
| 140 | 
             
              const paginatedScores = highScores?.slice(startIndex, startIndex + ITEMS_PER_PAGE);
         | 
| 141 |  | 
| @@ -151,6 +195,19 @@ export const HighScoreBoard = ({ | |
| 151 | 
             
                }
         | 
| 152 | 
             
              };
         | 
| 153 |  | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 154 | 
             
              return (
         | 
| 155 | 
             
                <div className="space-y-6">
         | 
| 156 | 
             
                  <div className="text-center">
         | 
| @@ -167,6 +224,7 @@ export const HighScoreBoard = ({ | |
| 167 | 
             
                        placeholder="Enter your name"
         | 
| 168 | 
             
                        value={playerName}
         | 
| 169 | 
             
                        onChange={(e) => setPlayerName(e.target.value)}
         | 
|  | |
| 170 | 
             
                        className="flex-1"
         | 
| 171 | 
             
                      />
         | 
| 172 | 
             
                      <Button
         | 
| @@ -222,7 +280,10 @@ export const HighScoreBoard = ({ | |
| 222 | 
             
                          <PaginationPrevious 
         | 
| 223 | 
             
                            onClick={handlePreviousPage}
         | 
| 224 | 
             
                            className={currentPage === 1 ? "pointer-events-none opacity-50" : ""}
         | 
| 225 | 
            -
                           | 
|  | |
|  | |
|  | |
| 226 | 
             
                        </PaginationItem>
         | 
| 227 | 
             
                        {Array.from({ length: totalPages }, (_, i) => i + 1).map((page) => (
         | 
| 228 | 
             
                          <PaginationItem key={page}>
         | 
| @@ -238,17 +299,19 @@ export const HighScoreBoard = ({ | |
| 238 | 
             
                          <PaginationNext
         | 
| 239 | 
             
                            onClick={handleNextPage}
         | 
| 240 | 
             
                            className={currentPage === totalPages ? "pointer-events-none opacity-50" : ""}
         | 
| 241 | 
            -
                           | 
|  | |
|  | |
|  | |
| 242 | 
             
                        </PaginationItem>
         | 
| 243 | 
             
                      </PaginationContent>
         | 
| 244 | 
             
                    </Pagination>
         | 
| 245 | 
             
                  )}
         | 
| 246 |  | 
| 247 | 
            -
                  <div className="flex justify-end | 
| 248 | 
             
                    <Button variant="outline" onClick={onClose}>
         | 
| 249 | 
            -
                      Close
         | 
| 250 | 
             
                    </Button>
         | 
| 251 | 
            -
                    <Button onClick={onPlayAgain}>Play Again</Button>
         | 
| 252 | 
             
                  </div>
         | 
| 253 | 
             
                </div>
         | 
| 254 | 
             
              );
         | 
|  | |
| 1 | 
            +
            import { useEffect, useState } from "react";
         | 
| 2 | 
             
            import { Button } from "@/components/ui/button";
         | 
| 3 | 
             
            import { Input } from "@/components/ui/input";
         | 
| 4 | 
             
            import { supabase } from "@/integrations/supabase/client";
         | 
|  | |
| 36 | 
             
              onPlayAgain: () => void;
         | 
| 37 | 
             
            }
         | 
| 38 |  | 
| 39 | 
            +
            const ITEMS_PER_PAGE = 5;
         | 
| 40 | 
            +
            const MAX_PAGES = 4;
         | 
| 41 |  | 
| 42 | 
             
            const getRankMedal = (rank: number) => {
         | 
| 43 | 
             
              switch (rank) {
         | 
|  | |
| 56 | 
             
              currentScore,
         | 
| 57 | 
             
              avgWordsPerRound,
         | 
| 58 | 
             
              onClose,
         | 
|  | |
| 59 | 
             
            }: HighScoreBoardProps) => {
         | 
| 60 | 
             
              const [playerName, setPlayerName] = useState("");
         | 
| 61 | 
             
              const [isSubmitting, setIsSubmitting] = useState(false);
         | 
|  | |
| 107 |  | 
| 108 | 
             
                setIsSubmitting(true);
         | 
| 109 | 
             
                try {
         | 
| 110 | 
            +
                  // Check if player already exists
         | 
| 111 | 
            +
                  const { data: existingScores } = await supabase
         | 
| 112 | 
            +
                    .from("high_scores")
         | 
| 113 | 
            +
                    .select("*")
         | 
| 114 | 
            +
                    .eq("player_name", playerName.trim());
         | 
| 115 |  | 
| 116 | 
            +
                  const existingScore = existingScores?.[0];
         | 
| 117 |  | 
| 118 | 
            +
                  if (existingScore) {
         | 
| 119 | 
            +
                    // Only update if the new score is better
         | 
| 120 | 
            +
                    if (currentScore > existingScore.score) {
         | 
| 121 | 
            +
                      const { error } = await supabase
         | 
| 122 | 
            +
                        .from("high_scores")
         | 
| 123 | 
            +
                        .update({
         | 
| 124 | 
            +
                          score: currentScore,
         | 
| 125 | 
            +
                          avg_words_per_round: avgWordsPerRound,
         | 
| 126 | 
            +
                        })
         | 
| 127 | 
            +
                        .eq("id", existingScore.id);
         | 
| 128 | 
            +
             | 
| 129 | 
            +
                      if (error) throw error;
         | 
| 130 | 
            +
             | 
| 131 | 
            +
                      toast({
         | 
| 132 | 
            +
                        title: "New High Score!",
         | 
| 133 | 
            +
                        description: `You beat your previous record of ${existingScore.score} rounds!`,
         | 
| 134 | 
            +
                      });
         | 
| 135 | 
            +
                    } else {
         | 
| 136 | 
            +
                      toast({
         | 
| 137 | 
            +
                        title: "Score Not Updated",
         | 
| 138 | 
            +
                        description: `Your current score (${currentScore}) is not higher than your best score (${existingScore.score})`,
         | 
| 139 | 
            +
                        variant: "destructive",
         | 
| 140 | 
            +
                      });
         | 
| 141 | 
            +
                      setIsSubmitting(false);
         | 
| 142 | 
            +
                      return;
         | 
| 143 | 
            +
                    }
         | 
| 144 | 
            +
                  } else {
         | 
| 145 | 
            +
                    // Insert new score
         | 
| 146 | 
            +
                    const { error } = await supabase.from("high_scores").insert({
         | 
| 147 | 
            +
                      player_name: playerName.trim(),
         | 
| 148 | 
            +
                      score: currentScore,
         | 
| 149 | 
            +
                      avg_words_per_round: avgWordsPerRound,
         | 
| 150 | 
            +
                    });
         | 
| 151 | 
            +
             | 
| 152 | 
            +
                    if (error) throw error;
         | 
| 153 | 
            +
             | 
| 154 | 
            +
                    toast({
         | 
| 155 | 
            +
                      title: "Success!",
         | 
| 156 | 
            +
                      description: "Your score has been recorded",
         | 
| 157 | 
            +
                    });
         | 
| 158 | 
            +
                  }
         | 
| 159 |  | 
| 160 | 
             
                  setHasSubmitted(true);
         | 
| 161 | 
             
                  await refetch();
         | 
|  | |
| 172 | 
             
                }
         | 
| 173 | 
             
              };
         | 
| 174 |  | 
| 175 | 
            +
              const handleKeyDown = async (e: React.KeyboardEvent<HTMLInputElement>) => {
         | 
| 176 | 
            +
                if (e.key === 'Enter') {
         | 
| 177 | 
            +
                  e.preventDefault();
         | 
| 178 | 
            +
                  await handleSubmitScore();
         | 
| 179 | 
            +
                }
         | 
| 180 | 
            +
              };
         | 
| 181 | 
            +
             | 
| 182 | 
            +
              const totalPages = highScores ? Math.min(Math.ceil(highScores.length / ITEMS_PER_PAGE), MAX_PAGES) : 0;
         | 
| 183 | 
             
              const startIndex = (currentPage - 1) * ITEMS_PER_PAGE;
         | 
| 184 | 
             
              const paginatedScores = highScores?.slice(startIndex, startIndex + ITEMS_PER_PAGE);
         | 
| 185 |  | 
|  | |
| 195 | 
             
                }
         | 
| 196 | 
             
              };
         | 
| 197 |  | 
| 198 | 
            +
              useEffect(() => {
         | 
| 199 | 
            +
                const handleKeyDown = (e: KeyboardEvent) => {
         | 
| 200 | 
            +
                  if (e.key === 'ArrowLeft') {
         | 
| 201 | 
            +
                    handlePreviousPage();
         | 
| 202 | 
            +
                  } else if (e.key === 'ArrowRight') {
         | 
| 203 | 
            +
                    handleNextPage();
         | 
| 204 | 
            +
                  }
         | 
| 205 | 
            +
                };
         | 
| 206 | 
            +
             | 
| 207 | 
            +
                window.addEventListener('keydown', handleKeyDown);
         | 
| 208 | 
            +
                return () => window.removeEventListener('keydown', handleKeyDown);
         | 
| 209 | 
            +
              }, [currentPage, totalPages]);
         | 
| 210 | 
            +
             | 
| 211 | 
             
              return (
         | 
| 212 | 
             
                <div className="space-y-6">
         | 
| 213 | 
             
                  <div className="text-center">
         | 
|  | |
| 224 | 
             
                        placeholder="Enter your name"
         | 
| 225 | 
             
                        value={playerName}
         | 
| 226 | 
             
                        onChange={(e) => setPlayerName(e.target.value)}
         | 
| 227 | 
            +
                        onKeyDown={handleKeyDown}
         | 
| 228 | 
             
                        className="flex-1"
         | 
| 229 | 
             
                      />
         | 
| 230 | 
             
                      <Button
         | 
|  | |
| 280 | 
             
                          <PaginationPrevious 
         | 
| 281 | 
             
                            onClick={handlePreviousPage}
         | 
| 282 | 
             
                            className={currentPage === 1 ? "pointer-events-none opacity-50" : ""}
         | 
| 283 | 
            +
                          >
         | 
| 284 | 
            +
                            <span className="hidden sm:inline">Previous</span>
         | 
| 285 | 
            +
                            <span className="text-xs text-muted-foreground ml-1">←</span>
         | 
| 286 | 
            +
                          </PaginationPrevious>
         | 
| 287 | 
             
                        </PaginationItem>
         | 
| 288 | 
             
                        {Array.from({ length: totalPages }, (_, i) => i + 1).map((page) => (
         | 
| 289 | 
             
                          <PaginationItem key={page}>
         | 
|  | |
| 299 | 
             
                          <PaginationNext
         | 
| 300 | 
             
                            onClick={handleNextPage}
         | 
| 301 | 
             
                            className={currentPage === totalPages ? "pointer-events-none opacity-50" : ""}
         | 
| 302 | 
            +
                          >
         | 
| 303 | 
            +
                            <span className="hidden sm:inline">Next</span>
         | 
| 304 | 
            +
                            <span className="text-xs text-muted-foreground ml-1">→</span>
         | 
| 305 | 
            +
                          </PaginationNext>
         | 
| 306 | 
             
                        </PaginationItem>
         | 
| 307 | 
             
                      </PaginationContent>
         | 
| 308 | 
             
                    </Pagination>
         | 
| 309 | 
             
                  )}
         | 
| 310 |  | 
| 311 | 
            +
                  <div className="flex justify-end">
         | 
| 312 | 
             
                    <Button variant="outline" onClick={onClose}>
         | 
| 313 | 
            +
                      Close <span className="text-xs text-muted-foreground ml-1">Esc</span>
         | 
| 314 | 
             
                    </Button>
         | 
|  | |
| 315 | 
             
                  </div>
         | 
| 316 | 
             
                </div>
         | 
| 317 | 
             
              );
         | 
    	
        src/components/game/GameOver.tsx
    CHANGED
    
    | @@ -1,5 +1,6 @@ | |
| 1 | 
             
            import { Button } from "@/components/ui/button";
         | 
| 2 | 
             
            import { motion } from "framer-motion";
         | 
|  | |
| 3 |  | 
| 4 | 
             
            interface GameOverProps {
         | 
| 5 | 
             
              successfulRounds: number;
         | 
| @@ -10,6 +11,17 @@ export const GameOver = ({ | |
| 10 | 
             
              successfulRounds,
         | 
| 11 | 
             
              onPlayAgain,
         | 
| 12 | 
             
            }: GameOverProps) => {
         | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 13 | 
             
              return (
         | 
| 14 | 
             
                <motion.div
         | 
| 15 | 
             
                  initial={{ opacity: 0 }}
         | 
|  | |
| 1 | 
             
            import { Button } from "@/components/ui/button";
         | 
| 2 | 
             
            import { motion } from "framer-motion";
         | 
| 3 | 
            +
            import { useEffect } from "react";
         | 
| 4 |  | 
| 5 | 
             
            interface GameOverProps {
         | 
| 6 | 
             
              successfulRounds: number;
         | 
|  | |
| 11 | 
             
              successfulRounds,
         | 
| 12 | 
             
              onPlayAgain,
         | 
| 13 | 
             
            }: GameOverProps) => {
         | 
| 14 | 
            +
              useEffect(() => {
         | 
| 15 | 
            +
                const handleKeyPress = (e: KeyboardEvent) => {
         | 
| 16 | 
            +
                  if (e.key.toLowerCase() === 'enter') {
         | 
| 17 | 
            +
                    onPlayAgain();
         | 
| 18 | 
            +
                  }
         | 
| 19 | 
            +
                };
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                window.addEventListener('keydown', handleKeyPress);
         | 
| 22 | 
            +
                return () => window.removeEventListener('keydown', handleKeyPress);
         | 
| 23 | 
            +
              }, [onPlayAgain]);
         | 
| 24 | 
            +
             | 
| 25 | 
             
              return (
         | 
| 26 | 
             
                <motion.div
         | 
| 27 | 
             
                  initial={{ opacity: 0 }}
         | 
    	
        src/components/game/ThemeSelector.tsx
    ADDED
    
    | @@ -0,0 +1,146 @@ | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | 
|  | |
| 1 | 
            +
            import { useState, useEffect, useRef } from "react";
         | 
| 2 | 
            +
            import { Button } from "@/components/ui/button";
         | 
| 3 | 
            +
            import { Input } from "@/components/ui/input";
         | 
| 4 | 
            +
            import { motion } from "framer-motion";
         | 
| 5 | 
            +
             | 
| 6 | 
            +
            type Theme = "standard" | "sports" | "food" | "custom";
         | 
| 7 | 
            +
             | 
| 8 | 
            +
            interface ThemeSelectorProps {
         | 
| 9 | 
            +
              onThemeSelect: (theme: string) => void;
         | 
| 10 | 
            +
            }
         | 
| 11 | 
            +
             | 
| 12 | 
            +
            export const ThemeSelector = ({ onThemeSelect }: ThemeSelectorProps) => {
         | 
| 13 | 
            +
              const [selectedTheme, setSelectedTheme] = useState<Theme>("standard");
         | 
| 14 | 
            +
              const [customTheme, setCustomTheme] = useState("");
         | 
| 15 | 
            +
              const [isGenerating, setIsGenerating] = useState(false);
         | 
| 16 | 
            +
              const inputRef = useRef<HTMLInputElement>(null);
         | 
| 17 | 
            +
             | 
| 18 | 
            +
              useEffect(() => {
         | 
| 19 | 
            +
                const handleKeyPress = (e: KeyboardEvent) => {
         | 
| 20 | 
            +
                  if (e.target instanceof HTMLInputElement) return; // Ignore when typing in input
         | 
| 21 | 
            +
                  
         | 
| 22 | 
            +
                  switch(e.key.toLowerCase()) {
         | 
| 23 | 
            +
                    case 'a':
         | 
| 24 | 
            +
                      setSelectedTheme("standard");
         | 
| 25 | 
            +
                      break;
         | 
| 26 | 
            +
                    case 'b':
         | 
| 27 | 
            +
                      setSelectedTheme("sports");
         | 
| 28 | 
            +
                      break;
         | 
| 29 | 
            +
                    case 'c':
         | 
| 30 | 
            +
                      setSelectedTheme("food");
         | 
| 31 | 
            +
                      break;
         | 
| 32 | 
            +
                    case 'd':
         | 
| 33 | 
            +
                      e.preventDefault(); // Prevent 'd' from being entered in the input
         | 
| 34 | 
            +
                      setSelectedTheme("custom");
         | 
| 35 | 
            +
                      break;
         | 
| 36 | 
            +
                    case 'enter':
         | 
| 37 | 
            +
                      if (selectedTheme !== "custom" || customTheme.trim()) {
         | 
| 38 | 
            +
                        handleSubmit();
         | 
| 39 | 
            +
                      }
         | 
| 40 | 
            +
                      break;
         | 
| 41 | 
            +
                  }
         | 
| 42 | 
            +
                };
         | 
| 43 | 
            +
             | 
| 44 | 
            +
                window.addEventListener('keydown', handleKeyPress);
         | 
| 45 | 
            +
                return () => window.removeEventListener('keydown', handleKeyPress);
         | 
| 46 | 
            +
              }, [selectedTheme, customTheme]);
         | 
| 47 | 
            +
             | 
| 48 | 
            +
              useEffect(() => {
         | 
| 49 | 
            +
                if (selectedTheme === "custom") {
         | 
| 50 | 
            +
                  setTimeout(() => {
         | 
| 51 | 
            +
                    inputRef.current?.focus();
         | 
| 52 | 
            +
                  }, 100);
         | 
| 53 | 
            +
                }
         | 
| 54 | 
            +
              }, [selectedTheme]);
         | 
| 55 | 
            +
             | 
| 56 | 
            +
              const handleSubmit = async () => {
         | 
| 57 | 
            +
                if (selectedTheme === "custom" && !customTheme.trim()) return;
         | 
| 58 | 
            +
                
         | 
| 59 | 
            +
                setIsGenerating(true);
         | 
| 60 | 
            +
                try {
         | 
| 61 | 
            +
                  await onThemeSelect(selectedTheme === "custom" ? customTheme : selectedTheme);
         | 
| 62 | 
            +
                } finally {
         | 
| 63 | 
            +
                  setIsGenerating(false);
         | 
| 64 | 
            +
                }
         | 
| 65 | 
            +
              };
         | 
| 66 | 
            +
             | 
| 67 | 
            +
              const handleInputKeyPress = (e: React.KeyboardEvent<HTMLInputElement>) => {
         | 
| 68 | 
            +
                if (e.key === 'Enter' && customTheme.trim()) {
         | 
| 69 | 
            +
                  handleSubmit();
         | 
| 70 | 
            +
                }
         | 
| 71 | 
            +
              };
         | 
| 72 | 
            +
             | 
| 73 | 
            +
              return (
         | 
| 74 | 
            +
                <motion.div
         | 
| 75 | 
            +
                  initial={{ opacity: 0 }}
         | 
| 76 | 
            +
                  animate={{ opacity: 1 }}
         | 
| 77 | 
            +
                  className="space-y-6"
         | 
| 78 | 
            +
                >
         | 
| 79 | 
            +
                  <div className="text-center space-y-2">
         | 
| 80 | 
            +
                    <h2 className="text-2xl font-bold text-gray-900">Choose a Theme</h2>
         | 
| 81 | 
            +
                    <p className="text-gray-600">Select a theme for your word-guessing adventure</p>
         | 
| 82 | 
            +
                  </div>
         | 
| 83 | 
            +
             | 
| 84 | 
            +
                  <div className="space-y-4">
         | 
| 85 | 
            +
                    <Button
         | 
| 86 | 
            +
                      variant={selectedTheme === "standard" ? "default" : "outline"}
         | 
| 87 | 
            +
                      className="w-full justify-between"
         | 
| 88 | 
            +
                      onClick={() => setSelectedTheme("standard")}
         | 
| 89 | 
            +
                    >
         | 
| 90 | 
            +
                      Standard <span className="text-sm opacity-50">Press A</span>
         | 
| 91 | 
            +
                    </Button>
         | 
| 92 | 
            +
                    
         | 
| 93 | 
            +
                    <Button
         | 
| 94 | 
            +
                      variant={selectedTheme === "sports" ? "default" : "outline"}
         | 
| 95 | 
            +
                      className="w-full justify-between"
         | 
| 96 | 
            +
                      onClick={() => setSelectedTheme("sports")}
         | 
| 97 | 
            +
                    >
         | 
| 98 | 
            +
                      Sports <span className="text-sm opacity-50">Press B</span>
         | 
| 99 | 
            +
                    </Button>
         | 
| 100 | 
            +
                    
         | 
| 101 | 
            +
                    <Button
         | 
| 102 | 
            +
                      variant={selectedTheme === "food" ? "default" : "outline"}
         | 
| 103 | 
            +
                      className="w-full justify-between"
         | 
| 104 | 
            +
                      onClick={() => setSelectedTheme("food")}
         | 
| 105 | 
            +
                    >
         | 
| 106 | 
            +
                      Food <span className="text-sm opacity-50">Press C</span>
         | 
| 107 | 
            +
                    </Button>
         | 
| 108 | 
            +
             | 
| 109 | 
            +
                    <Button
         | 
| 110 | 
            +
                      variant={selectedTheme === "custom" ? "default" : "outline"}
         | 
| 111 | 
            +
                      className="w-full justify-between"
         | 
| 112 | 
            +
                      onClick={() => setSelectedTheme("custom")}
         | 
| 113 | 
            +
                    >
         | 
| 114 | 
            +
                      Choose your theme <span className="text-sm opacity-50">Press D</span>
         | 
| 115 | 
            +
                    </Button>
         | 
| 116 | 
            +
             | 
| 117 | 
            +
                    {selectedTheme === "custom" && (
         | 
| 118 | 
            +
                      <motion.div
         | 
| 119 | 
            +
                        initial={{ opacity: 0, height: 0 }}
         | 
| 120 | 
            +
                        animate={{ opacity: 1, height: "auto" }}
         | 
| 121 | 
            +
                        exit={{ opacity: 0, height: 0 }}
         | 
| 122 | 
            +
                        transition={{ duration: 0.2 }}
         | 
| 123 | 
            +
                      >
         | 
| 124 | 
            +
                        <Input
         | 
| 125 | 
            +
                          ref={inputRef}
         | 
| 126 | 
            +
                          type="text"
         | 
| 127 | 
            +
                          placeholder="Enter a theme (e.g., Animals, Movies)"
         | 
| 128 | 
            +
                          value={customTheme}
         | 
| 129 | 
            +
                          onChange={(e) => setCustomTheme(e.target.value)}
         | 
| 130 | 
            +
                          onKeyPress={handleInputKeyPress}
         | 
| 131 | 
            +
                          className="w-full"
         | 
| 132 | 
            +
                        />
         | 
| 133 | 
            +
                      </motion.div>
         | 
| 134 | 
            +
                    )}
         | 
| 135 | 
            +
                  </div>
         | 
| 136 | 
            +
             | 
| 137 | 
            +
                  <Button
         | 
| 138 | 
            +
                    onClick={handleSubmit}
         | 
| 139 | 
            +
                    className="w-full"
         | 
| 140 | 
            +
                    disabled={selectedTheme === "custom" && !customTheme.trim() || isGenerating}
         | 
| 141 | 
            +
                  >
         | 
| 142 | 
            +
                    {isGenerating ? "Generating themed words..." : "Continue ⏎"}
         | 
| 143 | 
            +
                  </Button>
         | 
| 144 | 
            +
                </motion.div>
         | 
| 145 | 
            +
              );
         | 
| 146 | 
            +
            };
         | 
    	
        src/services/themeService.ts
    ADDED
    
    | @@ -0,0 +1,26 @@ | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | 
|  | |
| 1 | 
            +
            import { supabase } from "@/integrations/supabase/client";
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            export const getThemedWord = async (theme: string, usedWords: string[] = []): Promise<string> => {
         | 
| 4 | 
            +
              if (theme === "standard") {
         | 
| 5 | 
            +
                throw new Error("Standard theme should use the words list");
         | 
| 6 | 
            +
              }
         | 
| 7 | 
            +
             | 
| 8 | 
            +
              console.log('Getting themed word for:', theme, 'excluding:', usedWords);
         | 
| 9 | 
            +
              
         | 
| 10 | 
            +
              const { data, error } = await supabase.functions.invoke('generate-themed-word', {
         | 
| 11 | 
            +
                body: { theme, usedWords }
         | 
| 12 | 
            +
              });
         | 
| 13 | 
            +
             | 
| 14 | 
            +
              if (error) {
         | 
| 15 | 
            +
                console.error('Error generating themed word:', error);
         | 
| 16 | 
            +
                throw error;
         | 
| 17 | 
            +
              }
         | 
| 18 | 
            +
             | 
| 19 | 
            +
              if (!data?.word) {
         | 
| 20 | 
            +
                console.error('No word generated in response:', data);
         | 
| 21 | 
            +
                throw new Error('No word generated');
         | 
| 22 | 
            +
              }
         | 
| 23 | 
            +
             | 
| 24 | 
            +
              console.log('Generated themed word:', data.word);
         | 
| 25 | 
            +
              return data.word;
         | 
| 26 | 
            +
            };
         | 
    	
        supabase/config.toml
    CHANGED
    
    | @@ -4,4 +4,6 @@ enabled = true | |
| 4 | 
             
            [analytics]
         | 
| 5 | 
             
            enabled = false
         | 
| 6 | 
             
            [realtime]
         | 
| 7 | 
            -
            enabled = false
         | 
|  | |
|  | 
|  | |
| 4 | 
             
            [analytics]
         | 
| 5 | 
             
            enabled = false
         | 
| 6 | 
             
            [realtime]
         | 
| 7 | 
            +
            enabled = false
         | 
| 8 | 
            +
            [functions.generate-themed-word]
         | 
| 9 | 
            +
            verify_jwt = false
         | 
    	
        supabase/functions/generate-themed-word/index.ts
    ADDED
    
    | @@ -0,0 +1,70 @@ | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | 
|  | |
| 1 | 
            +
            import "https://deno.land/x/[email protected]/mod.ts";
         | 
| 2 | 
            +
            import { serve } from "https://deno.land/[email protected]/http/server.ts";
         | 
| 3 | 
            +
            import { Mistral } from 'npm:@mistralai/mistralai';
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            const corsHeaders = {
         | 
| 6 | 
            +
              'Access-Control-Allow-Origin': '*',
         | 
| 7 | 
            +
              'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
         | 
| 8 | 
            +
            };
         | 
| 9 | 
            +
             | 
| 10 | 
            +
            serve(async (req) => {
         | 
| 11 | 
            +
              if (req.method === 'OPTIONS') {
         | 
| 12 | 
            +
                return new Response(null, { headers: corsHeaders });
         | 
| 13 | 
            +
              }
         | 
| 14 | 
            +
             | 
| 15 | 
            +
              try {
         | 
| 16 | 
            +
                const { theme, usedWords = [] } = await req.json();
         | 
| 17 | 
            +
                console.log('Generating word for theme:', theme, 'excluding:', usedWords);
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                const client = new Mistral({
         | 
| 20 | 
            +
                  apiKey: Deno.env.get('MISTRAL_API_KEY'),
         | 
| 21 | 
            +
                });
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                const response = await client.chat.complete({
         | 
| 24 | 
            +
                  model: "mistral-large-latest",
         | 
| 25 | 
            +
                  messages: [
         | 
| 26 | 
            +
                    {
         | 
| 27 | 
            +
                      role: "system",
         | 
| 28 | 
            +
                      content: `You are helping generate words for a word-guessing game. Generate a single word related to the theme "${theme}". 
         | 
| 29 | 
            +
                      The word should be:
         | 
| 30 | 
            +
                      - A single word (no spaces or hyphens)
         | 
| 31 | 
            +
                      - Common enough that people would know it
         | 
| 32 | 
            +
                      - Specific enough to be interesting
         | 
| 33 | 
            +
                      - Related to the theme "${theme}"
         | 
| 34 | 
            +
                      - Between 4 and 12 letters
         | 
| 35 | 
            +
                      - A noun
         | 
| 36 | 
            +
                      - NOT be any of these previously used words: ${usedWords.join(', ')}
         | 
| 37 | 
            +
                      
         | 
| 38 | 
            +
                      Respond with just the word in UPPERCASE, nothing else.`
         | 
| 39 | 
            +
                    }
         | 
| 40 | 
            +
                  ],
         | 
| 41 | 
            +
                  maxTokens: 10,
         | 
| 42 | 
            +
                  temperature: 0.9
         | 
| 43 | 
            +
                });
         | 
| 44 | 
            +
             | 
| 45 | 
            +
                const word = response.choices[0].message.content.trim();
         | 
| 46 | 
            +
                console.log('Generated word:', word);
         | 
| 47 | 
            +
             | 
| 48 | 
            +
                return new Response(
         | 
| 49 | 
            +
                  JSON.stringify({ word }),
         | 
| 50 | 
            +
                  { 
         | 
| 51 | 
            +
                    headers: { 
         | 
| 52 | 
            +
                      ...corsHeaders,
         | 
| 53 | 
            +
                      'Content-Type': 'application/json'
         | 
| 54 | 
            +
                    }
         | 
| 55 | 
            +
                  }
         | 
| 56 | 
            +
                );
         | 
| 57 | 
            +
              } catch (error) {
         | 
| 58 | 
            +
                console.error('Error generating themed word:', error);
         | 
| 59 | 
            +
                return new Response(
         | 
| 60 | 
            +
                  JSON.stringify({ error: error.message }),
         | 
| 61 | 
            +
                  { 
         | 
| 62 | 
            +
                    status: 500,
         | 
| 63 | 
            +
                    headers: { 
         | 
| 64 | 
            +
                      ...corsHeaders,
         | 
| 65 | 
            +
                      'Content-Type': 'application/json'
         | 
| 66 | 
            +
                    }
         | 
| 67 | 
            +
                  }
         | 
| 68 | 
            +
                );
         | 
| 69 | 
            +
              }
         | 
| 70 | 
            +
            });
         | 
