Felix Zieger commited on
Commit
fe9a0c4
·
1 Parent(s): 1b620c7
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>word-duelers</title>
7
- <meta name="description" content="Lovable Generated Project" />
8
- <meta name="author" content="Lovable" />
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 { WordDisplay } from "./game/WordDisplay";
8
  import { SentenceBuilder } from "./game/SentenceBuilder";
9
  import { GuessDisplay } from "./game/GuessDisplay";
10
  import { GameOver } from "./game/GameOver";
11
 
12
- type GameState = "welcome" | "showing-word" | "building-sentence" | "showing-guess" | "game-over";
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
- const word = getRandomWord();
49
- setCurrentWord(word);
50
- setGameState("showing-word");
51
- setSuccessfulRounds(0);
52
- setTotalWords(0);
53
- console.log("Game started with word:", word);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- const finalSentence = sentence.join(' ');
90
- const guess = await guessWord(finalSentence);
 
 
 
 
 
 
 
 
 
 
 
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 word = getRandomWord();
108
- setCurrentWord(word);
109
- setGameState("showing-word");
110
- setSentence([]);
111
- setAiGuess("");
112
- console.log("Next round started with word:", word);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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); // The total words include the ones in the failed last round, so we also count it in the denominator
147
  };
148
 
149
  return (
@@ -155,12 +192,8 @@ export const GameContainer = () => {
155
  >
156
  {gameState === "welcome" ? (
157
  <WelcomeScreen onStart={handleStart} />
158
- ) : gameState === "showing-word" ? (
159
- <WordDisplay
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 = 10;
 
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
- const { error } = await supabase.from("high_scores").insert({
111
- player_name: playerName.trim(),
112
- score: currentScore,
113
- avg_words_per_round: avgWordsPerRound,
114
- });
115
 
116
- if (error) throw error;
117
 
118
- toast({
119
- title: "Success!",
120
- description: "Your score has been recorded",
121
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
122
 
123
  setHasSubmitted(true);
124
  await refetch();
@@ -135,7 +172,14 @@ export const HighScoreBoard = ({
135
  }
136
  };
137
 
138
- const totalPages = highScores ? Math.ceil(highScores.length / ITEMS_PER_PAGE) : 0;
 
 
 
 
 
 
 
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 gap-4">
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
+ });