Felix Zieger
commited on
Commit
·
831f7e7
1
Parent(s):
34f302f
multi language support
Browse files- src/App.tsx +13 -10
- src/components/GameContainer.tsx +13 -7
- src/components/HighScoreBoard.tsx +27 -40
- src/components/game/GuessDisplay.tsx +13 -13
- src/components/game/LanguageSelector.tsx +32 -0
- src/components/game/SentenceBuilder.tsx +12 -15
- src/components/game/ThemeSelector.tsx +44 -19
- src/components/game/WelcomeScreen.tsx +35 -33
- src/contexts/LanguageContext.tsx +38 -0
- src/hooks/useTranslation.ts +8 -0
- src/i18n/translations/de.ts +94 -0
- src/i18n/translations/en.ts +94 -0
- src/i18n/translations/es.ts +94 -0
- src/i18n/translations/fr.ts +94 -0
- src/i18n/translations/index.ts +16 -0
- src/i18n/translations/it.ts +94 -0
- src/services/themeService.ts +4 -3
- supabase/functions/generate-themed-word/index.ts +30 -15
- supabase/functions/generate-word/index.ts +35 -16
- supabase/functions/guess-word/index.ts +30 -12
src/App.tsx
CHANGED
@@ -3,22 +3,25 @@ import { Toaster as Sonner } from "@/components/ui/sonner";
|
|
3 |
import { TooltipProvider } from "@/components/ui/tooltip";
|
4 |
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
5 |
import { BrowserRouter, Routes, Route } from "react-router-dom";
|
|
|
6 |
import Index from "./pages/Index";
|
7 |
|
8 |
const queryClient = new QueryClient();
|
9 |
|
10 |
const App = () => (
|
11 |
<QueryClientProvider client={queryClient}>
|
12 |
-
<
|
13 |
-
<
|
14 |
-
|
15 |
-
|
16 |
-
<
|
17 |
-
<
|
18 |
-
|
19 |
-
|
20 |
-
|
|
|
|
|
21 |
</QueryClientProvider>
|
22 |
);
|
23 |
|
24 |
-
export default App;
|
|
|
3 |
import { TooltipProvider } from "@/components/ui/tooltip";
|
4 |
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
5 |
import { BrowserRouter, Routes, Route } from "react-router-dom";
|
6 |
+
import { LanguageProvider } from "./contexts/LanguageContext";
|
7 |
import Index from "./pages/Index";
|
8 |
|
9 |
const queryClient = new QueryClient();
|
10 |
|
11 |
const App = () => (
|
12 |
<QueryClientProvider client={queryClient}>
|
13 |
+
<LanguageProvider>
|
14 |
+
<TooltipProvider>
|
15 |
+
<Toaster />
|
16 |
+
<Sonner />
|
17 |
+
<BrowserRouter>
|
18 |
+
<Routes>
|
19 |
+
<Route path="/" element={<Index />} />
|
20 |
+
</Routes>
|
21 |
+
</BrowserRouter>
|
22 |
+
</TooltipProvider>
|
23 |
+
</LanguageProvider>
|
24 |
</QueryClientProvider>
|
25 |
);
|
26 |
|
27 |
+
export default App;
|
src/components/GameContainer.tsx
CHANGED
@@ -1,4 +1,4 @@
|
|
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";
|
@@ -9,6 +9,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 |
|
@@ -24,6 +26,8 @@ export const GameContainer = () => {
|
|
24 |
const [totalWords, setTotalWords] = useState<number>(0);
|
25 |
const [usedWords, setUsedWords] = useState<string[]>([]);
|
26 |
const { toast } = useToast();
|
|
|
|
|
27 |
|
28 |
useEffect(() => {
|
29 |
const handleKeyPress = (e: KeyboardEvent) => {
|
@@ -52,13 +56,15 @@ export const GameContainer = () => {
|
|
52 |
const handleThemeSelect = async (theme: string) => {
|
53 |
setCurrentTheme(theme);
|
54 |
try {
|
55 |
-
const word = theme === "standard" ?
|
|
|
|
|
56 |
setCurrentWord(word);
|
57 |
setGameState("building-sentence");
|
58 |
setSuccessfulRounds(0);
|
59 |
setTotalWords(0);
|
60 |
-
setUsedWords([word]);
|
61 |
-
console.log("Game started with word:", word, "theme:", theme);
|
62 |
} catch (error) {
|
63 |
console.error('Error getting themed word:', error);
|
64 |
toast({
|
@@ -88,8 +94,8 @@ export const GameContainer = () => {
|
|
88 |
} catch (error) {
|
89 |
console.error('Error in AI turn:', error);
|
90 |
toast({
|
91 |
-
title:
|
92 |
-
description:
|
93 |
variant: "default",
|
94 |
});
|
95 |
} finally {
|
@@ -224,4 +230,4 @@ export const GameContainer = () => {
|
|
224 |
</motion.div>
|
225 |
</div>
|
226 |
);
|
227 |
-
};
|
|
|
1 |
+
import { useState, KeyboardEvent, useEffect, useContext } from "react";
|
2 |
import { getRandomWord } from "@/lib/words";
|
3 |
import { motion } from "framer-motion";
|
4 |
import { generateAIResponse, guessWord } from "@/services/mistralService";
|
|
|
9 |
import { SentenceBuilder } from "./game/SentenceBuilder";
|
10 |
import { GuessDisplay } from "./game/GuessDisplay";
|
11 |
import { GameOver } from "./game/GameOver";
|
12 |
+
import { useTranslation } from "@/hooks/useTranslation";
|
13 |
+
import { LanguageContext } from "@/contexts/LanguageContext";
|
14 |
|
15 |
type GameState = "welcome" | "theme-selection" | "building-sentence" | "showing-guess" | "game-over";
|
16 |
|
|
|
26 |
const [totalWords, setTotalWords] = useState<number>(0);
|
27 |
const [usedWords, setUsedWords] = useState<string[]>([]);
|
28 |
const { toast } = useToast();
|
29 |
+
const t = useTranslation();
|
30 |
+
const { language } = useContext(LanguageContext);
|
31 |
|
32 |
useEffect(() => {
|
33 |
const handleKeyPress = (e: KeyboardEvent) => {
|
|
|
56 |
const handleThemeSelect = async (theme: string) => {
|
57 |
setCurrentTheme(theme);
|
58 |
try {
|
59 |
+
const word = theme === "standard" ?
|
60 |
+
getRandomWord() :
|
61 |
+
await getThemedWord(theme, usedWords, language);
|
62 |
setCurrentWord(word);
|
63 |
setGameState("building-sentence");
|
64 |
setSuccessfulRounds(0);
|
65 |
setTotalWords(0);
|
66 |
+
setUsedWords([word]);
|
67 |
+
console.log("Game started with word:", word, "theme:", theme, "language:", language);
|
68 |
} catch (error) {
|
69 |
console.error('Error getting themed word:', error);
|
70 |
toast({
|
|
|
94 |
} catch (error) {
|
95 |
console.error('Error in AI turn:', error);
|
96 |
toast({
|
97 |
+
title: t.game.aiThinking,
|
98 |
+
description: t.game.aiDelayed,
|
99 |
variant: "default",
|
100 |
});
|
101 |
} finally {
|
|
|
230 |
</motion.div>
|
231 |
</div>
|
232 |
);
|
233 |
+
};
|
src/components/HighScoreBoard.tsx
CHANGED
@@ -20,6 +20,7 @@ import {
|
|
20 |
PaginationNext,
|
21 |
PaginationPrevious,
|
22 |
} from "@/components/ui/pagination";
|
|
|
23 |
|
24 |
interface HighScore {
|
25 |
id: string;
|
@@ -62,6 +63,7 @@ export const HighScoreBoard = ({
|
|
62 |
const [hasSubmitted, setHasSubmitted] = useState(false);
|
63 |
const [currentPage, setCurrentPage] = useState(1);
|
64 |
const { toast } = useToast();
|
|
|
65 |
|
66 |
const { data: highScores, refetch } = useQuery({
|
67 |
queryKey: ["highScores"],
|
@@ -78,11 +80,10 @@ export const HighScoreBoard = ({
|
|
78 |
});
|
79 |
|
80 |
const handleSubmitScore = async () => {
|
81 |
-
// Validate player name (only alphanumeric characters allowed)
|
82 |
if (!playerName.trim() || !/^[a-zA-Z0-9]+$/.test(playerName.trim())) {
|
83 |
toast({
|
84 |
-
title:
|
85 |
-
description:
|
86 |
variant: "destructive",
|
87 |
});
|
88 |
return;
|
@@ -90,8 +91,8 @@ export const HighScoreBoard = ({
|
|
90 |
|
91 |
if (currentScore < 1) {
|
92 |
toast({
|
93 |
-
title:
|
94 |
-
description:
|
95 |
variant: "destructive",
|
96 |
});
|
97 |
return;
|
@@ -99,8 +100,8 @@ export const HighScoreBoard = ({
|
|
99 |
|
100 |
if (hasSubmitted) {
|
101 |
toast({
|
102 |
-
title:
|
103 |
-
description:
|
104 |
variant: "destructive",
|
105 |
});
|
106 |
return;
|
@@ -108,7 +109,6 @@ export const HighScoreBoard = ({
|
|
108 |
|
109 |
setIsSubmitting(true);
|
110 |
try {
|
111 |
-
// Check if player already exists
|
112 |
const { data: existingScores } = await supabase
|
113 |
.from("high_scores")
|
114 |
.select("*")
|
@@ -117,7 +117,6 @@ export const HighScoreBoard = ({
|
|
117 |
const existingScore = existingScores?.[0];
|
118 |
|
119 |
if (existingScore) {
|
120 |
-
// Only update if the new score is better
|
121 |
if (currentScore > existingScore.score) {
|
122 |
const { error } = await supabase
|
123 |
.from("high_scores")
|
@@ -130,20 +129,20 @@ export const HighScoreBoard = ({
|
|
130 |
if (error) throw error;
|
131 |
|
132 |
toast({
|
133 |
-
title:
|
134 |
-
description:
|
135 |
});
|
136 |
} else {
|
137 |
toast({
|
138 |
-
title:
|
139 |
-
|
|
|
140 |
variant: "destructive",
|
141 |
});
|
142 |
setIsSubmitting(false);
|
143 |
return;
|
144 |
}
|
145 |
} else {
|
146 |
-
// Insert new score
|
147 |
const { error } = await supabase.from("high_scores").insert({
|
148 |
player_name: playerName.trim(),
|
149 |
score: currentScore,
|
@@ -151,11 +150,6 @@ export const HighScoreBoard = ({
|
|
151 |
});
|
152 |
|
153 |
if (error) throw error;
|
154 |
-
|
155 |
-
toast({
|
156 |
-
title: "Success!",
|
157 |
-
description: "Your score has been recorded",
|
158 |
-
});
|
159 |
}
|
160 |
|
161 |
setHasSubmitted(true);
|
@@ -164,8 +158,8 @@ export const HighScoreBoard = ({
|
|
164 |
} catch (error) {
|
165 |
console.error("Error submitting score:", error);
|
166 |
toast({
|
167 |
-
title:
|
168 |
-
description:
|
169 |
variant: "destructive",
|
170 |
});
|
171 |
} finally {
|
@@ -212,20 +206,19 @@ export const HighScoreBoard = ({
|
|
212 |
return (
|
213 |
<div className="space-y-6">
|
214 |
<div className="text-center">
|
215 |
-
<h2 className="text-2xl font-bold mb-2">
|
216 |
<p className="text-gray-600">
|
217 |
-
|
218 |
-
{currentScore > 0 && ` (${avgWordsPerRound.toFixed(1)}
|
219 |
</p>
|
220 |
</div>
|
221 |
|
222 |
{!hasSubmitted && currentScore > 0 && (
|
223 |
<div className="flex gap-4 mb-6">
|
224 |
<Input
|
225 |
-
placeholder=
|
226 |
value={playerName}
|
227 |
onChange={(e) => {
|
228 |
-
// Only allow alphanumeric input
|
229 |
const value = e.target.value.replace(/[^a-zA-Z0-9]/g, '');
|
230 |
setPlayerName(value);
|
231 |
}}
|
@@ -237,7 +230,7 @@ export const HighScoreBoard = ({
|
|
237 |
onClick={handleSubmitScore}
|
238 |
disabled={isSubmitting || !playerName.trim() || hasSubmitted}
|
239 |
>
|
240 |
-
{isSubmitting ?
|
241 |
</Button>
|
242 |
</div>
|
243 |
)}
|
@@ -246,10 +239,10 @@ export const HighScoreBoard = ({
|
|
246 |
<Table>
|
247 |
<TableHeader>
|
248 |
<TableRow>
|
249 |
-
<TableHead>
|
250 |
-
<TableHead>
|
251 |
-
<TableHead>
|
252 |
-
<TableHead>
|
253 |
</TableRow>
|
254 |
</TableHeader>
|
255 |
<TableBody>
|
@@ -271,7 +264,7 @@ export const HighScoreBoard = ({
|
|
271 |
{!paginatedScores?.length && (
|
272 |
<TableRow>
|
273 |
<TableCell colSpan={4} className="text-center">
|
274 |
-
|
275 |
</TableCell>
|
276 |
</TableRow>
|
277 |
)}
|
@@ -287,7 +280,7 @@ export const HighScoreBoard = ({
|
|
287 |
onClick={handlePreviousPage}
|
288 |
className={currentPage === 1 ? "pointer-events-none opacity-50" : ""}
|
289 |
>
|
290 |
-
<span className="hidden sm:inline">
|
291 |
<span className="text-xs text-muted-foreground ml-1">←</span>
|
292 |
</PaginationPrevious>
|
293 |
</PaginationItem>
|
@@ -306,19 +299,13 @@ export const HighScoreBoard = ({
|
|
306 |
onClick={handleNextPage}
|
307 |
className={currentPage === totalPages ? "pointer-events-none opacity-50" : ""}
|
308 |
>
|
309 |
-
<span className="hidden sm:inline">
|
310 |
<span className="text-xs text-muted-foreground ml-1">→</span>
|
311 |
</PaginationNext>
|
312 |
</PaginationItem>
|
313 |
</PaginationContent>
|
314 |
</Pagination>
|
315 |
)}
|
316 |
-
|
317 |
-
<div className="flex justify-end">
|
318 |
-
<Button variant="outline" onClick={onClose}>
|
319 |
-
Close <span className="text-xs text-muted-foreground ml-1">Esc</span>
|
320 |
-
</Button>
|
321 |
-
</div>
|
322 |
</div>
|
323 |
);
|
324 |
};
|
|
|
20 |
PaginationNext,
|
21 |
PaginationPrevious,
|
22 |
} from "@/components/ui/pagination";
|
23 |
+
import { useTranslation } from "@/hooks/useTranslation";
|
24 |
|
25 |
interface HighScore {
|
26 |
id: string;
|
|
|
63 |
const [hasSubmitted, setHasSubmitted] = useState(false);
|
64 |
const [currentPage, setCurrentPage] = useState(1);
|
65 |
const { toast } = useToast();
|
66 |
+
const t = useTranslation();
|
67 |
|
68 |
const { data: highScores, refetch } = useQuery({
|
69 |
queryKey: ["highScores"],
|
|
|
80 |
});
|
81 |
|
82 |
const handleSubmitScore = async () => {
|
|
|
83 |
if (!playerName.trim() || !/^[a-zA-Z0-9]+$/.test(playerName.trim())) {
|
84 |
toast({
|
85 |
+
title: t.leaderboard.error.invalidName,
|
86 |
+
description: t.leaderboard.error.invalidName,
|
87 |
variant: "destructive",
|
88 |
});
|
89 |
return;
|
|
|
91 |
|
92 |
if (currentScore < 1) {
|
93 |
toast({
|
94 |
+
title: t.leaderboard.error.noRounds,
|
95 |
+
description: t.leaderboard.error.noRounds,
|
96 |
variant: "destructive",
|
97 |
});
|
98 |
return;
|
|
|
100 |
|
101 |
if (hasSubmitted) {
|
102 |
toast({
|
103 |
+
title: t.leaderboard.error.alreadySubmitted,
|
104 |
+
description: t.leaderboard.error.alreadySubmitted,
|
105 |
variant: "destructive",
|
106 |
});
|
107 |
return;
|
|
|
109 |
|
110 |
setIsSubmitting(true);
|
111 |
try {
|
|
|
112 |
const { data: existingScores } = await supabase
|
113 |
.from("high_scores")
|
114 |
.select("*")
|
|
|
117 |
const existingScore = existingScores?.[0];
|
118 |
|
119 |
if (existingScore) {
|
|
|
120 |
if (currentScore > existingScore.score) {
|
121 |
const { error } = await supabase
|
122 |
.from("high_scores")
|
|
|
129 |
if (error) throw error;
|
130 |
|
131 |
toast({
|
132 |
+
title: t.leaderboard.error.newHighScore,
|
133 |
+
description: t.leaderboard.error.beatRecord.replace("{score}", String(existingScore.score)),
|
134 |
});
|
135 |
} else {
|
136 |
toast({
|
137 |
+
title: t.leaderboard.error.notHigher
|
138 |
+
.replace("{current}", String(currentScore))
|
139 |
+
.replace("{best}", String(existingScore.score)),
|
140 |
variant: "destructive",
|
141 |
});
|
142 |
setIsSubmitting(false);
|
143 |
return;
|
144 |
}
|
145 |
} else {
|
|
|
146 |
const { error } = await supabase.from("high_scores").insert({
|
147 |
player_name: playerName.trim(),
|
148 |
score: currentScore,
|
|
|
150 |
});
|
151 |
|
152 |
if (error) throw error;
|
|
|
|
|
|
|
|
|
|
|
153 |
}
|
154 |
|
155 |
setHasSubmitted(true);
|
|
|
158 |
} catch (error) {
|
159 |
console.error("Error submitting score:", error);
|
160 |
toast({
|
161 |
+
title: t.leaderboard.error.submitError,
|
162 |
+
description: t.leaderboard.error.submitError,
|
163 |
variant: "destructive",
|
164 |
});
|
165 |
} finally {
|
|
|
206 |
return (
|
207 |
<div className="space-y-6">
|
208 |
<div className="text-center">
|
209 |
+
<h2 className="text-2xl font-bold mb-2">{t.leaderboard.title}</h2>
|
210 |
<p className="text-gray-600">
|
211 |
+
{t.leaderboard.yourScore}: {currentScore} {t.leaderboard.roundCount}
|
212 |
+
{currentScore > 0 && ` (${avgWordsPerRound.toFixed(1)} ${t.leaderboard.wordsPerRound})`}
|
213 |
</p>
|
214 |
</div>
|
215 |
|
216 |
{!hasSubmitted && currentScore > 0 && (
|
217 |
<div className="flex gap-4 mb-6">
|
218 |
<Input
|
219 |
+
placeholder={t.leaderboard.enterName}
|
220 |
value={playerName}
|
221 |
onChange={(e) => {
|
|
|
222 |
const value = e.target.value.replace(/[^a-zA-Z0-9]/g, '');
|
223 |
setPlayerName(value);
|
224 |
}}
|
|
|
230 |
onClick={handleSubmitScore}
|
231 |
disabled={isSubmitting || !playerName.trim() || hasSubmitted}
|
232 |
>
|
233 |
+
{isSubmitting ? t.leaderboard.submitting : t.leaderboard.submit}
|
234 |
</Button>
|
235 |
</div>
|
236 |
)}
|
|
|
239 |
<Table>
|
240 |
<TableHeader>
|
241 |
<TableRow>
|
242 |
+
<TableHead>{t.leaderboard.rank}</TableHead>
|
243 |
+
<TableHead>{t.leaderboard.player}</TableHead>
|
244 |
+
<TableHead>{t.leaderboard.roundsColumn}</TableHead>
|
245 |
+
<TableHead>{t.leaderboard.avgWords}</TableHead>
|
246 |
</TableRow>
|
247 |
</TableHeader>
|
248 |
<TableBody>
|
|
|
264 |
{!paginatedScores?.length && (
|
265 |
<TableRow>
|
266 |
<TableCell colSpan={4} className="text-center">
|
267 |
+
{t.leaderboard.noScores}
|
268 |
</TableCell>
|
269 |
</TableRow>
|
270 |
)}
|
|
|
280 |
onClick={handlePreviousPage}
|
281 |
className={currentPage === 1 ? "pointer-events-none opacity-50" : ""}
|
282 |
>
|
283 |
+
<span className="hidden sm:inline">{t.leaderboard.previous}</span>
|
284 |
<span className="text-xs text-muted-foreground ml-1">←</span>
|
285 |
</PaginationPrevious>
|
286 |
</PaginationItem>
|
|
|
299 |
onClick={handleNextPage}
|
300 |
className={currentPage === totalPages ? "pointer-events-none opacity-50" : ""}
|
301 |
>
|
302 |
+
<span className="hidden sm:inline">{t.leaderboard.next}</span>
|
303 |
<span className="text-xs text-muted-foreground ml-1">→</span>
|
304 |
</PaginationNext>
|
305 |
</PaginationItem>
|
306 |
</PaginationContent>
|
307 |
</Pagination>
|
308 |
)}
|
|
|
|
|
|
|
|
|
|
|
|
|
309 |
</div>
|
310 |
);
|
311 |
};
|
src/components/game/GuessDisplay.tsx
CHANGED
@@ -7,6 +7,7 @@ import {
|
|
7 |
} from "@/components/ui/dialog";
|
8 |
import { HighScoreBoard } from "@/components/HighScoreBoard";
|
9 |
import { useState } from "react";
|
|
|
10 |
|
11 |
interface GuessDisplayProps {
|
12 |
sentence: string[];
|
@@ -29,6 +30,7 @@ export const GuessDisplay = ({
|
|
29 |
}: GuessDisplayProps) => {
|
30 |
const isGuessCorrect = () => aiGuess.toLowerCase() === currentWord.toLowerCase();
|
31 |
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
|
|
32 |
|
33 |
return (
|
34 |
<motion.div
|
@@ -36,21 +38,19 @@ export const GuessDisplay = ({
|
|
36 |
animate={{ opacity: 1 }}
|
37 |
className="text-center"
|
38 |
>
|
39 |
-
<h2 className="mb-4 text-2xl font-semibold text-gray-900">
|
40 |
<div className="mb-6 rounded-lg bg-gray-50 p-4">
|
41 |
<p className="mb-4 text-lg text-gray-800">
|
42 |
-
|
|
|
|
|
|
|
43 |
</p>
|
44 |
-
<p className="text-xl font-bold text-primary">AI guessed: {aiGuess}</p>
|
45 |
<p className="mt-4 text-lg">
|
46 |
{isGuessCorrect() ? (
|
47 |
-
<span className="text-green-600">
|
48 |
-
Correct guess! 🎉 Ready for the next round? Press Enter
|
49 |
-
</span>
|
50 |
) : (
|
51 |
-
<span className="text-red-600">
|
52 |
-
Game Over! Press Enter to play again
|
53 |
-
</span>
|
54 |
)}
|
55 |
</p>
|
56 |
</div>
|
@@ -60,7 +60,7 @@ export const GuessDisplay = ({
|
|
60 |
onClick={onNextRound}
|
61 |
className="w-full bg-primary text-lg hover:bg-primary/90"
|
62 |
>
|
63 |
-
|
64 |
</Button>
|
65 |
) : (
|
66 |
<>
|
@@ -69,7 +69,7 @@ export const GuessDisplay = ({
|
|
69 |
<Button
|
70 |
className="w-full bg-secondary text-lg hover:bg-secondary/90"
|
71 |
>
|
72 |
-
|
73 |
</Button>
|
74 |
</DialogTrigger>
|
75 |
<DialogContent className="max-w-md bg-white">
|
@@ -88,11 +88,11 @@ export const GuessDisplay = ({
|
|
88 |
onClick={onPlayAgain}
|
89 |
className="w-full bg-primary text-lg hover:bg-primary/90"
|
90 |
>
|
91 |
-
|
92 |
</Button>
|
93 |
</>
|
94 |
)}
|
95 |
</div>
|
96 |
</motion.div>
|
97 |
);
|
98 |
-
};
|
|
|
7 |
} from "@/components/ui/dialog";
|
8 |
import { HighScoreBoard } from "@/components/HighScoreBoard";
|
9 |
import { useState } from "react";
|
10 |
+
import { useTranslation } from "@/hooks/useTranslation";
|
11 |
|
12 |
interface GuessDisplayProps {
|
13 |
sentence: string[];
|
|
|
30 |
}: GuessDisplayProps) => {
|
31 |
const isGuessCorrect = () => aiGuess.toLowerCase() === currentWord.toLowerCase();
|
32 |
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
33 |
+
const t = useTranslation();
|
34 |
|
35 |
return (
|
36 |
<motion.div
|
|
|
38 |
animate={{ opacity: 1 }}
|
39 |
className="text-center"
|
40 |
>
|
41 |
+
<h2 className="mb-4 text-2xl font-semibold text-gray-900">{t.guess.title}</h2>
|
42 |
<div className="mb-6 rounded-lg bg-gray-50 p-4">
|
43 |
<p className="mb-4 text-lg text-gray-800">
|
44 |
+
{t.guess.sentence}: {sentence.join(" ")}
|
45 |
+
</p>
|
46 |
+
<p className="text-xl font-bold text-primary">
|
47 |
+
{t.guess.aiGuessed}: {aiGuess}
|
48 |
</p>
|
|
|
49 |
<p className="mt-4 text-lg">
|
50 |
{isGuessCorrect() ? (
|
51 |
+
<span className="text-green-600">{t.guess.correct}</span>
|
|
|
|
|
52 |
) : (
|
53 |
+
<span className="text-red-600">{t.guess.incorrect}</span>
|
|
|
|
|
54 |
)}
|
55 |
</p>
|
56 |
</div>
|
|
|
60 |
onClick={onNextRound}
|
61 |
className="w-full bg-primary text-lg hover:bg-primary/90"
|
62 |
>
|
63 |
+
{t.guess.nextRound} ⏎
|
64 |
</Button>
|
65 |
) : (
|
66 |
<>
|
|
|
69 |
<Button
|
70 |
className="w-full bg-secondary text-lg hover:bg-secondary/90"
|
71 |
>
|
72 |
+
{t.guess.viewLeaderboard} 🏆
|
73 |
</Button>
|
74 |
</DialogTrigger>
|
75 |
<DialogContent className="max-w-md bg-white">
|
|
|
88 |
onClick={onPlayAgain}
|
89 |
className="w-full bg-primary text-lg hover:bg-primary/90"
|
90 |
>
|
91 |
+
{t.guess.playAgain} ⏎
|
92 |
</Button>
|
93 |
</>
|
94 |
)}
|
95 |
</div>
|
96 |
</motion.div>
|
97 |
);
|
98 |
+
};
|
src/components/game/LanguageSelector.tsx
ADDED
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { Button } from "@/components/ui/button";
|
2 |
+
import { useContext } from "react";
|
3 |
+
import { LanguageContext } from "@/contexts/LanguageContext";
|
4 |
+
import { Language } from "@/i18n/translations";
|
5 |
+
|
6 |
+
const languages: { code: Language; name: string; flag: string }[] = [
|
7 |
+
{ code: 'en', name: 'English', flag: '🇬🇧' },
|
8 |
+
{ code: 'fr', name: 'Français', flag: '🇫🇷' },
|
9 |
+
{ code: 'de', name: 'Deutsch', flag: '🇩🇪' },
|
10 |
+
{ code: 'it', name: 'Italiano', flag: '🇮🇹' },
|
11 |
+
{ code: 'es', name: 'Español', flag: '🇪🇸' },
|
12 |
+
];
|
13 |
+
|
14 |
+
export const LanguageSelector = () => {
|
15 |
+
const { language, setLanguage } = useContext(LanguageContext);
|
16 |
+
|
17 |
+
return (
|
18 |
+
<div className="flex flex-wrap justify-center gap-2 mb-4">
|
19 |
+
{languages.map(({ code, name, flag }) => (
|
20 |
+
<Button
|
21 |
+
key={code}
|
22 |
+
variant={language === code ? "default" : "outline"}
|
23 |
+
onClick={() => setLanguage(code)}
|
24 |
+
className="flex items-center gap-2"
|
25 |
+
>
|
26 |
+
<span>{flag}</span>
|
27 |
+
<span>{name}</span>
|
28 |
+
</Button>
|
29 |
+
))}
|
30 |
+
</div>
|
31 |
+
);
|
32 |
+
};
|
src/components/game/SentenceBuilder.tsx
CHANGED
@@ -3,6 +3,7 @@ import { Input } from "@/components/ui/input";
|
|
3 |
import { motion } from "framer-motion";
|
4 |
import { KeyboardEvent, useRef, useEffect, useState } from "react";
|
5 |
import { useToast } from "@/hooks/use-toast";
|
|
|
6 |
|
7 |
interface SentenceBuilderProps {
|
8 |
currentWord: string;
|
@@ -29,6 +30,7 @@ export const SentenceBuilder = ({
|
|
29 |
const [imageLoaded, setImageLoaded] = useState(false);
|
30 |
const imagePath = `/think_in_sync_assets/${currentWord.toLowerCase()}.jpg`;
|
31 |
const { toast } = useToast();
|
|
|
32 |
|
33 |
useEffect(() => {
|
34 |
const img = new Image();
|
@@ -37,14 +39,12 @@ export const SentenceBuilder = ({
|
|
37 |
console.log("Attempting to load image:", imagePath);
|
38 |
}, [imagePath]);
|
39 |
|
40 |
-
// Focus input on initial render
|
41 |
useEffect(() => {
|
42 |
setTimeout(() => {
|
43 |
inputRef.current?.focus();
|
44 |
}, 100);
|
45 |
}, []);
|
46 |
|
47 |
-
// Focus input after AI finishes thinking
|
48 |
useEffect(() => {
|
49 |
if (!isAiThinking && sentence.length > 0 && sentence.length % 2 === 0) {
|
50 |
setTimeout(() => {
|
@@ -59,7 +59,6 @@ export const SentenceBuilder = ({
|
|
59 |
if (playerInput.trim()) {
|
60 |
handleSubmit(e as any);
|
61 |
}
|
62 |
-
// Make the guess immediately without waiting for AI response
|
63 |
onMakeGuess();
|
64 |
}
|
65 |
};
|
@@ -69,11 +68,10 @@ export const SentenceBuilder = ({
|
|
69 |
const input = playerInput.trim().toLowerCase();
|
70 |
const target = currentWord.toLowerCase();
|
71 |
|
72 |
-
// Check if the input contains only letters
|
73 |
if (!/^[a-zA-Z]+$/.test(input)) {
|
74 |
toast({
|
75 |
-
title:
|
76 |
-
description:
|
77 |
variant: "destructive",
|
78 |
});
|
79 |
return;
|
@@ -81,8 +79,8 @@ export const SentenceBuilder = ({
|
|
81 |
|
82 |
if (input.includes(target)) {
|
83 |
toast({
|
84 |
-
title:
|
85 |
-
description:
|
86 |
variant: "destructive",
|
87 |
});
|
88 |
return;
|
@@ -98,10 +96,10 @@ export const SentenceBuilder = ({
|
|
98 |
className="text-center"
|
99 |
>
|
100 |
<h2 className="mb-4 text-2xl font-semibold text-gray-900">
|
101 |
-
|
102 |
</h2>
|
103 |
<p className="mb-6 text-sm text-gray-600">
|
104 |
-
|
105 |
</p>
|
106 |
<div className="mb-4 overflow-hidden rounded-lg bg-secondary/10">
|
107 |
{imageLoaded && (
|
@@ -117,7 +115,7 @@ export const SentenceBuilder = ({
|
|
117 |
</div>
|
118 |
<div className="mb-6 rounded-lg bg-gray-50 p-4">
|
119 |
<p className="text-lg text-gray-800">
|
120 |
-
{sentence.length > 0 ? sentence.join(" ") :
|
121 |
</p>
|
122 |
</div>
|
123 |
<form onSubmit={handleSubmit} className="mb-4">
|
@@ -126,12 +124,11 @@ export const SentenceBuilder = ({
|
|
126 |
type="text"
|
127 |
value={playerInput}
|
128 |
onChange={(e) => {
|
129 |
-
// Only allow letters in the input
|
130 |
const value = e.target.value.replace(/[^a-zA-Z]/g, '');
|
131 |
onInputChange(value);
|
132 |
}}
|
133 |
onKeyDown={handleKeyDown}
|
134 |
-
placeholder=
|
135 |
className="mb-4"
|
136 |
disabled={isAiThinking}
|
137 |
/>
|
@@ -141,7 +138,7 @@ export const SentenceBuilder = ({
|
|
141 |
className="flex-1 bg-primary text-lg hover:bg-primary/90"
|
142 |
disabled={!playerInput.trim() || isAiThinking}
|
143 |
>
|
144 |
-
{isAiThinking ?
|
145 |
</Button>
|
146 |
<Button
|
147 |
type="button"
|
@@ -149,7 +146,7 @@ export const SentenceBuilder = ({
|
|
149 |
className="flex-1 bg-secondary text-lg hover:bg-secondary/90"
|
150 |
disabled={(!sentence.length && !playerInput.trim()) || isAiThinking}
|
151 |
>
|
152 |
-
{isAiThinking ?
|
153 |
</Button>
|
154 |
</div>
|
155 |
</form>
|
|
|
3 |
import { motion } from "framer-motion";
|
4 |
import { KeyboardEvent, useRef, useEffect, useState } from "react";
|
5 |
import { useToast } from "@/hooks/use-toast";
|
6 |
+
import { useTranslation } from "@/hooks/useTranslation";
|
7 |
|
8 |
interface SentenceBuilderProps {
|
9 |
currentWord: string;
|
|
|
30 |
const [imageLoaded, setImageLoaded] = useState(false);
|
31 |
const imagePath = `/think_in_sync_assets/${currentWord.toLowerCase()}.jpg`;
|
32 |
const { toast } = useToast();
|
33 |
+
const t = useTranslation();
|
34 |
|
35 |
useEffect(() => {
|
36 |
const img = new Image();
|
|
|
39 |
console.log("Attempting to load image:", imagePath);
|
40 |
}, [imagePath]);
|
41 |
|
|
|
42 |
useEffect(() => {
|
43 |
setTimeout(() => {
|
44 |
inputRef.current?.focus();
|
45 |
}, 100);
|
46 |
}, []);
|
47 |
|
|
|
48 |
useEffect(() => {
|
49 |
if (!isAiThinking && sentence.length > 0 && sentence.length % 2 === 0) {
|
50 |
setTimeout(() => {
|
|
|
59 |
if (playerInput.trim()) {
|
60 |
handleSubmit(e as any);
|
61 |
}
|
|
|
62 |
onMakeGuess();
|
63 |
}
|
64 |
};
|
|
|
68 |
const input = playerInput.trim().toLowerCase();
|
69 |
const target = currentWord.toLowerCase();
|
70 |
|
|
|
71 |
if (!/^[a-zA-Z]+$/.test(input)) {
|
72 |
toast({
|
73 |
+
title: t.game.invalidWord,
|
74 |
+
description: t.game.lettersOnly,
|
75 |
variant: "destructive",
|
76 |
});
|
77 |
return;
|
|
|
79 |
|
80 |
if (input.includes(target)) {
|
81 |
toast({
|
82 |
+
title: t.game.invalidWord,
|
83 |
+
description: `${t.game.cantUseTargetWord} "${currentWord}"`,
|
84 |
variant: "destructive",
|
85 |
});
|
86 |
return;
|
|
|
96 |
className="text-center"
|
97 |
>
|
98 |
<h2 className="mb-4 text-2xl font-semibold text-gray-900">
|
99 |
+
{t.game.buildDescription}
|
100 |
</h2>
|
101 |
<p className="mb-6 text-sm text-gray-600">
|
102 |
+
{t.game.buildSubtitle}
|
103 |
</p>
|
104 |
<div className="mb-4 overflow-hidden rounded-lg bg-secondary/10">
|
105 |
{imageLoaded && (
|
|
|
115 |
</div>
|
116 |
<div className="mb-6 rounded-lg bg-gray-50 p-4">
|
117 |
<p className="text-lg text-gray-800">
|
118 |
+
{sentence.length > 0 ? sentence.join(" ") : t.game.startSentence}
|
119 |
</p>
|
120 |
</div>
|
121 |
<form onSubmit={handleSubmit} className="mb-4">
|
|
|
124 |
type="text"
|
125 |
value={playerInput}
|
126 |
onChange={(e) => {
|
|
|
127 |
const value = e.target.value.replace(/[^a-zA-Z]/g, '');
|
128 |
onInputChange(value);
|
129 |
}}
|
130 |
onKeyDown={handleKeyDown}
|
131 |
+
placeholder={t.game.inputPlaceholder}
|
132 |
className="mb-4"
|
133 |
disabled={isAiThinking}
|
134 |
/>
|
|
|
138 |
className="flex-1 bg-primary text-lg hover:bg-primary/90"
|
139 |
disabled={!playerInput.trim() || isAiThinking}
|
140 |
>
|
141 |
+
{isAiThinking ? t.game.aiThinking : t.game.addWord}
|
142 |
</Button>
|
143 |
<Button
|
144 |
type="button"
|
|
|
146 |
className="flex-1 bg-secondary text-lg hover:bg-secondary/90"
|
147 |
disabled={(!sentence.length && !playerInput.trim()) || isAiThinking}
|
148 |
>
|
149 |
+
{isAiThinking ? t.game.aiThinking : t.game.makeGuess}
|
150 |
</Button>
|
151 |
</div>
|
152 |
</form>
|
src/components/game/ThemeSelector.tsx
CHANGED
@@ -2,8 +2,11 @@ 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;
|
@@ -14,14 +17,26 @@ export const ThemeSelector = ({ onThemeSelect }: ThemeSelectorProps) => {
|
|
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;
|
21 |
|
22 |
switch(e.key.toLowerCase()) {
|
23 |
case 'a':
|
24 |
-
|
|
|
|
|
|
|
|
|
25 |
break;
|
26 |
case 'b':
|
27 |
setSelectedTheme("sports");
|
@@ -30,7 +45,7 @@ export const ThemeSelector = ({ onThemeSelect }: ThemeSelectorProps) => {
|
|
30 |
setSelectedTheme("food");
|
31 |
break;
|
32 |
case 'd':
|
33 |
-
e.preventDefault();
|
34 |
setSelectedTheme("custom");
|
35 |
break;
|
36 |
case 'enter':
|
@@ -43,7 +58,7 @@ export const ThemeSelector = ({ onThemeSelect }: ThemeSelectorProps) => {
|
|
43 |
|
44 |
window.addEventListener('keydown', handleKeyPress);
|
45 |
return () => window.removeEventListener('keydown', handleKeyPress);
|
46 |
-
}, [selectedTheme, customTheme]);
|
47 |
|
48 |
useEffect(() => {
|
49 |
if (selectedTheme === "custom") {
|
@@ -77,25 +92,35 @@ export const ThemeSelector = ({ onThemeSelect }: ThemeSelectorProps) => {
|
|
77 |
className="space-y-6"
|
78 |
>
|
79 |
<div className="text-center space-y-2">
|
80 |
-
<h2 className="text-2xl font-bold text-gray-900">
|
81 |
-
<p className="text-gray-600">
|
82 |
</div>
|
83 |
|
84 |
<div className="space-y-4">
|
85 |
-
|
86 |
-
|
87 |
-
|
88 |
-
|
89 |
-
|
90 |
-
|
91 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
92 |
|
93 |
<Button
|
94 |
variant={selectedTheme === "sports" ? "default" : "outline"}
|
95 |
className="w-full justify-between"
|
96 |
onClick={() => setSelectedTheme("sports")}
|
97 |
>
|
98 |
-
|
99 |
</Button>
|
100 |
|
101 |
<Button
|
@@ -103,7 +128,7 @@ export const ThemeSelector = ({ onThemeSelect }: ThemeSelectorProps) => {
|
|
103 |
className="w-full justify-between"
|
104 |
onClick={() => setSelectedTheme("food")}
|
105 |
>
|
106 |
-
|
107 |
</Button>
|
108 |
|
109 |
<Button
|
@@ -111,7 +136,7 @@ export const ThemeSelector = ({ onThemeSelect }: ThemeSelectorProps) => {
|
|
111 |
className="w-full justify-between"
|
112 |
onClick={() => setSelectedTheme("custom")}
|
113 |
>
|
114 |
-
|
115 |
</Button>
|
116 |
|
117 |
{selectedTheme === "custom" && (
|
@@ -124,7 +149,7 @@ export const ThemeSelector = ({ onThemeSelect }: ThemeSelectorProps) => {
|
|
124 |
<Input
|
125 |
ref={inputRef}
|
126 |
type="text"
|
127 |
-
placeholder=
|
128 |
value={customTheme}
|
129 |
onChange={(e) => setCustomTheme(e.target.value)}
|
130 |
onKeyPress={handleInputKeyPress}
|
@@ -139,7 +164,7 @@ export const ThemeSelector = ({ onThemeSelect }: ThemeSelectorProps) => {
|
|
139 |
className="w-full"
|
140 |
disabled={selectedTheme === "custom" && !customTheme.trim() || isGenerating}
|
141 |
>
|
142 |
-
{isGenerating ?
|
143 |
</Button>
|
144 |
</motion.div>
|
145 |
);
|
|
|
2 |
import { Button } from "@/components/ui/button";
|
3 |
import { Input } from "@/components/ui/input";
|
4 |
import { motion } from "framer-motion";
|
5 |
+
import { useTranslation } from "@/hooks/useTranslation";
|
6 |
+
import { useContext } from "react";
|
7 |
+
import { LanguageContext } from "@/contexts/LanguageContext";
|
8 |
|
9 |
+
type Theme = "standard" | "technology" | "sports" | "food" | "custom";
|
10 |
|
11 |
interface ThemeSelectorProps {
|
12 |
onThemeSelect: (theme: string) => void;
|
|
|
17 |
const [customTheme, setCustomTheme] = useState("");
|
18 |
const [isGenerating, setIsGenerating] = useState(false);
|
19 |
const inputRef = useRef<HTMLInputElement>(null);
|
20 |
+
const t = useTranslation();
|
21 |
+
const { language } = useContext(LanguageContext);
|
22 |
+
|
23 |
+
useEffect(() => {
|
24 |
+
if (language !== 'en') {
|
25 |
+
setSelectedTheme("technology");
|
26 |
+
}
|
27 |
+
}, [language]);
|
28 |
|
29 |
useEffect(() => {
|
30 |
const handleKeyPress = (e: KeyboardEvent) => {
|
31 |
+
if (e.target instanceof HTMLInputElement) return;
|
32 |
|
33 |
switch(e.key.toLowerCase()) {
|
34 |
case 'a':
|
35 |
+
if (language === 'en') {
|
36 |
+
setSelectedTheme("standard");
|
37 |
+
} else {
|
38 |
+
setSelectedTheme("technology");
|
39 |
+
}
|
40 |
break;
|
41 |
case 'b':
|
42 |
setSelectedTheme("sports");
|
|
|
45 |
setSelectedTheme("food");
|
46 |
break;
|
47 |
case 'd':
|
48 |
+
e.preventDefault();
|
49 |
setSelectedTheme("custom");
|
50 |
break;
|
51 |
case 'enter':
|
|
|
58 |
|
59 |
window.addEventListener('keydown', handleKeyPress);
|
60 |
return () => window.removeEventListener('keydown', handleKeyPress);
|
61 |
+
}, [selectedTheme, customTheme, language]);
|
62 |
|
63 |
useEffect(() => {
|
64 |
if (selectedTheme === "custom") {
|
|
|
92 |
className="space-y-6"
|
93 |
>
|
94 |
<div className="text-center space-y-2">
|
95 |
+
<h2 className="text-2xl font-bold text-gray-900">{t.themes.title}</h2>
|
96 |
+
<p className="text-gray-600">{t.themes.subtitle}</p>
|
97 |
</div>
|
98 |
|
99 |
<div className="space-y-4">
|
100 |
+
{language === 'en' ? (
|
101 |
+
<Button
|
102 |
+
variant={selectedTheme === "standard" ? "default" : "outline"}
|
103 |
+
className="w-full justify-between"
|
104 |
+
onClick={() => setSelectedTheme("standard")}
|
105 |
+
>
|
106 |
+
{t.themes.standard} <span className="text-sm opacity-50">{t.themes.pressKey} A</span>
|
107 |
+
</Button>
|
108 |
+
) : (
|
109 |
+
<Button
|
110 |
+
variant={selectedTheme === "technology" ? "default" : "outline"}
|
111 |
+
className="w-full justify-between"
|
112 |
+
onClick={() => setSelectedTheme("technology")}
|
113 |
+
>
|
114 |
+
{t.themes.technology} <span className="text-sm opacity-50">{t.themes.pressKey} A</span>
|
115 |
+
</Button>
|
116 |
+
)}
|
117 |
|
118 |
<Button
|
119 |
variant={selectedTheme === "sports" ? "default" : "outline"}
|
120 |
className="w-full justify-between"
|
121 |
onClick={() => setSelectedTheme("sports")}
|
122 |
>
|
123 |
+
{t.themes.sports} <span className="text-sm opacity-50">{t.themes.pressKey} B</span>
|
124 |
</Button>
|
125 |
|
126 |
<Button
|
|
|
128 |
className="w-full justify-between"
|
129 |
onClick={() => setSelectedTheme("food")}
|
130 |
>
|
131 |
+
{t.themes.food} <span className="text-sm opacity-50">{t.themes.pressKey} C</span>
|
132 |
</Button>
|
133 |
|
134 |
<Button
|
|
|
136 |
className="w-full justify-between"
|
137 |
onClick={() => setSelectedTheme("custom")}
|
138 |
>
|
139 |
+
{t.themes.custom} <span className="text-sm opacity-50">{t.themes.pressKey} D</span>
|
140 |
</Button>
|
141 |
|
142 |
{selectedTheme === "custom" && (
|
|
|
149 |
<Input
|
150 |
ref={inputRef}
|
151 |
type="text"
|
152 |
+
placeholder={t.themes.customPlaceholder}
|
153 |
value={customTheme}
|
154 |
onChange={(e) => setCustomTheme(e.target.value)}
|
155 |
onKeyPress={handleInputKeyPress}
|
|
|
164 |
className="w-full"
|
165 |
disabled={selectedTheme === "custom" && !customTheme.trim() || isGenerating}
|
166 |
>
|
167 |
+
{isGenerating ? t.themes.generating : `${t.themes.continue} ⏎`}
|
168 |
</Button>
|
169 |
</motion.div>
|
170 |
);
|
src/components/game/WelcomeScreen.tsx
CHANGED
@@ -3,7 +3,10 @@ import { motion } from "framer-motion";
|
|
3 |
import { useState } from "react";
|
4 |
import { HighScoreBoard } from "../HighScoreBoard";
|
5 |
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
6 |
-
import {
|
|
|
|
|
|
|
7 |
|
8 |
interface WelcomeScreenProps {
|
9 |
onStart: () => void;
|
@@ -12,30 +15,8 @@ interface WelcomeScreenProps {
|
|
12 |
export const WelcomeScreen = ({ onStart }: WelcomeScreenProps) => {
|
13 |
const [showHighScores, setShowHighScores] = useState(false);
|
14 |
const [showHowToPlay, setShowHowToPlay] = useState(false);
|
15 |
-
|
16 |
-
const
|
17 |
-
<div className="space-y-6">
|
18 |
-
<div className="grid gap-4 text-gray-600">
|
19 |
-
<div>
|
20 |
-
<h3 className="font-medium text-gray-800">The Setup</h3>
|
21 |
-
<p>You'll work with two AIs: one as your partner giving clues, and another trying to guess the word.</p>
|
22 |
-
</div>
|
23 |
-
<div>
|
24 |
-
<h3 className="font-medium text-gray-800">Your Goal</h3>
|
25 |
-
<p>Help the AI guess the secret word using one-word clues. Each correct guess earns you a point!</p>
|
26 |
-
</div>
|
27 |
-
<div>
|
28 |
-
<h3 className="font-medium text-gray-800">The Rules</h3>
|
29 |
-
<ul className="list-disc list-inside space-y-1">
|
30 |
-
<li>One word per clue only</li>
|
31 |
-
<li>No parts of the secret word or translations</li>
|
32 |
-
<li>Clues must relate to the word (be creative!)</li>
|
33 |
-
<li>No spelling out the answer</li>
|
34 |
-
</ul>
|
35 |
-
</div>
|
36 |
-
</div>
|
37 |
-
</div>
|
38 |
-
);
|
39 |
|
40 |
return (
|
41 |
<>
|
@@ -44,10 +25,12 @@ export const WelcomeScreen = ({ onStart }: WelcomeScreenProps) => {
|
|
44 |
animate={{ opacity: 1 }}
|
45 |
className="max-w-2xl mx-auto text-center space-y-8"
|
46 |
>
|
|
|
|
|
47 |
<div>
|
48 |
-
<h1 className="mb-4 text-4xl font-bold text-gray-900">
|
49 |
<p className="text-lg text-gray-600">
|
50 |
-
|
51 |
</p>
|
52 |
</div>
|
53 |
|
@@ -56,7 +39,7 @@ export const WelcomeScreen = ({ onStart }: WelcomeScreenProps) => {
|
|
56 |
onClick={onStart}
|
57 |
className="w-full bg-primary text-lg hover:bg-primary/90"
|
58 |
>
|
59 |
-
|
60 |
</Button>
|
61 |
<div className="grid grid-cols-2 gap-4">
|
62 |
<Button
|
@@ -64,14 +47,14 @@ export const WelcomeScreen = ({ onStart }: WelcomeScreenProps) => {
|
|
64 |
variant="outline"
|
65 |
className="text-lg"
|
66 |
>
|
67 |
-
|
68 |
</Button>
|
69 |
<Button
|
70 |
onClick={() => setShowHighScores(true)}
|
71 |
variant="outline"
|
72 |
className="text-lg"
|
73 |
>
|
74 |
-
|
75 |
</Button>
|
76 |
</div>
|
77 |
</div>
|
@@ -91,11 +74,30 @@ export const WelcomeScreen = ({ onStart }: WelcomeScreenProps) => {
|
|
91 |
<Dialog open={showHowToPlay} onOpenChange={setShowHowToPlay}>
|
92 |
<DialogContent className="sm:max-w-[600px]">
|
93 |
<DialogHeader>
|
94 |
-
<DialogTitle className="text-xl font-semibold text-primary">
|
95 |
</DialogHeader>
|
96 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
97 |
</DialogContent>
|
98 |
</Dialog>
|
99 |
</>
|
100 |
);
|
101 |
-
};
|
|
|
3 |
import { useState } from "react";
|
4 |
import { HighScoreBoard } from "../HighScoreBoard";
|
5 |
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
6 |
+
import { LanguageSelector } from "./LanguageSelector";
|
7 |
+
import { useTranslation } from "@/hooks/useTranslation";
|
8 |
+
import { useContext } from "react";
|
9 |
+
import { LanguageContext } from "@/contexts/LanguageContext";
|
10 |
|
11 |
interface WelcomeScreenProps {
|
12 |
onStart: () => void;
|
|
|
15 |
export const WelcomeScreen = ({ onStart }: WelcomeScreenProps) => {
|
16 |
const [showHighScores, setShowHighScores] = useState(false);
|
17 |
const [showHowToPlay, setShowHowToPlay] = useState(false);
|
18 |
+
const t = useTranslation();
|
19 |
+
const { language } = useContext(LanguageContext);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
20 |
|
21 |
return (
|
22 |
<>
|
|
|
25 |
animate={{ opacity: 1 }}
|
26 |
className="max-w-2xl mx-auto text-center space-y-8"
|
27 |
>
|
28 |
+
<LanguageSelector />
|
29 |
+
|
30 |
<div>
|
31 |
+
<h1 className="mb-4 text-4xl font-bold text-gray-900">{t.welcome.title}</h1>
|
32 |
<p className="text-lg text-gray-600">
|
33 |
+
{t.welcome.subtitle}
|
34 |
</p>
|
35 |
</div>
|
36 |
|
|
|
39 |
onClick={onStart}
|
40 |
className="w-full bg-primary text-lg hover:bg-primary/90"
|
41 |
>
|
42 |
+
{t.welcome.startButton} ⏎
|
43 |
</Button>
|
44 |
<div className="grid grid-cols-2 gap-4">
|
45 |
<Button
|
|
|
47 |
variant="outline"
|
48 |
className="text-lg"
|
49 |
>
|
50 |
+
{t.welcome.howToPlay} 📖
|
51 |
</Button>
|
52 |
<Button
|
53 |
onClick={() => setShowHighScores(true)}
|
54 |
variant="outline"
|
55 |
className="text-lg"
|
56 |
>
|
57 |
+
{t.welcome.leaderboard} 🏆
|
58 |
</Button>
|
59 |
</div>
|
60 |
</div>
|
|
|
74 |
<Dialog open={showHowToPlay} onOpenChange={setShowHowToPlay}>
|
75 |
<DialogContent className="sm:max-w-[600px]">
|
76 |
<DialogHeader>
|
77 |
+
<DialogTitle className="text-xl font-semibold text-primary">{t.welcome.howToPlay}</DialogTitle>
|
78 |
</DialogHeader>
|
79 |
+
<div className="space-y-6">
|
80 |
+
<div className="grid gap-4 text-gray-600">
|
81 |
+
<div>
|
82 |
+
<h3 className="font-medium text-gray-800">{t.howToPlay.setup.title}</h3>
|
83 |
+
<p>{t.howToPlay.setup.description}</p>
|
84 |
+
</div>
|
85 |
+
<div>
|
86 |
+
<h3 className="font-medium text-gray-800">{t.howToPlay.goal.title}</h3>
|
87 |
+
<p>{t.howToPlay.goal.description}</p>
|
88 |
+
</div>
|
89 |
+
<div>
|
90 |
+
<h3 className="font-medium text-gray-800">{t.howToPlay.rules.title}</h3>
|
91 |
+
<ul className="list-disc list-inside space-y-1">
|
92 |
+
{t.howToPlay.rules.items.map((rule, index) => (
|
93 |
+
<li key={index}>{rule}</li>
|
94 |
+
))}
|
95 |
+
</ul>
|
96 |
+
</div>
|
97 |
+
</div>
|
98 |
+
</div>
|
99 |
</DialogContent>
|
100 |
</Dialog>
|
101 |
</>
|
102 |
);
|
103 |
+
};
|
src/contexts/LanguageContext.tsx
ADDED
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { createContext, useState, useEffect, ReactNode } from 'react';
|
2 |
+
import { Language } from '@/i18n/translations';
|
3 |
+
|
4 |
+
interface LanguageContextType {
|
5 |
+
language: Language;
|
6 |
+
setLanguage: (lang: Language) => void;
|
7 |
+
}
|
8 |
+
|
9 |
+
export const LanguageContext = createContext<LanguageContextType>({
|
10 |
+
language: 'en',
|
11 |
+
setLanguage: () => {},
|
12 |
+
});
|
13 |
+
|
14 |
+
interface LanguageProviderProps {
|
15 |
+
children: ReactNode;
|
16 |
+
}
|
17 |
+
|
18 |
+
export const LanguageProvider = ({ children }: LanguageProviderProps) => {
|
19 |
+
const [language, setLanguage] = useState<Language>('en');
|
20 |
+
|
21 |
+
useEffect(() => {
|
22 |
+
const savedLang = localStorage.getItem('language') as Language;
|
23 |
+
if (savedLang && ['en', 'fr', 'de', 'it', 'es'].includes(savedLang)) {
|
24 |
+
setLanguage(savedLang);
|
25 |
+
}
|
26 |
+
}, []);
|
27 |
+
|
28 |
+
const handleSetLanguage = (lang: Language) => {
|
29 |
+
setLanguage(lang);
|
30 |
+
localStorage.setItem('language', lang);
|
31 |
+
};
|
32 |
+
|
33 |
+
return (
|
34 |
+
<LanguageContext.Provider value={{ language, setLanguage: handleSetLanguage }}>
|
35 |
+
{children}
|
36 |
+
</LanguageContext.Provider>
|
37 |
+
);
|
38 |
+
};
|
src/hooks/useTranslation.ts
ADDED
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { useContext } from 'react';
|
2 |
+
import { LanguageContext } from '@/contexts/LanguageContext';
|
3 |
+
import { translations } from '@/i18n/translations';
|
4 |
+
|
5 |
+
export const useTranslation = () => {
|
6 |
+
const { language } = useContext(LanguageContext);
|
7 |
+
return translations[language];
|
8 |
+
};
|
src/i18n/translations/de.ts
ADDED
@@ -0,0 +1,94 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
export const de = {
|
2 |
+
welcome: {
|
3 |
+
title: "Denken im Einklang",
|
4 |
+
subtitle: "In diesem Spiel arbeiten Sie mit KI zusammen, um geheime Wörter zu erraten!",
|
5 |
+
startButton: "Spiel Starten",
|
6 |
+
howToPlay: "Spielanleitung",
|
7 |
+
leaderboard: "Bestenliste"
|
8 |
+
},
|
9 |
+
howToPlay: {
|
10 |
+
setup: {
|
11 |
+
title: "Der Aufbau",
|
12 |
+
description: "Sie arbeiten mit zwei KIs: eine als Partner, der Hinweise gibt, und eine andere, die versucht, das Wort zu erraten."
|
13 |
+
},
|
14 |
+
goal: {
|
15 |
+
title: "Ihr Ziel",
|
16 |
+
description: "Helfen Sie der KI, das geheime Wort mit Einwort-Hinweisen zu erraten. Jede richtige Vermutung bringt Ihnen einen Punkt!"
|
17 |
+
},
|
18 |
+
rules: {
|
19 |
+
title: "Die Regeln",
|
20 |
+
items: [
|
21 |
+
"Nur ein Wort pro Hinweis",
|
22 |
+
"Keine Teile des geheimen Wortes oder Übersetzungen",
|
23 |
+
"Hinweise müssen sich auf das Wort beziehen (seien Sie kreativ!)",
|
24 |
+
"Keine Buchstabierung der Antwort"
|
25 |
+
]
|
26 |
+
}
|
27 |
+
},
|
28 |
+
game: {
|
29 |
+
buildDescription: "Beschreibung Erstellen",
|
30 |
+
buildSubtitle: "Wechseln Sie sich mit der KI ab, um Ihr Wort zu beschreiben, ohne das Wort selbst zu verwenden!",
|
31 |
+
startSentence: "Beginnen Sie Ihren Satz...",
|
32 |
+
inputPlaceholder: "Geben Sie Ihr Wort ein (nur Buchstaben)...",
|
33 |
+
addWord: "Wort Hinzufügen",
|
34 |
+
makeGuess: "KI Raten Lassen",
|
35 |
+
aiThinking: "KI denkt nach...",
|
36 |
+
aiDelayed: "Die KI ist derzeit beschäftigt. Bitte versuchen Sie es in einem Moment erneut.",
|
37 |
+
invalidWord: "Ungültiges Wort",
|
38 |
+
cantUseTargetWord: "Sie können keine Wörter verwenden, die enthalten",
|
39 |
+
lettersOnly: "Bitte verwenden Sie nur Buchstaben (keine Zahlen oder Sonderzeichen)"
|
40 |
+
},
|
41 |
+
guess: {
|
42 |
+
title: "KI-Vermutung",
|
43 |
+
sentence: "Ihr Satz",
|
44 |
+
aiGuessed: "KI hat geraten",
|
45 |
+
correct: "Richtig geraten! 🎉 Bereit für die nächste Runde? Drücken Sie Enter",
|
46 |
+
incorrect: "Spiel vorbei! Drücken Sie Enter zum Neustart",
|
47 |
+
nextRound: "Nächste Runde",
|
48 |
+
playAgain: "Erneut Spielen",
|
49 |
+
viewLeaderboard: "Bestenliste Anzeigen"
|
50 |
+
},
|
51 |
+
gameOver: {
|
52 |
+
title: "Spiel Vorbei!",
|
53 |
+
completedRounds: "Sie haben {count} Runden erfolgreich abgeschlossen!",
|
54 |
+
playAgain: "Erneut Spielen"
|
55 |
+
},
|
56 |
+
themes: {
|
57 |
+
title: "Thema Wählen",
|
58 |
+
subtitle: "Wählen Sie ein Thema für Ihr Wörter-Rate-Abenteuer",
|
59 |
+
standard: "",
|
60 |
+
technology: "Technologie",
|
61 |
+
sports: "Sport",
|
62 |
+
food: "Essen",
|
63 |
+
custom: "Wählen Sie Ihr Thema",
|
64 |
+
customPlaceholder: "Geben Sie ein Thema ein (z.B. Tiere, Filme)",
|
65 |
+
continue: "Weiter",
|
66 |
+
generating: "Generiere thematische Wörter...",
|
67 |
+
pressKey: "Drücken Sie"
|
68 |
+
},
|
69 |
+
leaderboard: {
|
70 |
+
title: "Bestenliste",
|
71 |
+
yourScore: "Ihre Punktzahl",
|
72 |
+
roundCount: "Runden",
|
73 |
+
wordsPerRound: "Wörter/Runde",
|
74 |
+
enterName: "Geben Sie Ihren Namen ein (nur Buchstaben und Zahlen)",
|
75 |
+
submit: "Punktzahl Einreichen",
|
76 |
+
submitting: "Wird eingereicht...",
|
77 |
+
rank: "Rang",
|
78 |
+
player: "Spieler",
|
79 |
+
roundsColumn: "Runden",
|
80 |
+
avgWords: "Durchschn. Wörter/Runde",
|
81 |
+
noScores: "Noch keine Highscores. Seien Sie der Erste!",
|
82 |
+
previous: "Zurück",
|
83 |
+
next: "Weiter",
|
84 |
+
error: {
|
85 |
+
invalidName: "Bitte geben Sie einen gültigen Namen ein (nur Buchstaben und Zahlen)",
|
86 |
+
noRounds: "Sie müssen mindestens eine Runde abschließen, um eine Punktzahl einzureichen",
|
87 |
+
alreadySubmitted: "Sie haben Ihre Punktzahl für dieses Spiel bereits eingereicht",
|
88 |
+
newHighScore: "Neuer Highscore!",
|
89 |
+
beatRecord: "Sie haben Ihren bisherigen Rekord von {score} Runden übertroffen!",
|
90 |
+
notHigher: "Ihre aktuelle Punktzahl ({current}) ist nicht höher als Ihre beste Punktzahl ({best})",
|
91 |
+
submitError: "Fehler beim Einreichen der Punktzahl. Bitte versuchen Sie es erneut."
|
92 |
+
}
|
93 |
+
}
|
94 |
+
};
|
src/i18n/translations/en.ts
ADDED
@@ -0,0 +1,94 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
export const en = {
|
2 |
+
welcome: {
|
3 |
+
title: "Think in Sync",
|
4 |
+
subtitle: "In this game you team up with AI to guess secret words!",
|
5 |
+
startButton: "Start Game",
|
6 |
+
howToPlay: "How to Play",
|
7 |
+
leaderboard: "Leaderboard"
|
8 |
+
},
|
9 |
+
howToPlay: {
|
10 |
+
setup: {
|
11 |
+
title: "The Setup",
|
12 |
+
description: "You'll work with two AIs: one as your partner giving clues, and another trying to guess the word."
|
13 |
+
},
|
14 |
+
goal: {
|
15 |
+
title: "Your Goal",
|
16 |
+
description: "Help the AI guess the secret word using one-word clues. Each correct guess earns you a point!"
|
17 |
+
},
|
18 |
+
rules: {
|
19 |
+
title: "The Rules",
|
20 |
+
items: [
|
21 |
+
"One word per clue only",
|
22 |
+
"No parts of the secret word or translations",
|
23 |
+
"Clues must relate to the word (be creative!)",
|
24 |
+
"No spelling out the answer"
|
25 |
+
]
|
26 |
+
}
|
27 |
+
},
|
28 |
+
game: {
|
29 |
+
buildDescription: "Build a Description",
|
30 |
+
buildSubtitle: "Take turns with AI to describe your word without using the word itself!",
|
31 |
+
startSentence: "Start your sentence...",
|
32 |
+
inputPlaceholder: "Enter your word (letters only)...",
|
33 |
+
addWord: "Add Word",
|
34 |
+
makeGuess: "Make AI Guess",
|
35 |
+
aiThinking: "AI is thinking...",
|
36 |
+
aiDelayed: "The AI is currently busy. Please try again in a moment.",
|
37 |
+
invalidWord: "Invalid Word",
|
38 |
+
cantUseTargetWord: "You cannot use words that contain",
|
39 |
+
lettersOnly: "Please use only letters (no numbers or special characters)"
|
40 |
+
},
|
41 |
+
guess: {
|
42 |
+
title: "AI's Guess",
|
43 |
+
sentence: "Your sentence",
|
44 |
+
aiGuessed: "AI guessed",
|
45 |
+
correct: "Correct guess! 🎉 Ready for the next round? Press Enter",
|
46 |
+
incorrect: "Game Over! Press Enter to play again",
|
47 |
+
nextRound: "Next Round",
|
48 |
+
playAgain: "Play Again",
|
49 |
+
viewLeaderboard: "View Leaderboard"
|
50 |
+
},
|
51 |
+
gameOver: {
|
52 |
+
title: "Game Over!",
|
53 |
+
completedRounds: "You completed {count} rounds successfully!",
|
54 |
+
playAgain: "Play Again"
|
55 |
+
},
|
56 |
+
themes: {
|
57 |
+
title: "Choose a Theme",
|
58 |
+
subtitle: "Select a theme for your word-guessing adventure",
|
59 |
+
standard: "Standard",
|
60 |
+
technology: "Technology",
|
61 |
+
sports: "Sports",
|
62 |
+
food: "Food",
|
63 |
+
custom: "Choose your theme",
|
64 |
+
customPlaceholder: "Enter a theme (e.g., Animals, Movies)",
|
65 |
+
continue: "Continue",
|
66 |
+
generating: "Generating themed words...",
|
67 |
+
pressKey: "Press"
|
68 |
+
},
|
69 |
+
leaderboard: {
|
70 |
+
title: "Leaderboard",
|
71 |
+
yourScore: "Your score",
|
72 |
+
roundCount: "rounds",
|
73 |
+
wordsPerRound: "words/round",
|
74 |
+
enterName: "Enter your name (letters and numbers only)",
|
75 |
+
submit: "Submit Score",
|
76 |
+
submitting: "Submitting...",
|
77 |
+
rank: "Rank",
|
78 |
+
player: "Player",
|
79 |
+
roundsColumn: "Rounds",
|
80 |
+
avgWords: "Avg Words/Round",
|
81 |
+
noScores: "No high scores yet. Be the first!",
|
82 |
+
previous: "Previous",
|
83 |
+
next: "Next",
|
84 |
+
error: {
|
85 |
+
invalidName: "Please enter a valid name (only letters and numbers allowed)",
|
86 |
+
noRounds: "You need to complete at least one round to submit a score",
|
87 |
+
alreadySubmitted: "You have already submitted your score for this game",
|
88 |
+
newHighScore: "New High Score!",
|
89 |
+
beatRecord: "You beat your previous record of {score} rounds!",
|
90 |
+
notHigher: "Your current score ({current}) is not higher than your best score ({best})",
|
91 |
+
submitError: "Failed to submit score. Please try again."
|
92 |
+
}
|
93 |
+
}
|
94 |
+
};
|
src/i18n/translations/es.ts
ADDED
@@ -0,0 +1,94 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
export const es = {
|
2 |
+
welcome: {
|
3 |
+
title: "Pensar en Sintonía",
|
4 |
+
subtitle: "¡En este juego te unes a la IA para adivinar palabras secretas!",
|
5 |
+
startButton: "Comenzar Juego",
|
6 |
+
howToPlay: "Cómo Jugar",
|
7 |
+
leaderboard: "Tabla de Posiciones"
|
8 |
+
},
|
9 |
+
howToPlay: {
|
10 |
+
setup: {
|
11 |
+
title: "La Configuración",
|
12 |
+
description: "Trabajarás con dos IAs: una como compañera dando pistas y otra intentando adivinar la palabra."
|
13 |
+
},
|
14 |
+
goal: {
|
15 |
+
title: "Tu Objetivo",
|
16 |
+
description: "Ayuda a la IA a adivinar la palabra secreta usando pistas de una sola palabra. ¡Cada adivinanza correcta te da un punto!"
|
17 |
+
},
|
18 |
+
rules: {
|
19 |
+
title: "Las Reglas",
|
20 |
+
items: [
|
21 |
+
"Solo una palabra por pista",
|
22 |
+
"No usar partes de la palabra secreta ni traducciones",
|
23 |
+
"Las pistas deben relacionarse con la palabra (¡sé creativo!)",
|
24 |
+
"No deletrear la respuesta"
|
25 |
+
]
|
26 |
+
}
|
27 |
+
},
|
28 |
+
game: {
|
29 |
+
buildDescription: "Construye una Descripción",
|
30 |
+
buildSubtitle: "¡Alterna con la IA para describir tu palabra sin usar la palabra misma!",
|
31 |
+
startSentence: "Comienza tu frase...",
|
32 |
+
inputPlaceholder: "Ingresa tu palabra (solo letras)...",
|
33 |
+
addWord: "Agregar Palabra",
|
34 |
+
makeGuess: "Hacer que la IA Adivine",
|
35 |
+
aiThinking: "La IA está pensando...",
|
36 |
+
aiDelayed: "La IA está ocupada en este momento. Por favor, inténtalo de nuevo en un momento.",
|
37 |
+
invalidWord: "Palabra Inválida",
|
38 |
+
cantUseTargetWord: "No puedes usar palabras que contengan",
|
39 |
+
lettersOnly: "Por favor usa solo letras (sin números ni caracteres especiales)"
|
40 |
+
},
|
41 |
+
guess: {
|
42 |
+
title: "Intento de la IA",
|
43 |
+
sentence: "Tu frase",
|
44 |
+
aiGuessed: "La IA adivinó",
|
45 |
+
correct: "¡Adivinanza correcta! 🎉 ¿Listo para la siguiente ronda? Presiona Enter",
|
46 |
+
incorrect: "¡Juego terminado! Presiona Enter para jugar de nuevo",
|
47 |
+
nextRound: "Siguiente Ronda",
|
48 |
+
playAgain: "Jugar de Nuevo",
|
49 |
+
viewLeaderboard: "Ver Tabla de Posiciones"
|
50 |
+
},
|
51 |
+
gameOver: {
|
52 |
+
title: "¡Juego Terminado!",
|
53 |
+
completedRounds: "¡Completaste {count} rondas exitosamente!",
|
54 |
+
playAgain: "Jugar de Nuevo"
|
55 |
+
},
|
56 |
+
themes: {
|
57 |
+
title: "Elegir un Tema",
|
58 |
+
subtitle: "Selecciona un tema para tu aventura de adivinanzas",
|
59 |
+
standard: "",
|
60 |
+
technology: "Tecnología",
|
61 |
+
sports: "Deportes",
|
62 |
+
food: "Comida",
|
63 |
+
custom: "Elige tu tema",
|
64 |
+
customPlaceholder: "Ingresa un tema (ej: Animales, Películas)",
|
65 |
+
continue: "Continuar",
|
66 |
+
generating: "Generando palabras temáticas...",
|
67 |
+
pressKey: "Presiona"
|
68 |
+
},
|
69 |
+
leaderboard: {
|
70 |
+
title: "Tabla de Posiciones",
|
71 |
+
yourScore: "Tu puntaje",
|
72 |
+
roundCount: "rondas",
|
73 |
+
wordsPerRound: "palabras/ronda",
|
74 |
+
enterName: "Ingresa tu nombre (solo letras y números)",
|
75 |
+
submit: "Enviar Puntaje",
|
76 |
+
submitting: "Enviando...",
|
77 |
+
rank: "Posición",
|
78 |
+
player: "Jugador",
|
79 |
+
roundsColumn: "Rondas",
|
80 |
+
avgWords: "Prom. Palabras/Ronda",
|
81 |
+
noScores: "Aún no hay puntajes altos. ¡Sé el primero!",
|
82 |
+
previous: "Anterior",
|
83 |
+
next: "Siguiente",
|
84 |
+
error: {
|
85 |
+
invalidName: "Por favor ingresa un nombre válido (solo letras y números)",
|
86 |
+
noRounds: "Necesitas completar al menos una ronda para enviar un puntaje",
|
87 |
+
alreadySubmitted: "Ya has enviado tu puntaje para este juego",
|
88 |
+
newHighScore: "¡Nuevo Récord!",
|
89 |
+
beatRecord: "¡Superaste tu récord anterior de {score} rondas!",
|
90 |
+
notHigher: "Tu puntaje actual ({current}) no es mayor que tu mejor puntaje ({best})",
|
91 |
+
submitError: "Error al enviar el puntaje. Por favor intenta de nuevo."
|
92 |
+
}
|
93 |
+
}
|
94 |
+
};
|
src/i18n/translations/fr.ts
ADDED
@@ -0,0 +1,94 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
export const fr = {
|
2 |
+
welcome: {
|
3 |
+
title: "Penser en Sync",
|
4 |
+
subtitle: "Dans ce jeu, vous faites équipe avec l'IA pour deviner des mots secrets !",
|
5 |
+
startButton: "Commencer",
|
6 |
+
howToPlay: "Comment Jouer",
|
7 |
+
leaderboard: "Classement"
|
8 |
+
},
|
9 |
+
howToPlay: {
|
10 |
+
setup: {
|
11 |
+
title: "La Configuration",
|
12 |
+
description: "Vous travaillerez avec deux IA : une comme partenaire donnant des indices, et une autre essayant de deviner le mot."
|
13 |
+
},
|
14 |
+
goal: {
|
15 |
+
title: "Votre Objectif",
|
16 |
+
description: "Aidez l'IA à deviner le mot secret en utilisant des indices d'un seul mot. Chaque devinette correcte vous rapporte un point !"
|
17 |
+
},
|
18 |
+
rules: {
|
19 |
+
title: "Les Règles",
|
20 |
+
items: [
|
21 |
+
"Un seul mot par indice",
|
22 |
+
"Pas de parties du mot secret ni de traductions",
|
23 |
+
"Les indices doivent être liés au mot (soyez créatif !)",
|
24 |
+
"Ne pas épeler la réponse"
|
25 |
+
]
|
26 |
+
}
|
27 |
+
},
|
28 |
+
game: {
|
29 |
+
buildDescription: "Construire une Description",
|
30 |
+
buildSubtitle: "Alternez avec l'IA pour décrire votre mot sans utiliser le mot lui-même !",
|
31 |
+
startSentence: "Commencez votre phrase...",
|
32 |
+
inputPlaceholder: "Entrez votre mot (lettres uniquement)...",
|
33 |
+
addWord: "Ajouter un Mot",
|
34 |
+
makeGuess: "Faire Deviner l'IA",
|
35 |
+
aiThinking: "L'IA réfléchit...",
|
36 |
+
aiDelayed: "L'IA est actuellement occupée. Veuillez réessayer dans un moment.",
|
37 |
+
invalidWord: "Mot Invalide",
|
38 |
+
cantUseTargetWord: "Vous ne pouvez pas utiliser des mots qui contiennent",
|
39 |
+
lettersOnly: "Veuillez utiliser uniquement des lettres (pas de chiffres ni de caractères spéciaux)"
|
40 |
+
},
|
41 |
+
guess: {
|
42 |
+
title: "Devinette de l'IA",
|
43 |
+
sentence: "Votre phrase",
|
44 |
+
aiGuessed: "L'IA a deviné",
|
45 |
+
correct: "Devinette correcte ! 🎉 Prêt pour le prochain tour ? Appuyez sur Entrée",
|
46 |
+
incorrect: "Partie terminée ! Appuyez sur Entrée pour rejouer",
|
47 |
+
nextRound: "Tour Suivant",
|
48 |
+
playAgain: "Rejouer",
|
49 |
+
viewLeaderboard: "Voir le Classement"
|
50 |
+
},
|
51 |
+
gameOver: {
|
52 |
+
title: "Partie Terminée !",
|
53 |
+
completedRounds: "Vous avez complété {count} tours avec succès !",
|
54 |
+
playAgain: "Rejouer"
|
55 |
+
},
|
56 |
+
themes: {
|
57 |
+
title: "Choisir un Thème",
|
58 |
+
subtitle: "Sélectionnez un thème pour votre aventure de devinettes",
|
59 |
+
standard: "",
|
60 |
+
technology: "Technologie",
|
61 |
+
sports: "Sports",
|
62 |
+
food: "Nourriture",
|
63 |
+
custom: "Choisissez votre thème",
|
64 |
+
customPlaceholder: "Entrez un thème (ex: Animaux, Films)",
|
65 |
+
continue: "Continuer",
|
66 |
+
generating: "Génération des mots thématiques...",
|
67 |
+
pressKey: "Appuyez sur"
|
68 |
+
},
|
69 |
+
leaderboard: {
|
70 |
+
title: "Classement",
|
71 |
+
yourScore: "Votre score",
|
72 |
+
roundCount: "tours",
|
73 |
+
wordsPerRound: "mots/tour",
|
74 |
+
enterName: "Entrez votre nom (lettres et chiffres uniquement)",
|
75 |
+
submit: "Soumettre le Score",
|
76 |
+
submitting: "Soumission...",
|
77 |
+
rank: "Rang",
|
78 |
+
player: "Joueur",
|
79 |
+
roundsColumn: "Tours",
|
80 |
+
avgWords: "Moy. Mots/Tour",
|
81 |
+
noScores: "Pas encore de scores. Soyez le premier !",
|
82 |
+
previous: "Précédent",
|
83 |
+
next: "Suivant",
|
84 |
+
error: {
|
85 |
+
invalidName: "Veuillez entrer un nom valide (uniquement lettres et chiffres)",
|
86 |
+
noRounds: "Vous devez compléter au moins un tour pour soumettre un score",
|
87 |
+
alreadySubmitted: "Vous avez déjà soumis votre score pour cette partie",
|
88 |
+
newHighScore: "Nouveau Record !",
|
89 |
+
beatRecord: "Vous avez battu votre record précédent de {score} tours !",
|
90 |
+
notHigher: "Votre score actuel ({current}) n'est pas supérieur à votre meilleur score ({best})",
|
91 |
+
submitError: "Échec de la soumission du score. Veuillez réessayer."
|
92 |
+
}
|
93 |
+
}
|
94 |
+
};
|
src/i18n/translations/index.ts
ADDED
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { en } from './en';
|
2 |
+
import { fr } from './fr';
|
3 |
+
import { de } from './de';
|
4 |
+
import { it } from './it';
|
5 |
+
import { es } from './es';
|
6 |
+
|
7 |
+
export const translations = {
|
8 |
+
en,
|
9 |
+
fr,
|
10 |
+
de,
|
11 |
+
it,
|
12 |
+
es
|
13 |
+
} as const;
|
14 |
+
|
15 |
+
export type Language = keyof typeof translations;
|
16 |
+
export type Translation = typeof en;
|
src/i18n/translations/it.ts
ADDED
@@ -0,0 +1,94 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
export const it = {
|
2 |
+
welcome: {
|
3 |
+
title: "Pensare in Sincronia",
|
4 |
+
subtitle: "In questo gioco fai squadra con l'IA per indovinare parole segrete!",
|
5 |
+
startButton: "Inizia Gioco",
|
6 |
+
howToPlay: "Come Giocare",
|
7 |
+
leaderboard: "Classifica"
|
8 |
+
},
|
9 |
+
howToPlay: {
|
10 |
+
setup: {
|
11 |
+
title: "La Configurazione",
|
12 |
+
description: "Lavorerai con due IA: una come partner che fornisce indizi e un'altra che cerca di indovinare la parola."
|
13 |
+
},
|
14 |
+
goal: {
|
15 |
+
title: "Il tuo Obiettivo",
|
16 |
+
description: "Aiuta l'IA a indovinare la parola segreta usando indizi di una sola parola. Ogni risposta corretta ti fa guadagnare un punto!"
|
17 |
+
},
|
18 |
+
rules: {
|
19 |
+
title: "Le Regole",
|
20 |
+
items: [
|
21 |
+
"Solo una parola per indizio",
|
22 |
+
"Niente parti della parola segreta o traduzioni",
|
23 |
+
"Gli indizi devono essere correlati alla parola (sii creativo!)",
|
24 |
+
"Non si può sillabare la risposta"
|
25 |
+
]
|
26 |
+
}
|
27 |
+
},
|
28 |
+
game: {
|
29 |
+
buildDescription: "Costruisci una Descrizione",
|
30 |
+
buildSubtitle: "Alternati con l'IA per descrivere la tua parola senza usare la parola stessa!",
|
31 |
+
startSentence: "Inizia la tua frase...",
|
32 |
+
inputPlaceholder: "Inserisci la tua parola (solo lettere)...",
|
33 |
+
addWord: "Aggiungi Parola",
|
34 |
+
makeGuess: "Fai Indovinare l'IA",
|
35 |
+
aiThinking: "L'IA sta pensando...",
|
36 |
+
aiDelayed: "L'IA è attualmente occupata. Riprova tra un momento.",
|
37 |
+
invalidWord: "Parola Non Valida",
|
38 |
+
cantUseTargetWord: "Non puoi usare parole che contengono",
|
39 |
+
lettersOnly: "Per favore usa solo lettere (no numeri o caratteri speciali)"
|
40 |
+
},
|
41 |
+
guess: {
|
42 |
+
title: "Tentativo dell'IA",
|
43 |
+
sentence: "La tua frase",
|
44 |
+
aiGuessed: "L'IA ha indovinato",
|
45 |
+
correct: "Indovinato correttamente! 🎉 Pronto per il prossimo round? Premi Invio",
|
46 |
+
incorrect: "Game Over! Premi Invio per giocare di nuovo",
|
47 |
+
nextRound: "Prossimo Round",
|
48 |
+
playAgain: "Gioca Ancora",
|
49 |
+
viewLeaderboard: "Vedi Classifica"
|
50 |
+
},
|
51 |
+
gameOver: {
|
52 |
+
title: "Game Over!",
|
53 |
+
completedRounds: "Hai completato {count} round con successo!",
|
54 |
+
playAgain: "Gioca Ancora"
|
55 |
+
},
|
56 |
+
themes: {
|
57 |
+
title: "Scegli un Tema",
|
58 |
+
subtitle: "Seleziona un tema per la tua avventura di indovinelli",
|
59 |
+
standard: "",
|
60 |
+
technology: "Tecnologia",
|
61 |
+
sports: "Sport",
|
62 |
+
food: "Cibo",
|
63 |
+
custom: "Scegli il tuo tema",
|
64 |
+
customPlaceholder: "Inserisci un tema (es: Animali, Film)",
|
65 |
+
continue: "Continua",
|
66 |
+
generating: "Generazione parole tematiche...",
|
67 |
+
pressKey: "Premi"
|
68 |
+
},
|
69 |
+
leaderboard: {
|
70 |
+
title: "Classifica",
|
71 |
+
yourScore: "Il tuo punteggio",
|
72 |
+
roundCount: "round",
|
73 |
+
wordsPerRound: "parole/round",
|
74 |
+
enterName: "Inserisci il tuo nome (solo lettere e numeri)",
|
75 |
+
submit: "Invia Punteggio",
|
76 |
+
submitting: "Invio in corso...",
|
77 |
+
rank: "Posizione",
|
78 |
+
player: "Giocatore",
|
79 |
+
roundsColumn: "Round",
|
80 |
+
avgWords: "Media Parole/Round",
|
81 |
+
noScores: "Ancora nessun punteggio. Sii il primo!",
|
82 |
+
previous: "Precedente",
|
83 |
+
next: "Successivo",
|
84 |
+
error: {
|
85 |
+
invalidName: "Inserisci un nome valido (solo lettere e numeri)",
|
86 |
+
noRounds: "Devi completare almeno un round per inviare un punteggio",
|
87 |
+
alreadySubmitted: "Hai già inviato il tuo punteggio per questa partita",
|
88 |
+
newHighScore: "Nuovo Record!",
|
89 |
+
beatRecord: "Hai battuto il tuo record precedente di {score} round!",
|
90 |
+
notHigher: "Il tuo punteggio attuale ({current}) non è superiore al tuo miglior punteggio ({best})",
|
91 |
+
submitError: "Errore nell'invio del punteggio. Riprova."
|
92 |
+
}
|
93 |
+
}
|
94 |
+
};
|
src/services/themeService.ts
CHANGED
@@ -1,14 +1,15 @@
|
|
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) {
|
|
|
1 |
import { supabase } from "@/integrations/supabase/client";
|
2 |
+
import { Language } from "@/i18n/translations";
|
3 |
|
4 |
+
export const getThemedWord = async (theme: string, usedWords: string[] = [], language: Language = 'en'): Promise<string> => {
|
5 |
if (theme === "standard") {
|
6 |
throw new Error("Standard theme should use the words list");
|
7 |
}
|
8 |
|
9 |
+
console.log('Getting themed word for:', theme, 'language:', language, 'excluding:', usedWords);
|
10 |
|
11 |
const { data, error } = await supabase.functions.invoke('generate-themed-word', {
|
12 |
+
body: { theme, usedWords, language }
|
13 |
});
|
14 |
|
15 |
if (error) {
|
supabase/functions/generate-themed-word/index.ts
CHANGED
@@ -7,39 +7,54 @@ const corsHeaders = {
|
|
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:
|
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:
|
42 |
-
temperature: 0.
|
43 |
});
|
44 |
|
45 |
const word = response.choices[0].message.content.trim();
|
|
|
7 |
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
|
8 |
};
|
9 |
|
10 |
+
const languagePrompts = {
|
11 |
+
en: {
|
12 |
+
systemPrompt: "You are helping generate words for a word-guessing game. Generate a single word in English related to the theme",
|
13 |
+
requirements: "The word should be:\n- A single word (no spaces or hyphens)\n- Common enough that people would know it\n- Specific enough to be interesting\n- Related to the theme\n- Between 4 and 12 letters\n- A noun\n- NOT be any of these previously used words:"
|
14 |
+
},
|
15 |
+
fr: {
|
16 |
+
systemPrompt: "Vous aidez à générer des mots pour un jeu de devinettes. Générez un seul mot en français lié au thème",
|
17 |
+
requirements: "Le mot doit être :\n- Un seul mot (pas d'espaces ni de traits d'union)\n- Assez courant pour que les gens le connaissent\n- Suffisamment spécifique pour être intéressant\n- En rapport avec le thème\n- Entre 4 et 12 lettres\n- Un nom\n- NE PAS être l'un de ces mots déjà utilisés :"
|
18 |
+
},
|
19 |
+
de: {
|
20 |
+
systemPrompt: "Sie helfen bei der Generierung von Wörtern für ein Worträtselspiel. Generieren Sie ein einzelnes Wort auf Deutsch zum Thema",
|
21 |
+
requirements: "Das Wort sollte:\n- Ein einzelnes Wort sein (keine Leerzeichen oder Bindestriche)\n- Häufig genug, dass Menschen es kennen\n- Spezifisch genug, um interessant zu sein\n- Zum Thema passen\n- Zwischen 4 und 12 Buchstaben lang sein\n- Ein Substantiv sein\n- NICHT eines dieser bereits verwendeten Wörter sein:"
|
22 |
+
},
|
23 |
+
it: {
|
24 |
+
systemPrompt: "Stai aiutando a generare parole per un gioco di indovinelli. Genera una singola parola in italiano legata al tema",
|
25 |
+
requirements: "La parola deve essere:\n- Una singola parola (senza spazi o trattini)\n- Abbastanza comune da essere conosciuta\n- Sufficientemente specifica da essere interessante\n- Correlata al tema\n- Tra 4 e 12 lettere\n- Un sostantivo\n- NON essere una di queste parole già utilizzate:"
|
26 |
+
},
|
27 |
+
es: {
|
28 |
+
systemPrompt: "Estás ayudando a generar palabras para un juego de adivinanzas. Genera una sola palabra en español relacionada con el tema",
|
29 |
+
requirements: "La palabra debe ser:\n- Una sola palabra (sin espacios ni guiones)\n- Lo suficientemente común para que la gente la conozca\n- Lo suficientemente específica para ser interesante\n- Relacionada con el tema\n- Entre 4 y 12 letras\n- Un sustantivo\n- NO ser ninguna de estas palabras ya utilizadas:"
|
30 |
+
}
|
31 |
+
};
|
32 |
+
|
33 |
serve(async (req) => {
|
34 |
if (req.method === 'OPTIONS') {
|
35 |
return new Response(null, { headers: corsHeaders });
|
36 |
}
|
37 |
|
38 |
try {
|
39 |
+
const { theme, usedWords = [], language = 'en' } = await req.json();
|
40 |
+
console.log('Generating word for theme:', theme, 'language:', language, 'excluding:', usedWords);
|
41 |
|
42 |
const client = new Mistral({
|
43 |
apiKey: Deno.env.get('MISTRAL_API_KEY'),
|
44 |
});
|
45 |
|
46 |
+
const prompts = languagePrompts[language as keyof typeof languagePrompts] || languagePrompts.en;
|
47 |
+
|
48 |
const response = await client.chat.complete({
|
49 |
model: "mistral-large-latest",
|
50 |
messages: [
|
51 |
{
|
52 |
role: "system",
|
53 |
+
content: `${prompts.systemPrompt} "${theme}".\n${prompts.requirements} ${usedWords.join(', ')}\n\nRespond with just the word in UPPERCASE, nothing else.`
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
54 |
}
|
55 |
],
|
56 |
+
maxTokens: 15,
|
57 |
+
temperature: 0.99
|
58 |
});
|
59 |
|
60 |
const word = response.choices[0].message.content.trim();
|
supabase/functions/generate-word/index.ts
CHANGED
@@ -6,23 +6,50 @@ const corsHeaders = {
|
|
6 |
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
|
7 |
};
|
8 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
9 |
serve(async (req) => {
|
10 |
if (req.method === 'OPTIONS') {
|
11 |
return new Response(null, { headers: corsHeaders });
|
12 |
}
|
13 |
|
14 |
try {
|
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 |
});
|
24 |
|
25 |
-
// Add retry logic with exponential backoff
|
26 |
const maxRetries = 3;
|
27 |
let retryCount = 0;
|
28 |
let lastError = null;
|
@@ -34,25 +61,21 @@ serve(async (req) => {
|
|
34 |
messages: [
|
35 |
{
|
36 |
role: "system",
|
37 |
-
content:
|
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 |
],
|
43 |
-
maxTokens:
|
44 |
temperature: 0.5
|
45 |
});
|
46 |
|
47 |
const aiResponse = response.choices[0].message.content.trim();
|
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(/[.,!?]$/, '');
|
56 |
|
57 |
console.log('Extracted new word:', newWord);
|
58 |
|
@@ -64,27 +87,23 @@ serve(async (req) => {
|
|
64 |
console.error(`Attempt ${retryCount + 1} failed:`, error);
|
65 |
lastError = error;
|
66 |
|
67 |
-
// If it's a rate limit error, wait before retrying
|
68 |
if (error.message?.includes('rate limit') || error.status === 429) {
|
69 |
-
const waitTime = Math.pow(2, retryCount) * 1000;
|
70 |
console.log(`Rate limit hit, waiting ${waitTime}ms before retry`);
|
71 |
await new Promise(resolve => setTimeout(resolve, waitTime));
|
72 |
retryCount++;
|
73 |
continue;
|
74 |
}
|
75 |
|
76 |
-
// If it's not a rate limit error, throw immediately
|
77 |
throw error;
|
78 |
}
|
79 |
}
|
80 |
|
81 |
-
// If we've exhausted all retries
|
82 |
throw new Error(`Failed after ${maxRetries} attempts. Last error: ${lastError?.message}`);
|
83 |
|
84 |
} catch (error) {
|
85 |
console.error('Error generating word:', error);
|
86 |
|
87 |
-
// Provide a more user-friendly error message
|
88 |
const errorMessage = error.message?.includes('rate limit')
|
89 |
? "The AI service is currently busy. Please try again in a few moments."
|
90 |
: "Sorry, there was an error generating the word. Please try again.";
|
|
|
6 |
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
|
7 |
};
|
8 |
|
9 |
+
const languagePrompts = {
|
10 |
+
en: {
|
11 |
+
systemPrompt: "You are helping in a word game. The secret word is",
|
12 |
+
task: "Your task is to find a sentence to describe this word without using it directly.",
|
13 |
+
instruction: "Answer with a complete, grammatically correct sentence that starts with"
|
14 |
+
},
|
15 |
+
fr: {
|
16 |
+
systemPrompt: "Vous aidez dans un jeu de mots. Le mot secret est",
|
17 |
+
task: "Votre tâche est de trouver une phrase pour décrire ce mot sans l'utiliser directement.",
|
18 |
+
instruction: "Répondez avec une phrase complète et grammaticalement correcte qui commence par"
|
19 |
+
},
|
20 |
+
de: {
|
21 |
+
systemPrompt: "Sie helfen bei einem Wortspiel. Das geheime Wort ist",
|
22 |
+
task: "Ihre Aufgabe ist es, einen Satz zu finden, der dieses Wort beschreibt, ohne es direkt zu verwenden.",
|
23 |
+
instruction: "Antworten Sie mit einem vollständigen, grammatikalisch korrekten Satz, der beginnt mit"
|
24 |
+
},
|
25 |
+
it: {
|
26 |
+
systemPrompt: "Stai aiutando in un gioco di parole. La parola segreta è",
|
27 |
+
task: "Il tuo compito è trovare una frase per descrivere questa parola senza usarla direttamente.",
|
28 |
+
instruction: "Rispondi con una frase completa e grammaticalmente corretta che inizia con"
|
29 |
+
},
|
30 |
+
es: {
|
31 |
+
systemPrompt: "Estás ayudando en un juego de palabras. La palabra secreta es",
|
32 |
+
task: "Tu tarea es encontrar una frase para describir esta palabra sin usarla directamente.",
|
33 |
+
instruction: "Responde con una frase completa y gramaticalmente correcta que comience con"
|
34 |
+
}
|
35 |
+
};
|
36 |
+
|
37 |
serve(async (req) => {
|
38 |
if (req.method === 'OPTIONS') {
|
39 |
return new Response(null, { headers: corsHeaders });
|
40 |
}
|
41 |
|
42 |
try {
|
43 |
+
const { currentWord, currentSentence, language = 'en' } = await req.json();
|
44 |
+
console.log('Generating word for:', { currentWord, currentSentence, language });
|
45 |
|
|
|
46 |
const existingSentence = currentSentence || '';
|
47 |
+
const prompts = languagePrompts[language as keyof typeof languagePrompts] || languagePrompts.en;
|
48 |
|
49 |
const client = new Mistral({
|
50 |
apiKey: Deno.env.get('MISTRAL_API_KEY'),
|
51 |
});
|
52 |
|
|
|
53 |
const maxRetries = 3;
|
54 |
let retryCount = 0;
|
55 |
let lastError = null;
|
|
|
61 |
messages: [
|
62 |
{
|
63 |
role: "system",
|
64 |
+
content: `${prompts.systemPrompt} "${currentWord}". ${prompts.task} ${prompts.instruction} "${existingSentence}". Do not add quotes or backticks. Just answer with the sentence.`
|
|
|
|
|
|
|
65 |
}
|
66 |
],
|
67 |
+
maxTokens: 200,
|
68 |
temperature: 0.5
|
69 |
});
|
70 |
|
71 |
const aiResponse = response.choices[0].message.content.trim();
|
72 |
console.log('AI full response:', aiResponse);
|
73 |
|
|
|
74 |
const newWord = aiResponse
|
75 |
.slice(existingSentence.length)
|
76 |
.trim()
|
77 |
.split(' ')[0]
|
78 |
+
.replace(/[.,!?]$/, '');
|
79 |
|
80 |
console.log('Extracted new word:', newWord);
|
81 |
|
|
|
87 |
console.error(`Attempt ${retryCount + 1} failed:`, error);
|
88 |
lastError = error;
|
89 |
|
|
|
90 |
if (error.message?.includes('rate limit') || error.status === 429) {
|
91 |
+
const waitTime = Math.pow(2, retryCount) * 1000;
|
92 |
console.log(`Rate limit hit, waiting ${waitTime}ms before retry`);
|
93 |
await new Promise(resolve => setTimeout(resolve, waitTime));
|
94 |
retryCount++;
|
95 |
continue;
|
96 |
}
|
97 |
|
|
|
98 |
throw error;
|
99 |
}
|
100 |
}
|
101 |
|
|
|
102 |
throw new Error(`Failed after ${maxRetries} attempts. Last error: ${lastError?.message}`);
|
103 |
|
104 |
} catch (error) {
|
105 |
console.error('Error generating word:', error);
|
106 |
|
|
|
107 |
const errorMessage = error.message?.includes('rate limit')
|
108 |
? "The AI service is currently busy. Please try again in a few moments."
|
109 |
: "Sorry, there was an error generating the word. Please try again.";
|
supabase/functions/guess-word/index.ts
CHANGED
@@ -6,20 +6,44 @@ const corsHeaders = {
|
|
6 |
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
|
7 |
};
|
8 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
9 |
serve(async (req) => {
|
10 |
if (req.method === 'OPTIONS') {
|
11 |
return new Response(null, { headers: corsHeaders });
|
12 |
}
|
13 |
|
14 |
try {
|
15 |
-
const { sentence } = await req.json();
|
16 |
-
console.log('Trying to guess word from sentence:', sentence);
|
17 |
|
18 |
const client = new Mistral({
|
19 |
apiKey: Deno.env.get('MISTRAL_API_KEY'),
|
20 |
});
|
21 |
|
22 |
-
|
|
|
23 |
const maxRetries = 3;
|
24 |
let retryCount = 0;
|
25 |
let lastError = null;
|
@@ -31,13 +55,11 @@ serve(async (req) => {
|
|
31 |
messages: [
|
32 |
{
|
33 |
role: "system",
|
34 |
-
content:
|
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:
|
41 |
}
|
42 |
],
|
43 |
maxTokens: 10,
|
@@ -55,27 +77,23 @@ serve(async (req) => {
|
|
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;
|
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.";
|
|
|
6 |
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
|
7 |
};
|
8 |
|
9 |
+
const languagePrompts = {
|
10 |
+
en: {
|
11 |
+
systemPrompt: "You are playing a word guessing game in English. Given a descriptive sentence, your task is to guess the single word being described.",
|
12 |
+
instruction: "Based on this description, what single word is being described:"
|
13 |
+
},
|
14 |
+
fr: {
|
15 |
+
systemPrompt: "Vous jouez à un jeu de devinettes de mots en français. À partir d'une phrase descriptive, votre tâche est de deviner le mot unique décrit.",
|
16 |
+
instruction: "D'après cette description, quel mot unique est décrit :"
|
17 |
+
},
|
18 |
+
de: {
|
19 |
+
systemPrompt: "Sie spielen ein Worträtselspiel auf Deutsch. Anhand eines beschreibenden Satzes ist es Ihre Aufgabe, das beschriebene einzelne Wort zu erraten.",
|
20 |
+
instruction: "Welches einzelne Wort wird basierend auf dieser Beschreibung beschrieben:"
|
21 |
+
},
|
22 |
+
it: {
|
23 |
+
systemPrompt: "Stai giocando a un gioco di indovinelli in italiano. Data una frase descrittiva, il tuo compito è indovinare la singola parola descritta.",
|
24 |
+
instruction: "In base a questa descrizione, quale singola parola viene descritta:"
|
25 |
+
},
|
26 |
+
es: {
|
27 |
+
systemPrompt: "Estás jugando a un juego de adivinanzas de palabras en español. Dada una frase descriptiva, tu tarea es adivinar la única palabra que se describe.",
|
28 |
+
instruction: "Según esta descripción, ¿qué palabra única se está describiendo:"
|
29 |
+
}
|
30 |
+
};
|
31 |
+
|
32 |
serve(async (req) => {
|
33 |
if (req.method === 'OPTIONS') {
|
34 |
return new Response(null, { headers: corsHeaders });
|
35 |
}
|
36 |
|
37 |
try {
|
38 |
+
const { sentence, language = 'en' } = await req.json();
|
39 |
+
console.log('Trying to guess word from sentence:', sentence, 'language:', language);
|
40 |
|
41 |
const client = new Mistral({
|
42 |
apiKey: Deno.env.get('MISTRAL_API_KEY'),
|
43 |
});
|
44 |
|
45 |
+
const prompts = languagePrompts[language as keyof typeof languagePrompts] || languagePrompts.en;
|
46 |
+
|
47 |
const maxRetries = 3;
|
48 |
let retryCount = 0;
|
49 |
let lastError = null;
|
|
|
55 |
messages: [
|
56 |
{
|
57 |
role: "system",
|
58 |
+
content: `${prompts.systemPrompt} Respond with ONLY the word you think is being described, in uppercase letters. Do not add any explanation or punctuation.`
|
|
|
|
|
59 |
},
|
60 |
{
|
61 |
role: "user",
|
62 |
+
content: `${prompts.instruction} "${sentence}"`
|
63 |
}
|
64 |
],
|
65 |
maxTokens: 10,
|
|
|
77 |
console.error(`Attempt ${retryCount + 1} failed:`, error);
|
78 |
lastError = error;
|
79 |
|
|
|
80 |
if (error.message?.includes('rate limit') || error.status === 429) {
|
81 |
+
const waitTime = Math.pow(2, retryCount) * 1000;
|
82 |
console.log(`Rate limit hit, waiting ${waitTime}ms before retry`);
|
83 |
await new Promise(resolve => setTimeout(resolve, waitTime));
|
84 |
retryCount++;
|
85 |
continue;
|
86 |
}
|
87 |
|
|
|
88 |
throw error;
|
89 |
}
|
90 |
}
|
91 |
|
|
|
92 |
throw new Error(`Failed after ${maxRetries} attempts. Last error: ${lastError?.message}`);
|
93 |
|
94 |
} catch (error) {
|
95 |
console.error('Error generating guess:', error);
|
96 |
|
|
|
97 |
const errorMessage = error.message?.includes('rate limit')
|
98 |
? "The AI service is currently busy. Please try again in a few moments."
|
99 |
: "Sorry, there was an error generating the guess. Please try again.";
|