Felix Zieger commited on
Commit
0ce34cb
·
1 Parent(s): 6a6ea1a

app update

Browse files
README.md CHANGED
@@ -7,7 +7,11 @@ sdk: docker
7
  app_port: 8080
8
  pinned: false
9
  ---
 
10
 
 
 
 
11
 
12
  ## Develop locally
13
 
@@ -16,4 +20,14 @@ Add a .env file with `VITE_MISTRAL_API_KEY=xxx`
16
  ```
17
  npm i
18
  npm run dev
19
- ```
 
 
 
 
 
 
 
 
 
 
 
7
  app_port: 8080
8
  pinned: false
9
  ---
10
+ # Think in Sync
11
 
12
+ This game is a variation of a classical childrens game.
13
+ You will be given a secret word. Your goal is to describe this secret word so that an AI can guess it.
14
+ However, you are only allowed to say one word at the time, taking turns with another AI.
15
 
16
  ## Develop locally
17
 
 
20
  ```
21
  npm i
22
  npm run dev
23
+ ```
24
+
25
+ ## What technologies are used for this project?
26
+
27
+ This project is built with .
28
+
29
+ - Vite
30
+ - TypeScript
31
+ - React
32
+ - shadcn-ui
33
+ - Tailwind CSS
src/components/GameContainer.tsx CHANGED
@@ -3,14 +3,13 @@ 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");
@@ -35,7 +34,7 @@ export const GameContainer = () => {
35
  if (correct) {
36
  handleNextRound();
37
  } else {
38
- setGameState("high-scores");
39
  }
40
  }
41
  }
@@ -104,12 +103,16 @@ export const GameContainer = () => {
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 = () => {
@@ -135,13 +138,12 @@ export const GameContainer = () => {
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 (
@@ -175,27 +177,18 @@ export const GameContainer = () => {
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
- };
 
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");
 
34
  if (correct) {
35
  handleNextRound();
36
  } else {
37
+ setGameState("game-over");
38
  }
39
  }
40
  }
 
103
  };
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
+ }
116
  };
117
 
118
  const handlePlayAgain = () => {
 
138
  setSuccessfulRounds(prev => prev + 1);
139
  return true;
140
  }
 
141
  return false;
142
  };
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 (
 
177
  sentence={sentence}
178
  aiGuess={aiGuess}
179
  currentWord={currentWord}
180
+ onNextRound={handleNextRound}
 
 
 
181
  onPlayAgain={handlePlayAgain}
 
 
 
182
  currentScore={successfulRounds}
183
  avgWordsPerRound={getAverageWordsPerRound()}
 
 
184
  />
185
  ) : gameState === "game-over" ? (
186
  <GameOver
187
  successfulRounds={successfulRounds}
 
188
  onPlayAgain={handlePlayAgain}
189
  />
190
  ) : null}
191
  </motion.div>
192
  </div>
193
  );
194
+ };
src/components/HighScoreBoard.tsx CHANGED
@@ -12,6 +12,14 @@ import {
12
  TableRow,
13
  } from "@/components/ui/table";
14
  import { useToast } from "@/components/ui/use-toast";
 
 
 
 
 
 
 
 
15
 
16
  interface HighScore {
17
  id: string;
@@ -28,6 +36,8 @@ interface HighScoreBoardProps {
28
  onPlayAgain: () => void;
29
  }
30
 
 
 
31
  export const HighScoreBoard = ({
32
  currentScore,
33
  avgWordsPerRound,
@@ -36,6 +46,8 @@ export const HighScoreBoard = ({
36
  }: HighScoreBoardProps) => {
37
  const [playerName, setPlayerName] = useState("");
38
  const [isSubmitting, setIsSubmitting] = useState(false);
 
 
39
  const { toast } = useToast();
40
 
41
  const { data: highScores, refetch } = useQuery({
@@ -62,6 +74,24 @@ export const HighScoreBoard = ({
62
  return;
63
  }
64
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
  setIsSubmitting(true);
66
  try {
67
  const { error } = await supabase.from("high_scores").insert({
@@ -77,6 +107,7 @@ export const HighScoreBoard = ({
77
  description: "Your score has been recorded",
78
  });
79
 
 
80
  await refetch();
81
  setPlayerName("");
82
  } catch (error) {
@@ -91,6 +122,22 @@ export const HighScoreBoard = ({
91
  }
92
  };
93
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
94
  return (
95
  <div className="space-y-6">
96
  <div className="text-center">
@@ -101,20 +148,22 @@ export const HighScoreBoard = ({
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>
@@ -127,15 +176,15 @@ export const HighScoreBoard = ({
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!
@@ -146,6 +195,35 @@ export const HighScoreBoard = ({
146
  </Table>
147
  </div>
148
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
149
  <div className="flex justify-end gap-4">
150
  <Button variant="outline" onClick={onClose}>
151
  Close
 
12
  TableRow,
13
  } from "@/components/ui/table";
14
  import { useToast } from "@/components/ui/use-toast";
15
+ import {
16
+ Pagination,
17
+ PaginationContent,
18
+ PaginationItem,
19
+ PaginationLink,
20
+ PaginationNext,
21
+ PaginationPrevious,
22
+ } from "@/components/ui/pagination";
23
 
24
  interface HighScore {
25
  id: string;
 
36
  onPlayAgain: () => void;
37
  }
38
 
39
+ const ITEMS_PER_PAGE = 10;
40
+
41
  export const HighScoreBoard = ({
42
  currentScore,
43
  avgWordsPerRound,
 
46
  }: HighScoreBoardProps) => {
47
  const [playerName, setPlayerName] = useState("");
48
  const [isSubmitting, setIsSubmitting] = useState(false);
49
+ const [hasSubmitted, setHasSubmitted] = useState(false);
50
+ const [currentPage, setCurrentPage] = useState(1);
51
  const { toast } = useToast();
52
 
53
  const { data: highScores, refetch } = useQuery({
 
74
  return;
75
  }
76
 
77
+ if (currentScore < 1) {
78
+ toast({
79
+ title: "Error",
80
+ description: "You need to complete at least one round to submit a score",
81
+ variant: "destructive",
82
+ });
83
+ return;
84
+ }
85
+
86
+ if (hasSubmitted) {
87
+ toast({
88
+ title: "Error",
89
+ description: "You have already submitted your score for this game",
90
+ variant: "destructive",
91
+ });
92
+ return;
93
+ }
94
+
95
  setIsSubmitting(true);
96
  try {
97
  const { error } = await supabase.from("high_scores").insert({
 
107
  description: "Your score has been recorded",
108
  });
109
 
110
+ setHasSubmitted(true);
111
  await refetch();
112
  setPlayerName("");
113
  } catch (error) {
 
122
  }
123
  };
124
 
125
+ const totalPages = highScores ? Math.ceil(highScores.length / ITEMS_PER_PAGE) : 0;
126
+ const startIndex = (currentPage - 1) * ITEMS_PER_PAGE;
127
+ const paginatedScores = highScores?.slice(startIndex, startIndex + ITEMS_PER_PAGE);
128
+
129
+ const handlePreviousPage = () => {
130
+ if (currentPage > 1) {
131
+ setCurrentPage(p => p - 1);
132
+ }
133
+ };
134
+
135
+ const handleNextPage = () => {
136
+ if (currentPage < totalPages) {
137
+ setCurrentPage(p => p + 1);
138
+ }
139
+ };
140
+
141
  return (
142
  <div className="space-y-6">
143
  <div className="text-center">
 
148
  </p>
149
  </div>
150
 
151
+ {!hasSubmitted && currentScore > 0 && (
152
+ <div className="flex gap-4 mb-6">
153
+ <Input
154
+ placeholder="Enter your name"
155
+ value={playerName}
156
+ onChange={(e) => setPlayerName(e.target.value)}
157
+ className="flex-1"
158
+ />
159
+ <Button
160
+ onClick={handleSubmitScore}
161
+ disabled={isSubmitting || !playerName.trim() || hasSubmitted}
162
+ >
163
+ {isSubmitting ? "Submitting..." : "Submit Score"}
164
+ </Button>
165
+ </div>
166
+ )}
167
 
168
  <div className="rounded-md border">
169
  <Table>
 
176
  </TableRow>
177
  </TableHeader>
178
  <TableBody>
179
+ {paginatedScores?.map((score, index) => (
180
  <TableRow key={score.id}>
181
+ <TableCell>{startIndex + index + 1}</TableCell>
182
  <TableCell>{score.player_name}</TableCell>
183
  <TableCell>{score.score}</TableCell>
184
  <TableCell>{score.avg_words_per_round.toFixed(1)}</TableCell>
185
  </TableRow>
186
  ))}
187
+ {!paginatedScores?.length && (
188
  <TableRow>
189
  <TableCell colSpan={4} className="text-center">
190
  No high scores yet. Be the first!
 
195
  </Table>
196
  </div>
197
 
198
+ {totalPages > 1 && (
199
+ <Pagination>
200
+ <PaginationContent>
201
+ <PaginationItem>
202
+ <PaginationPrevious
203
+ onClick={handlePreviousPage}
204
+ className={currentPage === 1 ? "pointer-events-none opacity-50" : ""}
205
+ />
206
+ </PaginationItem>
207
+ {Array.from({ length: totalPages }, (_, i) => i + 1).map((page) => (
208
+ <PaginationItem key={page}>
209
+ <PaginationLink
210
+ onClick={() => setCurrentPage(page)}
211
+ isActive={currentPage === page}
212
+ >
213
+ {page}
214
+ </PaginationLink>
215
+ </PaginationItem>
216
+ ))}
217
+ <PaginationItem>
218
+ <PaginationNext
219
+ onClick={handleNextPage}
220
+ className={currentPage === totalPages ? "pointer-events-none opacity-50" : ""}
221
+ />
222
+ </PaginationItem>
223
+ </PaginationContent>
224
+ </Pagination>
225
+ )}
226
+
227
  <div className="flex justify-end gap-4">
228
  <Button variant="outline" onClick={onClose}>
229
  Close
src/components/game/GameOver.tsx CHANGED
@@ -3,13 +3,11 @@ 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 (
@@ -23,12 +21,6 @@ export const GameOver = ({
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"
 
3
 
4
  interface GameOverProps {
5
  successfulRounds: number;
 
6
  onPlayAgain: () => void;
7
  }
8
 
9
  export const GameOver = ({
10
  successfulRounds,
 
11
  onPlayAgain,
12
  }: GameOverProps) => {
13
  return (
 
21
  You completed {successfulRounds} rounds successfully!
22
  </p>
23
  <div className="flex gap-4">
 
 
 
 
 
 
24
  <Button
25
  onClick={onPlayAgain}
26
  className="flex-1 bg-primary text-lg hover:bg-primary/90"
src/components/game/GuessDisplay.tsx CHANGED
@@ -1,5 +1,12 @@
1
  import { Button } from "@/components/ui/button";
2
  import { motion } from "framer-motion";
 
 
 
 
 
 
 
3
 
4
  interface GuessDisplayProps {
5
  sentence: string[];
@@ -7,6 +14,8 @@ interface GuessDisplayProps {
7
  currentWord: string;
8
  onNextRound: () => void;
9
  onPlayAgain: () => void;
 
 
10
  }
11
 
12
  export const GuessDisplay = ({
@@ -15,8 +24,11 @@ export const GuessDisplay = ({
15
  currentWord,
16
  onNextRound,
17
  onPlayAgain,
 
 
18
  }: GuessDisplayProps) => {
19
  const isGuessCorrect = () => aiGuess.toLowerCase() === currentWord.toLowerCase();
 
20
 
21
  return (
22
  <motion.div
@@ -42,12 +54,45 @@ export const GuessDisplay = ({
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
  };
 
1
  import { Button } from "@/components/ui/button";
2
  import { motion } from "framer-motion";
3
+ import {
4
+ Dialog,
5
+ DialogContent,
6
+ DialogTrigger,
7
+ } from "@/components/ui/dialog";
8
+ import { HighScoreBoard } from "@/components/HighScoreBoard";
9
+ import { useState } from "react";
10
 
11
  interface GuessDisplayProps {
12
  sentence: string[];
 
14
  currentWord: string;
15
  onNextRound: () => void;
16
  onPlayAgain: () => void;
17
+ currentScore: number;
18
+ avgWordsPerRound: number;
19
  }
20
 
21
  export const GuessDisplay = ({
 
24
  currentWord,
25
  onNextRound,
26
  onPlayAgain,
27
+ currentScore,
28
+ avgWordsPerRound,
29
  }: GuessDisplayProps) => {
30
  const isGuessCorrect = () => aiGuess.toLowerCase() === currentWord.toLowerCase();
31
+ const [isDialogOpen, setIsDialogOpen] = useState(false);
32
 
33
  return (
34
  <motion.div
 
54
  )}
55
  </p>
56
  </div>
57
+ <div className="flex flex-col gap-4">
58
+ {isGuessCorrect() ? (
59
+ <Button
60
+ onClick={onNextRound}
61
+ className="w-full bg-primary text-lg hover:bg-primary/90"
62
+ >
63
+ Next Round ⏎
64
+ </Button>
65
+ ) : (
66
+ <>
67
+ <Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
68
+ <DialogTrigger asChild>
69
+ <Button
70
+ className="w-full bg-secondary text-lg hover:bg-secondary/90"
71
+ >
72
+ View High Scores
73
+ </Button>
74
+ </DialogTrigger>
75
+ <DialogContent className="max-w-md bg-white">
76
+ <HighScoreBoard
77
+ currentScore={currentScore}
78
+ avgWordsPerRound={avgWordsPerRound}
79
+ onClose={() => setIsDialogOpen(false)}
80
+ onPlayAgain={() => {
81
+ setIsDialogOpen(false);
82
+ onPlayAgain();
83
+ }}
84
+ />
85
+ </DialogContent>
86
+ </Dialog>
87
+ <Button
88
+ onClick={onPlayAgain}
89
+ className="w-full bg-primary text-lg hover:bg-primary/90"
90
+ >
91
+ Play Again ⏎
92
+ </Button>
93
+ </>
94
+ )}
95
+ </div>
96
  </motion.div>
97
  );
98
  };
src/components/game/SentenceBuilder.tsx CHANGED
@@ -1,7 +1,7 @@
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;
@@ -25,19 +25,53 @@ export const SentenceBuilder = ({
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
 
@@ -53,8 +87,15 @@ export const SentenceBuilder = ({
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>
@@ -89,9 +130,9 @@ export const SentenceBuilder = ({
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>
 
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, useState } from "react";
5
 
6
  interface SentenceBuilderProps {
7
  currentWord: string;
 
25
  onMakeGuess,
26
  }: SentenceBuilderProps) => {
27
  const inputRef = useRef<HTMLInputElement>(null);
28
+ const [imageLoaded, setImageLoaded] = useState(false);
29
+ const imagePath = `/think_in_sync_assets/${currentWord.toLowerCase()}.jpg`;
30
 
31
+ useEffect(() => {
32
+ const img = new Image();
33
+ img.onload = () => setImageLoaded(true);
34
+ img.src = imagePath;
35
+ console.log("Attempting to load image:", imagePath);
36
+ }, [imagePath]);
37
+
38
+ // Focus input on initial render
39
  useEffect(() => {
40
  setTimeout(() => {
41
  inputRef.current?.focus();
42
  }, 100);
43
  }, []);
44
 
45
+ // Focus input after AI finishes thinking
46
+ useEffect(() => {
47
+ if (!isAiThinking && sentence.length > 0 && sentence.length % 2 === 0) {
48
+ setTimeout(() => {
49
+ inputRef.current?.focus();
50
+ }, 100);
51
+ }
52
+ }, [isAiThinking, sentence.length]);
53
+
54
  const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
55
  if (e.shiftKey && e.key === 'Enter') {
56
  e.preventDefault();
57
+ handleMakeGuess();
58
+ }
59
+ };
60
+
61
+ const handleMakeGuess = () => {
62
+ if (playerInput.trim()) {
63
+ // Create a synthetic form event to add the current word
64
+ const syntheticEvent = {
65
+ preventDefault: () => {},
66
+ } as React.FormEvent;
67
+ onSubmitWord(syntheticEvent);
68
+
69
+ // Wait a brief moment for the state to update before making the guess
70
+ setTimeout(() => {
71
  onMakeGuess();
72
+ }, 100);
73
+ } else {
74
+ onMakeGuess();
75
  }
76
  };
77
 
 
87
  <p className="mb-6 text-sm text-gray-600">
88
  Take turns with AI to describe your word without using the word itself!
89
  </p>
90
+ <div className="mb-4 overflow-hidden rounded-lg bg-secondary/10">
91
+ {imageLoaded && (
92
+ <img
93
+ src={imagePath}
94
+ alt={currentWord}
95
+ className="mx-auto h-48 w-full object-cover"
96
+ />
97
+ )}
98
+ <p className="p-4 text-2xl font-bold tracking-wider text-secondary">
99
  {currentWord}
100
  </p>
101
  </div>
 
130
  </Button>
131
  <Button
132
  type="button"
133
+ onClick={handleMakeGuess}
134
  className="flex-1 bg-secondary text-lg hover:bg-secondary/90"
135
+ disabled={(!sentence.length && !playerInput.trim()) || isAiThinking}
136
  >
137
  {isAiThinking ? "AI is thinking..." : "Make AI Guess ⇧⏎"}
138
  </Button>
src/components/game/WelcomeScreen.tsx CHANGED
@@ -12,9 +12,11 @@ export const WelcomeScreen = ({ onStart }: WelcomeScreenProps) => {
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}
@@ -24,4 +26,4 @@ export const WelcomeScreen = ({ onStart }: WelcomeScreenProps) => {
24
  </Button>
25
  </motion.div>
26
  );
27
- };
 
12
  animate={{ opacity: 1 }}
13
  className="text-center"
14
  >
15
+ <h1 className="mb-6 text-4xl font-bold text-gray-900">Think in Sync</h1>
16
  <p className="mb-8 text-gray-600">
17
+ This game is a variation of a classical childrens game.
18
+ You will be given a secret word. Your goal is to describe this secret word so that an AI can guess it.
19
+ However, you are only allowed to say one word at the time, taking turns with another AI.
20
  </p>
21
  <Button
22
  onClick={onStart}
 
26
  </Button>
27
  </motion.div>
28
  );
29
+ };
src/components/game/WordDisplay.tsx CHANGED
@@ -1,5 +1,6 @@
1
  import { Button } from "@/components/ui/button";
2
  import { motion } from "framer-motion";
 
3
 
4
  interface WordDisplayProps {
5
  currentWord: string;
@@ -8,6 +9,16 @@ interface WordDisplayProps {
8
  }
9
 
10
  export const WordDisplay = ({ currentWord, successfulRounds, onContinue }: WordDisplayProps) => {
 
 
 
 
 
 
 
 
 
 
11
  return (
12
  <motion.div
13
  initial={{ opacity: 0 }}
@@ -15,8 +26,15 @@ export const WordDisplay = ({ currentWord, successfulRounds, onContinue }: WordD
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>
 
1
  import { Button } from "@/components/ui/button";
2
  import { motion } from "framer-motion";
3
+ import { useEffect, useState } from "react";
4
 
5
  interface WordDisplayProps {
6
  currentWord: string;
 
9
  }
10
 
11
  export const WordDisplay = ({ currentWord, successfulRounds, onContinue }: WordDisplayProps) => {
12
+ const [imageLoaded, setImageLoaded] = useState(false);
13
+ const imagePath = `/think_in_sync_assets/${currentWord.toLowerCase()}.jpg`;
14
+
15
+ useEffect(() => {
16
+ const img = new Image();
17
+ img.onload = () => setImageLoaded(true);
18
+ img.src = imagePath;
19
+ console.log("Attempting to load image:", imagePath);
20
+ }, [imagePath]);
21
+
22
  return (
23
  <motion.div
24
  initial={{ opacity: 0 }}
 
26
  className="text-center"
27
  >
28
  <h2 className="mb-4 text-2xl font-semibold text-gray-900">Your Word</h2>
29
+ <div className="mb-4 overflow-hidden rounded-lg bg-secondary/10">
30
+ {imageLoaded && (
31
+ <img
32
+ src={imagePath}
33
+ alt={currentWord}
34
+ className="mx-auto h-48 w-full object-cover"
35
+ />
36
+ )}
37
+ <p className="p-6 text-4xl font-bold tracking-wider text-secondary">
38
  {currentWord}
39
  </p>
40
  </div>
src/services/mistralService.ts CHANGED
@@ -1,8 +1,13 @@
1
  import { supabase } from "@/integrations/supabase/client";
2
 
3
  export const generateAIResponse = async (currentWord: string, currentSentence: string[]): Promise<string> => {
 
 
4
  const { data, error } = await supabase.functions.invoke('generate-word', {
5
- body: { currentWord, currentSentence }
 
 
 
6
  });
7
 
8
  if (error) {
@@ -13,7 +18,8 @@ export const generateAIResponse = async (currentWord: string, currentSentence: s
13
  throw error;
14
  }
15
 
16
- if (!data.word) {
 
17
  throw new Error('No word generated');
18
  }
19
 
@@ -22,6 +28,8 @@ export const generateAIResponse = async (currentWord: string, currentSentence: s
22
  };
23
 
24
  export const guessWord = async (sentence: string): Promise<string> => {
 
 
25
  const { data, error } = await supabase.functions.invoke('guess-word', {
26
  body: { sentence }
27
  });
@@ -34,7 +42,8 @@ export const guessWord = async (sentence: string): Promise<string> => {
34
  throw error;
35
  }
36
 
37
- if (!data.guess) {
 
38
  throw new Error('No guess generated');
39
  }
40
 
 
1
  import { supabase } from "@/integrations/supabase/client";
2
 
3
  export const generateAIResponse = async (currentWord: string, currentSentence: string[]): Promise<string> => {
4
+ console.log('Calling generate-word function with:', { currentWord, currentSentence });
5
+
6
  const { data, error } = await supabase.functions.invoke('generate-word', {
7
+ body: {
8
+ currentWord,
9
+ currentSentence: currentSentence.join(' ')
10
+ }
11
  });
12
 
13
  if (error) {
 
18
  throw error;
19
  }
20
 
21
+ if (!data?.word) {
22
+ console.error('No word generated in response:', data);
23
  throw new Error('No word generated');
24
  }
25
 
 
28
  };
29
 
30
  export const guessWord = async (sentence: string): Promise<string> => {
31
+ console.log('Calling guess-word function with sentence:', sentence);
32
+
33
  const { data, error } = await supabase.functions.invoke('guess-word', {
34
  body: { sentence }
35
  });
 
42
  throw error;
43
  }
44
 
45
+ if (!data?.guess) {
46
+ console.error('No guess generated in response:', data);
47
  throw new Error('No guess generated');
48
  }
49
 
supabase/functions/generate-word/index.ts CHANGED
@@ -15,6 +15,9 @@ serve(async (req) => {
15
  const { currentWord, currentSentence } = await req.json();
16
  console.log('Generating word for:', { currentWord, currentSentence });
17
 
 
 
 
18
  const client = new Mistral({
19
  apiKey: Deno.env.get('MISTRAL_API_KEY'),
20
  });
@@ -33,7 +36,7 @@ serve(async (req) => {
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
  ],
@@ -45,9 +48,8 @@ serve(async (req) => {
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
 
15
  const { currentWord, currentSentence } = await req.json();
16
  console.log('Generating word for:', { currentWord, currentSentence });
17
 
18
+ // currentSentence is already a string from the client
19
+ const existingSentence = currentSentence || '';
20
+
21
  const client = new Mistral({
22
  apiKey: Deno.env.get('MISTRAL_API_KEY'),
23
  });
 
36
  role: "system",
37
  content: `You are helping in a word game. The secret word is "${currentWord}".
38
  Your task is to find a sentence to describe this word without using it directly.
39
+ Answer with a complete, grammatically correct sentence that starts with "${existingSentence}".
40
  Do not add quotes or backticks. Just answer with the sentence.`
41
  }
42
  ],
 
48
  console.log('AI full response:', aiResponse);
49
 
50
  // Extract the new word by comparing with the existing sentence
 
51
  const newWord = aiResponse
52
+ .slice(existingSentence.length)
53
  .trim()
54
  .split(' ')[0]
55
  .replace(/[.,!?]$/, ''); // Remove any punctuation at the end