Felix Zieger commited on
Commit
8725cc4
·
1 Parent(s): ff438c3

app update

Browse files
src/components/GameContainer.tsx CHANGED
@@ -1,12 +1,16 @@
1
- import { useState } from "react";
2
  import { getRandomWord } from "@/lib/words";
3
- import { Button } from "@/components/ui/button";
4
- import { Input } from "@/components/ui/input";
5
  import { motion } from "framer-motion";
6
- import { generateAIResponse, guessWord, validateSentence } from "@/services/mistralService";
7
  import { useToast } from "@/components/ui/use-toast";
 
 
 
 
 
 
8
 
9
- type GameState = "welcome" | "showing-word" | "building-sentence" | "showing-guess" | "game-over";
10
 
11
  export const GameContainer = () => {
12
  const [gameState, setGameState] = useState<GameState>("welcome");
@@ -15,12 +19,38 @@ export const GameContainer = () => {
15
  const [playerInput, setPlayerInput] = useState<string>("");
16
  const [isAiThinking, setIsAiThinking] = useState(false);
17
  const [aiGuess, setAiGuess] = useState<string>("");
 
 
18
  const { toast } = useToast();
19
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
  const handleStart = () => {
21
  const word = getRandomWord();
22
  setCurrentWord(word);
23
  setGameState("showing-word");
 
 
24
  console.log("Game started with word:", word);
25
  };
26
 
@@ -32,86 +62,63 @@ export const GameContainer = () => {
32
  const newSentence = [...sentence, word];
33
  setSentence(newSentence);
34
  setPlayerInput("");
35
-
36
- // Check if the sentence is complete (ends with a period)
37
- if (word.endsWith('.')) {
38
- setIsAiThinking(true);
39
- try {
40
- const finalSentence = newSentence.join(' ');
41
-
42
- // Validate the sentence
43
- const isValid = await validateSentence(finalSentence);
44
- if (!isValid) {
45
- toast({
46
- title: "Invalid Sentence",
47
- description: "The sentence is not grammatically correct. Game Over!",
48
- variant: "destructive",
49
- });
50
- setGameState("game-over");
51
- setIsAiThinking(false);
52
- return;
53
- }
54
-
55
- const guess = await guessWord(finalSentence);
56
- setAiGuess(guess);
57
- setGameState("showing-guess");
58
- } catch (error) {
59
- console.error('Error getting AI guess:', error);
60
- toast({
61
- title: "Error",
62
- description: "Failed to get AI's guess. Please try again.",
63
- variant: "destructive",
64
- });
65
- } finally {
66
- setIsAiThinking(false);
67
- }
68
- return;
69
- }
70
 
71
  setIsAiThinking(true);
72
  try {
73
  const aiWord = await generateAIResponse(currentWord, newSentence);
74
  const newSentenceWithAi = [...newSentence, aiWord];
75
  setSentence(newSentenceWithAi);
 
 
 
 
 
 
 
 
 
 
 
 
76
 
77
- // Check if AI ended the sentence
78
- if (aiWord.endsWith('.')) {
79
- const finalSentence = newSentenceWithAi.join(' ');
80
-
81
- // Validate the sentence
82
- const isValid = await validateSentence(finalSentence);
83
- if (!isValid) {
84
- toast({
85
- title: "Invalid Sentence",
86
- description: "The AI generated an invalid sentence. Game Over!",
87
- variant: "destructive",
88
- });
89
- setGameState("game-over");
90
- setIsAiThinking(false);
91
- return;
92
- }
93
 
94
- const guess = await guessWord(finalSentence);
95
- setAiGuess(guess);
96
- setGameState("showing-guess");
97
- }
 
 
98
  } catch (error) {
99
- console.error('Error in AI turn:', error);
100
  toast({
101
- title: "Error",
102
- description: "Failed to get AI's response. Please try again.",
103
- variant: "destructive",
104
  });
105
  } finally {
106
  setIsAiThinking(false);
107
  }
108
  };
109
 
 
 
 
 
 
 
 
 
 
110
  const handlePlayAgain = () => {
111
  setGameState("welcome");
112
  setSentence([]);
113
  setAiGuess("");
114
  setCurrentWord("");
 
 
115
  };
116
 
117
  const handleContinue = () => {
@@ -119,6 +126,24 @@ export const GameContainer = () => {
119
  setSentence([]);
120
  };
121
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
122
  return (
123
  <div className="flex min-h-screen items-center justify-center p-4">
124
  <motion.div
@@ -127,145 +152,50 @@ export const GameContainer = () => {
127
  className="w-full max-w-md rounded-xl bg-white p-8 shadow-lg"
128
  >
129
  {gameState === "welcome" ? (
130
- <motion.div
131
- initial={{ opacity: 0 }}
132
- animate={{ opacity: 1 }}
133
- className="text-center"
134
- >
135
- <h1 className="mb-6 text-4xl font-bold text-gray-900">Word Game</h1>
136
- <p className="mb-8 text-gray-600">
137
- Ready to play? Click start to begin!
138
- </p>
139
- <Button
140
- onClick={handleStart}
141
- className="w-full bg-primary text-lg hover:bg-primary/90"
142
- >
143
- Start Game
144
- </Button>
145
- </motion.div>
146
  ) : gameState === "showing-word" ? (
147
- <motion.div
148
- initial={{ opacity: 0 }}
149
- animate={{ opacity: 1 }}
150
- className="text-center"
151
- >
152
- <h2 className="mb-4 text-2xl font-semibold text-gray-900">
153
- Your Word
154
- </h2>
155
- <div className="mb-8 rounded-lg bg-secondary/10 p-6">
156
- <p className="text-4xl font-bold tracking-wider text-secondary">
157
- {currentWord}
158
- </p>
159
- </div>
160
- <p className="mb-8 text-gray-600">
161
- Remember this word! You'll take turns with AI to create a sentence
162
- that describes it. End the sentence with a period when you're done, and another AI will try to guess it!
163
- </p>
164
- <Button
165
- onClick={handleContinue}
166
- className="w-full bg-primary text-lg hover:bg-primary/90"
167
- >
168
- Continue
169
- </Button>
170
- </motion.div>
171
  ) : gameState === "building-sentence" ? (
172
- <motion.div
173
- initial={{ opacity: 0 }}
174
- animate={{ opacity: 1 }}
175
- className="text-center"
176
- >
177
- <h2 className="mb-4 text-2xl font-semibold text-gray-900">
178
- Build a Description
179
- </h2>
180
- <p className="text-sm text-gray-600">
181
- Take turns with AI to describe "{currentWord}" without using the word
182
- itself!
183
- </p>
184
- <div className="mb-4 rounded-lg bg-secondary/10 p-4">
185
- <p className="text-2xl font-bold tracking-wider text-secondary">
186
- {currentWord}
187
- </p>
188
- </div>
189
- <div className="mb-6 rounded-lg bg-gray-50 p-4">
190
- <p className="text-lg text-gray-800">
191
- {sentence.length > 0 ? sentence.join(" ") : "Start your sentence..."}
192
- </p>
193
- </div>
194
- <form onSubmit={handlePlayerWord} className="mb-4">
195
- <Input
196
- type="text"
197
- value={playerInput}
198
- onChange={(e) => setPlayerInput(e.target.value.replace(/\s/g, ''))}
199
- placeholder="Enter your word (end with . to finish)..."
200
- className="mb-4"
201
- disabled={isAiThinking}
202
- />
203
- <Button
204
- type="submit"
205
- className="w-full bg-primary text-lg hover:bg-primary/90"
206
- disabled={!playerInput.trim() || isAiThinking}
207
- >
208
- {isAiThinking ? "AI is thinking..." : "Add Word"}
209
- </Button>
210
- </form>
211
- </motion.div>
212
  ) : gameState === "game-over" ? (
213
- <motion.div
214
- initial={{ opacity: 0 }}
215
- animate={{ opacity: 1 }}
216
- className="text-center"
217
- >
218
- <h2 className="mb-4 text-2xl font-semibold text-gray-900">
219
- Game Over
220
- </h2>
221
- <div className="mb-6 rounded-lg bg-gray-50 p-4">
222
- <p className="mb-4 text-lg text-gray-800">
223
- Your sentence was not grammatically correct:
224
- </p>
225
- <p className="italic text-gray-600">
226
- {sentence.join(" ")}
227
- </p>
228
- </div>
229
- <Button
230
- onClick={handlePlayAgain}
231
- className="w-full bg-primary text-lg hover:bg-primary/90"
232
- >
233
- Play Again
234
- </Button>
235
- </motion.div>
236
- ) : (
237
- <motion.div
238
- initial={{ opacity: 0 }}
239
- animate={{ opacity: 1 }}
240
- className="text-center"
241
- >
242
- <h2 className="mb-4 text-2xl font-semibold text-gray-900">
243
- AI's Guess
244
- </h2>
245
- <div className="mb-6 rounded-lg bg-gray-50 p-4">
246
- <p className="mb-4 text-lg text-gray-800">
247
- Your sentence: {sentence.join(" ")}
248
- </p>
249
- <p className="text-xl font-bold text-primary">
250
- AI guessed: {aiGuess}
251
- </p>
252
- <p className="mt-4 text-lg">
253
- {aiGuess.toLowerCase() === currentWord.toLowerCase() ? (
254
- <span className="text-green-600">Correct guess! 🎉</span>
255
- ) : (
256
- <span className="text-red-600">Wrong! The word was {currentWord}</span>
257
- )}
258
- </p>
259
- </div>
260
- <Button
261
- onClick={handlePlayAgain}
262
- className="w-full bg-primary text-lg hover:bg-primary/90"
263
- >
264
- Play Again
265
- </Button>
266
- </motion.div>
267
- )}
268
  </motion.div>
269
  </div>
270
  );
271
- };
 
1
+ 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 { HighScoreBoard } from "./HighScoreBoard";
7
+ import { WelcomeScreen } from "./game/WelcomeScreen";
8
+ import { WordDisplay } from "./game/WordDisplay";
9
+ import { SentenceBuilder } from "./game/SentenceBuilder";
10
+ import { GuessDisplay } from "./game/GuessDisplay";
11
+ import { GameOver } from "./game/GameOver";
12
 
13
+ type GameState = "welcome" | "showing-word" | "building-sentence" | "showing-guess" | "game-over" | "high-scores";
14
 
15
  export const GameContainer = () => {
16
  const [gameState, setGameState] = useState<GameState>("welcome");
 
19
  const [playerInput, setPlayerInput] = useState<string>("");
20
  const [isAiThinking, setIsAiThinking] = useState(false);
21
  const [aiGuess, setAiGuess] = useState<string>("");
22
+ const [successfulRounds, setSuccessfulRounds] = useState<number>(0);
23
+ const [totalWords, setTotalWords] = useState<number>(0);
24
  const { toast } = useToast();
25
 
26
+ useEffect(() => {
27
+ const handleKeyPress = (e: KeyboardEvent) => {
28
+ if (e.key === 'Enter') {
29
+ if (gameState === 'welcome') {
30
+ handleStart();
31
+ } else if (gameState === 'showing-word') {
32
+ handleContinue();
33
+ } else if (gameState === 'game-over' || gameState === 'showing-guess') {
34
+ const correct = isGuessCorrect();
35
+ if (correct) {
36
+ handleNextRound();
37
+ } else {
38
+ setGameState("high-scores");
39
+ }
40
+ }
41
+ }
42
+ };
43
+
44
+ window.addEventListener('keydown', handleKeyPress as any);
45
+ return () => window.removeEventListener('keydown', handleKeyPress as any);
46
+ }, [gameState, aiGuess, currentWord]);
47
+
48
  const handleStart = () => {
49
  const word = getRandomWord();
50
  setCurrentWord(word);
51
  setGameState("showing-word");
52
+ setSuccessfulRounds(0);
53
+ setTotalWords(0);
54
  console.log("Game started with word:", word);
55
  };
56
 
 
62
  const newSentence = [...sentence, word];
63
  setSentence(newSentence);
64
  setPlayerInput("");
65
+ setTotalWords(prev => prev + 1);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
 
67
  setIsAiThinking(true);
68
  try {
69
  const aiWord = await generateAIResponse(currentWord, newSentence);
70
  const newSentenceWithAi = [...newSentence, aiWord];
71
  setSentence(newSentenceWithAi);
72
+ setTotalWords(prev => prev + 1);
73
+ } catch (error) {
74
+ console.error('Error in AI turn:', error);
75
+ toast({
76
+ title: "AI Response Delayed",
77
+ description: "The AI is currently busy. Please try adding another word or wait a moment.",
78
+ variant: "default",
79
+ });
80
+ } finally {
81
+ setIsAiThinking(false);
82
+ }
83
+ };
84
 
85
+ const handleMakeGuess = async () => {
86
+ if (sentence.length === 0) return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
87
 
88
+ setIsAiThinking(true);
89
+ try {
90
+ const finalSentence = sentence.join(' ');
91
+ const guess = await guessWord(finalSentence);
92
+ setAiGuess(guess);
93
+ setGameState("showing-guess");
94
  } catch (error) {
95
+ console.error('Error getting AI guess:', error);
96
  toast({
97
+ title: "AI Response Delayed",
98
+ description: "The AI is currently busy. Please try again in a moment.",
99
+ variant: "default",
100
  });
101
  } finally {
102
  setIsAiThinking(false);
103
  }
104
  };
105
 
106
+ const handleNextRound = () => {
107
+ const word = getRandomWord();
108
+ setCurrentWord(word);
109
+ setGameState("showing-word");
110
+ setSentence([]);
111
+ setAiGuess("");
112
+ console.log("Next round started with word:", word);
113
+ };
114
+
115
  const handlePlayAgain = () => {
116
  setGameState("welcome");
117
  setSentence([]);
118
  setAiGuess("");
119
  setCurrentWord("");
120
+ setSuccessfulRounds(0);
121
+ setTotalWords(0);
122
  };
123
 
124
  const handleContinue = () => {
 
126
  setSentence([]);
127
  };
128
 
129
+ const isGuessCorrect = () => {
130
+ return aiGuess.toLowerCase() === currentWord.toLowerCase();
131
+ };
132
+
133
+ const handleGuessComplete = () => {
134
+ if (isGuessCorrect()) {
135
+ setSuccessfulRounds(prev => prev + 1);
136
+ return true;
137
+ }
138
+ setGameState("high-scores");
139
+ return false;
140
+ };
141
+
142
+ const getAverageWordsPerRound = () => {
143
+ if (successfulRounds === 0) return 0;
144
+ return totalWords / successfulRounds;
145
+ };
146
+
147
  return (
148
  <div className="flex min-h-screen items-center justify-center p-4">
149
  <motion.div
 
152
  className="w-full max-w-md rounded-xl bg-white p-8 shadow-lg"
153
  >
154
  {gameState === "welcome" ? (
155
+ <WelcomeScreen onStart={handleStart} />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
156
  ) : gameState === "showing-word" ? (
157
+ <WordDisplay
158
+ currentWord={currentWord}
159
+ successfulRounds={successfulRounds}
160
+ onContinue={handleContinue}
161
+ />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
162
  ) : gameState === "building-sentence" ? (
163
+ <SentenceBuilder
164
+ currentWord={currentWord}
165
+ successfulRounds={successfulRounds}
166
+ sentence={sentence}
167
+ playerInput={playerInput}
168
+ isAiThinking={isAiThinking}
169
+ onInputChange={setPlayerInput}
170
+ onSubmitWord={handlePlayerWord}
171
+ onMakeGuess={handleMakeGuess}
172
+ />
173
+ ) : gameState === "showing-guess" ? (
174
+ <GuessDisplay
175
+ sentence={sentence}
176
+ aiGuess={aiGuess}
177
+ currentWord={currentWord}
178
+ onNextRound={() => {
179
+ handleGuessComplete();
180
+ handleNextRound();
181
+ }}
182
+ onPlayAgain={handlePlayAgain}
183
+ />
184
+ ) : gameState === "high-scores" ? (
185
+ <HighScoreBoard
186
+ currentScore={successfulRounds}
187
+ avgWordsPerRound={getAverageWordsPerRound()}
188
+ onClose={() => setGameState("game-over")}
189
+ onPlayAgain={handlePlayAgain}
190
+ />
 
 
 
 
 
 
 
 
 
 
 
 
191
  ) : gameState === "game-over" ? (
192
+ <GameOver
193
+ successfulRounds={successfulRounds}
194
+ onViewHighScores={() => setGameState("high-scores")}
195
+ onPlayAgain={handlePlayAgain}
196
+ />
197
+ ) : null}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
198
  </motion.div>
199
  </div>
200
  );
201
+ };
src/components/HighScoreBoard.tsx ADDED
@@ -0,0 +1,157 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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";
5
+ import { useQuery } from "@tanstack/react-query";
6
+ import {
7
+ Table,
8
+ TableBody,
9
+ TableCell,
10
+ TableHead,
11
+ TableHeader,
12
+ TableRow,
13
+ } from "@/components/ui/table";
14
+ import { useToast } from "@/components/ui/use-toast";
15
+
16
+ interface HighScore {
17
+ id: string;
18
+ player_name: string;
19
+ score: number;
20
+ avg_words_per_round: number;
21
+ created_at: string;
22
+ }
23
+
24
+ interface HighScoreBoardProps {
25
+ currentScore: number;
26
+ avgWordsPerRound: number;
27
+ onClose: () => void;
28
+ onPlayAgain: () => void;
29
+ }
30
+
31
+ export const HighScoreBoard = ({
32
+ currentScore,
33
+ avgWordsPerRound,
34
+ onClose,
35
+ onPlayAgain,
36
+ }: HighScoreBoardProps) => {
37
+ const [playerName, setPlayerName] = useState("");
38
+ const [isSubmitting, setIsSubmitting] = useState(false);
39
+ const { toast } = useToast();
40
+
41
+ const { data: highScores, refetch } = useQuery({
42
+ queryKey: ["highScores"],
43
+ queryFn: async () => {
44
+ const { data, error } = await supabase
45
+ .from("high_scores")
46
+ .select("*")
47
+ .order("score", { ascending: false })
48
+ .order("avg_words_per_round", { ascending: true });
49
+
50
+ if (error) throw error;
51
+ return data as HighScore[];
52
+ },
53
+ });
54
+
55
+ const handleSubmitScore = async () => {
56
+ if (!playerName.trim()) {
57
+ toast({
58
+ title: "Error",
59
+ description: "Please enter your name",
60
+ variant: "destructive",
61
+ });
62
+ return;
63
+ }
64
+
65
+ setIsSubmitting(true);
66
+ try {
67
+ const { error } = await supabase.from("high_scores").insert({
68
+ player_name: playerName.trim(),
69
+ score: currentScore,
70
+ avg_words_per_round: avgWordsPerRound,
71
+ });
72
+
73
+ if (error) throw error;
74
+
75
+ toast({
76
+ title: "Success!",
77
+ description: "Your score has been recorded",
78
+ });
79
+
80
+ await refetch();
81
+ setPlayerName("");
82
+ } catch (error) {
83
+ console.error("Error submitting score:", error);
84
+ toast({
85
+ title: "Error",
86
+ description: "Failed to submit score. Please try again.",
87
+ variant: "destructive",
88
+ });
89
+ } finally {
90
+ setIsSubmitting(false);
91
+ }
92
+ };
93
+
94
+ return (
95
+ <div className="space-y-6">
96
+ <div className="text-center">
97
+ <h2 className="text-2xl font-bold mb-2">High Scores</h2>
98
+ <p className="text-gray-600">
99
+ Your score: {currentScore} rounds
100
+ {currentScore > 0 && ` (${avgWordsPerRound.toFixed(1)} words/round)`}
101
+ </p>
102
+ </div>
103
+
104
+ <div className="flex gap-4 mb-6">
105
+ <Input
106
+ placeholder="Enter your name"
107
+ value={playerName}
108
+ onChange={(e) => setPlayerName(e.target.value)}
109
+ className="flex-1"
110
+ />
111
+ <Button
112
+ onClick={handleSubmitScore}
113
+ disabled={isSubmitting || !playerName.trim()}
114
+ >
115
+ {isSubmitting ? "Submitting..." : "Submit Score"}
116
+ </Button>
117
+ </div>
118
+
119
+ <div className="rounded-md border">
120
+ <Table>
121
+ <TableHeader>
122
+ <TableRow>
123
+ <TableHead>Rank</TableHead>
124
+ <TableHead>Player</TableHead>
125
+ <TableHead>Rounds</TableHead>
126
+ <TableHead>Avg Words/Round</TableHead>
127
+ </TableRow>
128
+ </TableHeader>
129
+ <TableBody>
130
+ {highScores?.map((score, index) => (
131
+ <TableRow key={score.id}>
132
+ <TableCell>{index + 1}</TableCell>
133
+ <TableCell>{score.player_name}</TableCell>
134
+ <TableCell>{score.score}</TableCell>
135
+ <TableCell>{score.avg_words_per_round.toFixed(1)}</TableCell>
136
+ </TableRow>
137
+ ))}
138
+ {!highScores?.length && (
139
+ <TableRow>
140
+ <TableCell colSpan={4} className="text-center">
141
+ No high scores yet. Be the first!
142
+ </TableCell>
143
+ </TableRow>
144
+ )}
145
+ </TableBody>
146
+ </Table>
147
+ </div>
148
+
149
+ <div className="flex justify-end gap-4">
150
+ <Button variant="outline" onClick={onClose}>
151
+ Close
152
+ </Button>
153
+ <Button onClick={onPlayAgain}>Play Again</Button>
154
+ </div>
155
+ </div>
156
+ );
157
+ };
src/components/game/GameOver.tsx ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Button } from "@/components/ui/button";
2
+ import { motion } from "framer-motion";
3
+
4
+ interface GameOverProps {
5
+ successfulRounds: number;
6
+ onViewHighScores: () => void;
7
+ onPlayAgain: () => void;
8
+ }
9
+
10
+ export const GameOver = ({
11
+ successfulRounds,
12
+ onViewHighScores,
13
+ onPlayAgain,
14
+ }: GameOverProps) => {
15
+ return (
16
+ <motion.div
17
+ initial={{ opacity: 0 }}
18
+ animate={{ opacity: 1 }}
19
+ className="text-center"
20
+ >
21
+ <h2 className="mb-4 text-2xl font-semibold text-gray-900">Game Over!</h2>
22
+ <p className="mb-6 text-lg text-gray-800">
23
+ You completed {successfulRounds} rounds successfully!
24
+ </p>
25
+ <div className="flex gap-4">
26
+ <Button
27
+ onClick={onViewHighScores}
28
+ className="flex-1 bg-secondary text-lg hover:bg-secondary/90"
29
+ >
30
+ View High Scores
31
+ </Button>
32
+ <Button
33
+ onClick={onPlayAgain}
34
+ className="flex-1 bg-primary text-lg hover:bg-primary/90"
35
+ >
36
+ Play Again ⏎
37
+ </Button>
38
+ </div>
39
+ </motion.div>
40
+ );
41
+ };
src/components/game/GuessDisplay.tsx ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Button } from "@/components/ui/button";
2
+ import { motion } from "framer-motion";
3
+
4
+ interface GuessDisplayProps {
5
+ sentence: string[];
6
+ aiGuess: string;
7
+ currentWord: string;
8
+ onNextRound: () => void;
9
+ onPlayAgain: () => void;
10
+ }
11
+
12
+ export const GuessDisplay = ({
13
+ sentence,
14
+ aiGuess,
15
+ currentWord,
16
+ onNextRound,
17
+ onPlayAgain,
18
+ }: GuessDisplayProps) => {
19
+ const isGuessCorrect = () => aiGuess.toLowerCase() === currentWord.toLowerCase();
20
+
21
+ return (
22
+ <motion.div
23
+ initial={{ opacity: 0 }}
24
+ animate={{ opacity: 1 }}
25
+ className="text-center"
26
+ >
27
+ <h2 className="mb-4 text-2xl font-semibold text-gray-900">AI's Guess</h2>
28
+ <div className="mb-6 rounded-lg bg-gray-50 p-4">
29
+ <p className="mb-4 text-lg text-gray-800">
30
+ Your sentence: {sentence.join(" ")}
31
+ </p>
32
+ <p className="text-xl font-bold text-primary">AI guessed: {aiGuess}</p>
33
+ <p className="mt-4 text-lg">
34
+ {isGuessCorrect() ? (
35
+ <span className="text-green-600">
36
+ Correct guess! 🎉 Ready for the next round? Press Enter
37
+ </span>
38
+ ) : (
39
+ <span className="text-red-600">
40
+ Game Over! Press Enter to play again
41
+ </span>
42
+ )}
43
+ </p>
44
+ </div>
45
+ <Button
46
+ onClick={isGuessCorrect() ? onNextRound : onPlayAgain}
47
+ className="w-full bg-primary text-lg hover:bg-primary/90"
48
+ >
49
+ {isGuessCorrect() ? "Next Round ⏎" : "Play Again ⏎"}
50
+ </Button>
51
+ </motion.div>
52
+ );
53
+ };
src/components/game/SentenceBuilder.tsx ADDED
@@ -0,0 +1,102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Button } from "@/components/ui/button";
2
+ import { Input } from "@/components/ui/input";
3
+ import { motion } from "framer-motion";
4
+ import { KeyboardEvent, useRef, useEffect } from "react";
5
+
6
+ interface SentenceBuilderProps {
7
+ currentWord: string;
8
+ successfulRounds: number;
9
+ sentence: string[];
10
+ playerInput: string;
11
+ isAiThinking: boolean;
12
+ onInputChange: (value: string) => void;
13
+ onSubmitWord: (e: React.FormEvent) => void;
14
+ onMakeGuess: () => void;
15
+ }
16
+
17
+ export const SentenceBuilder = ({
18
+ currentWord,
19
+ successfulRounds,
20
+ sentence,
21
+ playerInput,
22
+ isAiThinking,
23
+ onInputChange,
24
+ onSubmitWord,
25
+ onMakeGuess,
26
+ }: SentenceBuilderProps) => {
27
+ const inputRef = useRef<HTMLInputElement>(null);
28
+
29
+ useEffect(() => {
30
+ setTimeout(() => {
31
+ inputRef.current?.focus();
32
+ }, 100);
33
+ }, []);
34
+
35
+ const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
36
+ if (e.shiftKey && e.key === 'Enter') {
37
+ e.preventDefault();
38
+ if (sentence.length > 0 && !isAiThinking) {
39
+ onMakeGuess();
40
+ }
41
+ }
42
+ };
43
+
44
+ return (
45
+ <motion.div
46
+ initial={{ opacity: 0 }}
47
+ animate={{ opacity: 1 }}
48
+ className="text-center"
49
+ >
50
+ <h2 className="mb-4 text-2xl font-semibold text-gray-900">
51
+ Build a Description
52
+ </h2>
53
+ <p className="mb-6 text-sm text-gray-600">
54
+ Take turns with AI to describe your word without using the word itself!
55
+ </p>
56
+ <div className="mb-4 rounded-lg bg-secondary/10 p-4">
57
+ <p className="text-2xl font-bold tracking-wider text-secondary">
58
+ {currentWord}
59
+ </p>
60
+ </div>
61
+ {successfulRounds > 0 && (
62
+ <p className="mb-4 text-green-600">
63
+ Successful rounds: {successfulRounds}
64
+ </p>
65
+ )}
66
+ <div className="mb-6 rounded-lg bg-gray-50 p-4">
67
+ <p className="text-lg text-gray-800">
68
+ {sentence.length > 0 ? sentence.join(" ") : "Start your sentence..."}
69
+ </p>
70
+ </div>
71
+ <form onSubmit={onSubmitWord} className="mb-4">
72
+ <Input
73
+ ref={inputRef}
74
+ type="text"
75
+ value={playerInput}
76
+ onChange={(e) => onInputChange(e.target.value.replace(/\s/g, ''))}
77
+ onKeyDown={handleKeyDown}
78
+ placeholder="Enter your word..."
79
+ className="mb-4"
80
+ disabled={isAiThinking}
81
+ />
82
+ <div className="flex gap-4">
83
+ <Button
84
+ type="submit"
85
+ className="flex-1 bg-primary text-lg hover:bg-primary/90"
86
+ disabled={!playerInput.trim() || isAiThinking}
87
+ >
88
+ {isAiThinking ? "AI is thinking..." : "Add Word ⏎"}
89
+ </Button>
90
+ <Button
91
+ type="button"
92
+ onClick={onMakeGuess}
93
+ className="flex-1 bg-secondary text-lg hover:bg-secondary/90"
94
+ disabled={sentence.length === 0 || isAiThinking}
95
+ >
96
+ {isAiThinking ? "AI is thinking..." : "Make AI Guess ⇧⏎"}
97
+ </Button>
98
+ </div>
99
+ </form>
100
+ </motion.div>
101
+ );
102
+ };
src/components/game/WelcomeScreen.tsx ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Button } from "@/components/ui/button";
2
+ import { motion } from "framer-motion";
3
+
4
+ interface WelcomeScreenProps {
5
+ onStart: () => void;
6
+ }
7
+
8
+ export const WelcomeScreen = ({ onStart }: WelcomeScreenProps) => {
9
+ return (
10
+ <motion.div
11
+ initial={{ opacity: 0 }}
12
+ animate={{ opacity: 1 }}
13
+ className="text-center"
14
+ >
15
+ <h1 className="mb-6 text-4xl font-bold text-gray-900">Word Game</h1>
16
+ <p className="mb-8 text-gray-600">
17
+ Ready to play? Click start or press Enter to begin!
18
+ </p>
19
+ <Button
20
+ onClick={onStart}
21
+ className="w-full bg-primary text-lg hover:bg-primary/90"
22
+ >
23
+ Start Game ⏎
24
+ </Button>
25
+ </motion.div>
26
+ );
27
+ };
src/components/game/WordDisplay.tsx ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Button } from "@/components/ui/button";
2
+ import { motion } from "framer-motion";
3
+
4
+ interface WordDisplayProps {
5
+ currentWord: string;
6
+ successfulRounds: number;
7
+ onContinue: () => void;
8
+ }
9
+
10
+ export const WordDisplay = ({ currentWord, successfulRounds, onContinue }: WordDisplayProps) => {
11
+ return (
12
+ <motion.div
13
+ initial={{ opacity: 0 }}
14
+ animate={{ opacity: 1 }}
15
+ className="text-center"
16
+ >
17
+ <h2 className="mb-4 text-2xl font-semibold text-gray-900">Your Word</h2>
18
+ <div className="mb-4 rounded-lg bg-secondary/10 p-6">
19
+ <p className="text-4xl font-bold tracking-wider text-secondary">
20
+ {currentWord}
21
+ </p>
22
+ </div>
23
+ {successfulRounds > 0 && (
24
+ <p className="mb-4 text-green-600">
25
+ Successful rounds: {successfulRounds}
26
+ </p>
27
+ )}
28
+ <p className="mb-8 text-gray-600">
29
+ You'll take turns with AI to create a sentence that describes this word.
30
+ </p>
31
+ <p className="mb-8 text-gray-600">
32
+ Click the "Make AI Guess" button to see if another AI can guess it!
33
+ </p>
34
+ <Button
35
+ onClick={onContinue}
36
+ className="w-full bg-primary text-lg hover:bg-primary/90"
37
+ >
38
+ Continue ⏎
39
+ </Button>
40
+ </motion.div>
41
+ );
42
+ };
src/integrations/supabase/types.ts CHANGED
@@ -9,7 +9,30 @@ export type Json =
9
  export type Database = {
10
  public: {
11
  Tables: {
12
- [_ in never]: never
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
  }
14
  Views: {
15
  [_ in never]: never
 
9
  export type Database = {
10
  public: {
11
  Tables: {
12
+ high_scores: {
13
+ Row: {
14
+ avg_words_per_round: number
15
+ created_at: string
16
+ id: string
17
+ player_name: string
18
+ score: number
19
+ }
20
+ Insert: {
21
+ avg_words_per_round: number
22
+ created_at?: string
23
+ id?: string
24
+ player_name: string
25
+ score: number
26
+ }
27
+ Update: {
28
+ avg_words_per_round?: number
29
+ created_at?: string
30
+ id?: string
31
+ player_name?: string
32
+ score?: number
33
+ }
34
+ Relationships: []
35
+ }
36
  }
37
  Views: {
38
  [_ in never]: never
src/lib/words.ts CHANGED
@@ -1,4 +1,3 @@
1
- // A small list of random words for the game
2
  export const words = [
3
  "ELEPHANT",
4
  "SUNSHINE",
@@ -12,7 +11,7 @@ export const words = [
12
  "DIAMOND",
13
  "STARDUST",
14
  "MEADOW",
15
- "WILDFLOWER",
16
  "HORIZON",
17
  "JOURNEY",
18
  "FEATHER",
@@ -34,7 +33,7 @@ export const words = [
34
  "COMET",
35
  "WILDERNESS",
36
  "FREEDOM",
37
- "NIGHTFALL",
38
  "FANTASY",
39
  "ADVENTURE",
40
  "SERENITY",
@@ -51,8 +50,57 @@ export const words = [
51
  "CRESCENT",
52
  "WATERFALL",
53
  "CITADEL",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
  ];
55
 
56
  export const getRandomWord = () => {
57
  return words[Math.floor(Math.random() * words.length)];
58
- };
 
 
1
  export const words = [
2
  "ELEPHANT",
3
  "SUNSHINE",
 
11
  "DIAMOND",
12
  "STARDUST",
13
  "MEADOW",
14
+ "FLOWER",
15
  "HORIZON",
16
  "JOURNEY",
17
  "FEATHER",
 
33
  "COMET",
34
  "WILDERNESS",
35
  "FREEDOM",
36
+ "NIGHT",
37
  "FANTASY",
38
  "ADVENTURE",
39
  "SERENITY",
 
50
  "CRESCENT",
51
  "WATERFALL",
52
  "CITADEL",
53
+ "AURORA",
54
+ "BLISS",
55
+ "CASCADE",
56
+ "DAWN",
57
+ "ECLIPSE",
58
+ "FIRELIGHT",
59
+ "GARDEN",
60
+ "HAVEN",
61
+ "INFINITY",
62
+ "JUBILEE",
63
+ "KALEIDOSCOPE",
64
+ "LULLABY",
65
+ "MARVEL",
66
+ "NEBULA",
67
+ "OASIS",
68
+ "PARADISE",
69
+ "QUIETUDE",
70
+ "RADIANCE",
71
+ "SANCTUARY",
72
+ "TRanquility",
73
+ "UNIVERSE",
74
+ "VELVET",
75
+ "WONDERLAND",
76
+ "YIELD",
77
+ "ABYSS",
78
+ "BALANCE",
79
+ "CALM",
80
+ "DAZZLE",
81
+ "EMBRACE",
82
+ "FLICKER",
83
+ "GLIMMER",
84
+ "HALO",
85
+ "ILLUMINATE",
86
+ "JEWEL",
87
+ "KINDLE",
88
+ "LUMINOUS",
89
+ "MYSTIC",
90
+ "NIRVANA",
91
+ "OPULENCE",
92
+ "PEACE",
93
+ "QUIET",
94
+ "RAPTURE",
95
+ "SERENE",
96
+ "TRANQUIL",
97
+ "UNITY",
98
+ "VISION",
99
+ "WONDERMENT",
100
+ "YEARNING",
101
+ "ZEAL"
102
  ];
103
 
104
  export const getRandomWord = () => {
105
  return words[Math.floor(Math.random() * words.length)];
106
+ };
src/services/mistralService.ts CHANGED
@@ -7,6 +7,9 @@ export const generateAIResponse = async (currentWord: string, currentSentence: s
7
 
8
  if (error) {
9
  console.error('Error generating AI response:', error);
 
 
 
10
  throw error;
11
  }
12
 
@@ -25,6 +28,9 @@ export const guessWord = async (sentence: string): Promise<string> => {
25
 
26
  if (error) {
27
  console.error('Error getting AI guess:', error);
 
 
 
28
  throw error;
29
  }
30
 
@@ -34,18 +40,4 @@ export const guessWord = async (sentence: string): Promise<string> => {
34
 
35
  console.log('AI guessed:', data.guess);
36
  return data.guess;
37
- };
38
-
39
- export const validateSentence = async (sentence: string): Promise<boolean> => {
40
- const { data, error } = await supabase.functions.invoke('validate-sentence', {
41
- body: { sentence }
42
- });
43
-
44
- if (error) {
45
- console.error('Error validating sentence:', error);
46
- throw error;
47
- }
48
-
49
- console.log('Sentence validation result:', data.isValid);
50
- return data.isValid;
51
  };
 
7
 
8
  if (error) {
9
  console.error('Error generating AI response:', error);
10
+ if (error.message?.includes('rate limit')) {
11
+ throw new Error('The AI service is currently busy. Please try again in a few moments.');
12
+ }
13
  throw error;
14
  }
15
 
 
28
 
29
  if (error) {
30
  console.error('Error getting AI guess:', error);
31
+ if (error.message?.includes('rate limit')) {
32
+ throw new Error('The AI service is currently busy. Please try again in a few moments.');
33
+ }
34
  throw error;
35
  }
36
 
 
40
 
41
  console.log('AI guessed:', data.guess);
42
  return data.guess;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
  };
supabase/functions/generate-word/index.ts CHANGED
@@ -19,44 +19,81 @@ serve(async (req) => {
19
  apiKey: Deno.env.get('MISTRAL_API_KEY'),
20
  });
21
 
22
- const response = await client.chat.complete({
23
- model: "mistral-large-latest",
24
- messages: [
25
- {
26
- role: "system",
27
- content: `You are helping in a word game. The secret word is "${currentWord}".
 
 
 
 
 
 
 
28
  Your task is to find a sentence to describe this word without using it directly.
29
  Answer with a complete, grammatically correct sentence that starts with "${currentSentence.join(' ')}".
30
  Do not add quotes or backticks. Just answer with the sentence.`
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
  }
32
- ],
33
- maxTokens: 10,
34
- temperature: 0.7
35
- });
 
36
 
37
- const aiResponse = response.choices[0].message.content.trim();
38
- console.log('AI full response:', aiResponse);
39
-
40
- // Extract the new word by comparing with the existing sentence
41
- const existingWords = currentSentence.join(' ');
42
- const newWord = aiResponse
43
- .slice(existingWords.length)
44
- .trim()
45
- .split(' ')[0]
46
- .replace(/[.,!?]$/, ''); // Remove any punctuation at the end
47
-
48
- console.log('Extracted new word:', newWord);
49
 
50
- return new Response(
51
- JSON.stringify({ word: newWord }),
52
- { headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
53
- );
54
  } catch (error) {
55
  console.error('Error generating word:', error);
 
 
 
 
 
 
56
  return new Response(
57
- JSON.stringify({ error: error.message }),
 
 
 
58
  {
59
- status: 500,
60
  headers: { ...corsHeaders, 'Content-Type': 'application/json' }
61
  }
62
  );
 
19
  apiKey: Deno.env.get('MISTRAL_API_KEY'),
20
  });
21
 
22
+ // Add retry logic with exponential backoff
23
+ const maxRetries = 3;
24
+ let retryCount = 0;
25
+ let lastError = null;
26
+
27
+ while (retryCount < maxRetries) {
28
+ try {
29
+ const response = await client.chat.complete({
30
+ model: "mistral-large-latest",
31
+ messages: [
32
+ {
33
+ role: "system",
34
+ content: `You are helping in a word game. The secret word is "${currentWord}".
35
  Your task is to find a sentence to describe this word without using it directly.
36
  Answer with a complete, grammatically correct sentence that starts with "${currentSentence.join(' ')}".
37
  Do not add quotes or backticks. Just answer with the sentence.`
38
+ }
39
+ ],
40
+ maxTokens: 10,
41
+ temperature: 0.7
42
+ });
43
+
44
+ const aiResponse = response.choices[0].message.content.trim();
45
+ console.log('AI full response:', aiResponse);
46
+
47
+ // Extract the new word by comparing with the existing sentence
48
+ const existingWords = currentSentence.join(' ');
49
+ const newWord = aiResponse
50
+ .slice(existingWords.length)
51
+ .trim()
52
+ .split(' ')[0]
53
+ .replace(/[.,!?]$/, ''); // Remove any punctuation at the end
54
+
55
+ console.log('Extracted new word:', newWord);
56
+
57
+ return new Response(
58
+ JSON.stringify({ word: newWord }),
59
+ { headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
60
+ );
61
+ } catch (error) {
62
+ console.error(`Attempt ${retryCount + 1} failed:`, error);
63
+ lastError = error;
64
+
65
+ // If it's a rate limit error, wait before retrying
66
+ if (error.message?.includes('rate limit') || error.status === 429) {
67
+ const waitTime = Math.pow(2, retryCount) * 1000; // Exponential backoff: 1s, 2s, 4s
68
+ console.log(`Rate limit hit, waiting ${waitTime}ms before retry`);
69
+ await new Promise(resolve => setTimeout(resolve, waitTime));
70
+ retryCount++;
71
+ continue;
72
  }
73
+
74
+ // If it's not a rate limit error, throw immediately
75
+ throw error;
76
+ }
77
+ }
78
 
79
+ // If we've exhausted all retries
80
+ throw new Error(`Failed after ${maxRetries} attempts. Last error: ${lastError?.message}`);
 
 
 
 
 
 
 
 
 
 
81
 
 
 
 
 
82
  } catch (error) {
83
  console.error('Error generating word:', error);
84
+
85
+ // Provide a more user-friendly error message
86
+ const errorMessage = error.message?.includes('rate limit')
87
+ ? "The AI service is currently busy. Please try again in a few moments."
88
+ : "Sorry, there was an error generating the word. Please try again.";
89
+
90
  return new Response(
91
+ JSON.stringify({
92
+ error: errorMessage,
93
+ details: error.message
94
+ }),
95
  {
96
+ status: error.message?.includes('rate limit') ? 429 : 500,
97
  headers: { ...corsHeaders, 'Content-Type': 'application/json' }
98
  }
99
  );
supabase/functions/guess-word/index.ts CHANGED
@@ -19,37 +19,74 @@ serve(async (req) => {
19
  apiKey: Deno.env.get('MISTRAL_API_KEY'),
20
  });
21
 
22
- const response = await client.chat.complete({
23
- model: "mistral-large-latest",
24
- messages: [
25
- {
26
- role: "system",
27
- content: `You are playing a word guessing game. Given a descriptive sentence, your task is to guess the single word being described.
 
 
 
 
 
 
 
28
  Respond with ONLY the word you think is being described, in uppercase letters.
29
  Do not add any explanation or punctuation.`
30
- },
31
- {
32
- role: "user",
33
- content: `Based on this description, what single word is being described: "${sentence}"`
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
  }
35
- ],
36
- maxTokens: 10,
37
- temperature: 0.2
38
- });
 
39
 
40
- const guess = response.choices[0].message.content.trim().toUpperCase();
41
- console.log('AI guess:', guess);
42
 
43
- return new Response(
44
- JSON.stringify({ guess }),
45
- { headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
46
- );
47
  } catch (error) {
48
  console.error('Error generating guess:', error);
 
 
 
 
 
 
49
  return new Response(
50
- JSON.stringify({ error: error.message }),
 
 
 
51
  {
52
- status: 500,
53
  headers: { ...corsHeaders, 'Content-Type': 'application/json' }
54
  }
55
  );
 
19
  apiKey: Deno.env.get('MISTRAL_API_KEY'),
20
  });
21
 
22
+ // Add retry logic with exponential backoff
23
+ const maxRetries = 3;
24
+ let retryCount = 0;
25
+ let lastError = null;
26
+
27
+ while (retryCount < maxRetries) {
28
+ try {
29
+ const response = await client.chat.complete({
30
+ model: "mistral-large-latest",
31
+ messages: [
32
+ {
33
+ role: "system",
34
+ content: `You are playing a word guessing game. Given a descriptive sentence, your task is to guess the single word being described.
35
  Respond with ONLY the word you think is being described, in uppercase letters.
36
  Do not add any explanation or punctuation.`
37
+ },
38
+ {
39
+ role: "user",
40
+ content: `Based on this description, what single word is being described: "${sentence}"`
41
+ }
42
+ ],
43
+ maxTokens: 10,
44
+ temperature: 0.2
45
+ });
46
+
47
+ const guess = response.choices[0].message.content.trim().toUpperCase();
48
+ console.log('AI guess:', guess);
49
+
50
+ return new Response(
51
+ JSON.stringify({ guess }),
52
+ { headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
53
+ );
54
+ } catch (error) {
55
+ console.error(`Attempt ${retryCount + 1} failed:`, error);
56
+ lastError = error;
57
+
58
+ // If it's a rate limit error, wait before retrying
59
+ if (error.message?.includes('rate limit') || error.status === 429) {
60
+ const waitTime = Math.pow(2, retryCount) * 1000; // Exponential backoff: 1s, 2s, 4s
61
+ console.log(`Rate limit hit, waiting ${waitTime}ms before retry`);
62
+ await new Promise(resolve => setTimeout(resolve, waitTime));
63
+ retryCount++;
64
+ continue;
65
  }
66
+
67
+ // If it's not a rate limit error, throw immediately
68
+ throw error;
69
+ }
70
+ }
71
 
72
+ // If we've exhausted all retries
73
+ throw new Error(`Failed after ${maxRetries} attempts. Last error: ${lastError?.message}`);
74
 
 
 
 
 
75
  } catch (error) {
76
  console.error('Error generating guess:', error);
77
+
78
+ // Provide a more user-friendly error message
79
+ const errorMessage = error.message?.includes('rate limit')
80
+ ? "The AI service is currently busy. Please try again in a few moments."
81
+ : "Sorry, there was an error generating the guess. Please try again.";
82
+
83
  return new Response(
84
+ JSON.stringify({
85
+ error: errorMessage,
86
+ details: error.message
87
+ }),
88
  {
89
+ status: error.message?.includes('rate limit') ? 429 : 500,
90
  headers: { ...corsHeaders, 'Content-Type': 'application/json' }
91
  }
92
  );