Felix Zieger
commited on
Commit
·
5835ecd
1
Parent(s):
f114b7f
update
Browse files- README.md +5 -4
- src/components/GameContainer.tsx +1 -1
- src/components/game/GuessDisplay.tsx +72 -29
- src/components/game/SentenceBuilder.tsx +28 -11
- src/i18n/translations/de.ts +6 -5
- src/i18n/translations/en.ts +77 -76
- src/i18n/translations/es.ts +77 -75
- src/i18n/translations/fr.ts +5 -3
- src/i18n/translations/it.ts +5 -3
- src/integrations/supabase/types.ts +27 -0
- src/lib/words.ts +95 -93
- src/services/mistralService.ts +28 -0
- supabase/functions/detect-fraud/index.ts +127 -0
- supabase/functions/guess-word/index.ts +11 -10
README.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1 |
---
|
2 |
title: Think in Sync
|
3 |
-
emoji:
|
4 |
colorFrom: blue
|
5 |
colorTo: pink
|
6 |
sdk: docker
|
@@ -9,8 +9,8 @@ pinned: false
|
|
9 |
---
|
10 |
# Think in Sync
|
11 |
|
12 |
-
You will be given a secret word.
|
13 |
-
However, you
|
14 |
|
15 |
## Develop locally
|
16 |
|
@@ -25,10 +25,11 @@ npm run dev
|
|
25 |
|
26 |
## What technologies are used for this project?
|
27 |
|
28 |
-
This project is built with
|
29 |
|
30 |
- Vite
|
31 |
- TypeScript
|
32 |
- React
|
33 |
- shadcn-ui
|
34 |
- Tailwind CSS
|
|
|
|
1 |
---
|
2 |
title: Think in Sync
|
3 |
+
emoji: 🧠
|
4 |
colorFrom: blue
|
5 |
colorTo: pink
|
6 |
sdk: docker
|
|
|
9 |
---
|
10 |
# Think in Sync
|
11 |
|
12 |
+
You will be given a secret word. You aim to describe this secret word so an AI can guess it.
|
13 |
+
However, you can only say one word at a time, taking turns with another AI.
|
14 |
|
15 |
## Develop locally
|
16 |
|
|
|
25 |
|
26 |
## What technologies are used for this project?
|
27 |
|
28 |
+
This project is built with
|
29 |
|
30 |
- Vite
|
31 |
- TypeScript
|
32 |
- React
|
33 |
- shadcn-ui
|
34 |
- Tailwind CSS
|
35 |
+
- Supabase
|
src/components/GameContainer.tsx
CHANGED
@@ -172,7 +172,7 @@ export const GameContainer = () => {
|
|
172 |
};
|
173 |
|
174 |
const handlePlayAgain = () => {
|
175 |
-
setGameState("
|
176 |
setSentence([]);
|
177 |
setAiGuess("");
|
178 |
setCurrentWord("");
|
|
|
172 |
};
|
173 |
|
174 |
const handlePlayAgain = () => {
|
175 |
+
setGameState("theme-selection");
|
176 |
setSentence([]);
|
177 |
setAiGuess("");
|
178 |
setCurrentWord("");
|
src/components/game/GuessDisplay.tsx
CHANGED
@@ -6,8 +6,10 @@ import {
|
|
6 |
DialogTrigger,
|
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[];
|
@@ -15,6 +17,7 @@ interface GuessDisplayProps {
|
|
15 |
currentWord: string;
|
16 |
onNextRound: () => void;
|
17 |
onPlayAgain: () => void;
|
|
|
18 |
currentScore: number;
|
19 |
avgWordsPerRound: number;
|
20 |
}
|
@@ -25,57 +28,97 @@ export const GuessDisplay = ({
|
|
25 |
currentWord,
|
26 |
onNextRound,
|
27 |
onPlayAgain,
|
|
|
28 |
currentScore,
|
29 |
avgWordsPerRound,
|
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
|
37 |
initial={{ opacity: 0 }}
|
38 |
animate={{ opacity: 1 }}
|
39 |
-
className="text-center relative"
|
40 |
>
|
41 |
-
<div className="
|
42 |
-
<
|
43 |
-
|
44 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
45 |
</div>
|
46 |
|
47 |
-
<h2 className="mb-4 text-2xl font-semibold text-gray-900">Think in Sync</h2>
|
48 |
-
|
49 |
<div>
|
50 |
-
<
|
51 |
-
<div className="mb-6 overflow-hidden rounded-lg bg-secondary/10">
|
52 |
-
<p className="p-4 text-2xl font-bold tracking-wider text-secondary">
|
53 |
-
{currentWord}
|
54 |
-
</p>
|
55 |
-
</div>
|
56 |
-
</div>
|
57 |
|
58 |
-
|
59 |
-
|
60 |
-
<
|
61 |
-
|
62 |
-
|
63 |
-
{sentence.join(" ")}
|
64 |
</p>
|
65 |
</div>
|
66 |
</div>
|
|
|
67 |
|
68 |
-
|
69 |
-
|
70 |
-
|
71 |
-
|
72 |
-
|
73 |
-
|
74 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
75 |
</div>
|
76 |
</div>
|
77 |
|
78 |
-
<div className="
|
79 |
{isGuessCorrect() ? (
|
80 |
<Button
|
81 |
onClick={onNextRound}
|
|
|
6 |
DialogTrigger,
|
7 |
} from "@/components/ui/dialog";
|
8 |
import { HighScoreBoard } from "@/components/HighScoreBoard";
|
9 |
+
import { useState, useEffect } from "react";
|
10 |
import { useTranslation } from "@/hooks/useTranslation";
|
11 |
+
import { supabase } from "@/integrations/supabase/client";
|
12 |
+
import { House } from "lucide-react";
|
13 |
|
14 |
interface GuessDisplayProps {
|
15 |
sentence: string[];
|
|
|
17 |
currentWord: string;
|
18 |
onNextRound: () => void;
|
19 |
onPlayAgain: () => void;
|
20 |
+
onBack?: () => void;
|
21 |
currentScore: number;
|
22 |
avgWordsPerRound: number;
|
23 |
}
|
|
|
28 |
currentWord,
|
29 |
onNextRound,
|
30 |
onPlayAgain,
|
31 |
+
onBack,
|
32 |
currentScore,
|
33 |
avgWordsPerRound,
|
34 |
}: GuessDisplayProps) => {
|
35 |
const isGuessCorrect = () => aiGuess.toLowerCase() === currentWord.toLowerCase();
|
36 |
+
const isCheating = () => aiGuess === 'CHEATING';
|
37 |
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
38 |
const t = useTranslation();
|
39 |
|
40 |
+
useEffect(() => {
|
41 |
+
const saveGameResult = async () => {
|
42 |
+
try {
|
43 |
+
const { error } = await supabase
|
44 |
+
.from('game_results')
|
45 |
+
.insert({
|
46 |
+
target_word: currentWord,
|
47 |
+
description: sentence.join(' '),
|
48 |
+
ai_guess: aiGuess,
|
49 |
+
is_correct: isGuessCorrect()
|
50 |
+
});
|
51 |
+
|
52 |
+
if (error) {
|
53 |
+
console.error('Error saving game result:', error);
|
54 |
+
} else {
|
55 |
+
console.log('Game result saved successfully');
|
56 |
+
}
|
57 |
+
} catch (error) {
|
58 |
+
console.error('Error in saveGameResult:', error);
|
59 |
+
}
|
60 |
+
};
|
61 |
+
|
62 |
+
saveGameResult();
|
63 |
+
}, []);
|
64 |
+
|
65 |
return (
|
66 |
<motion.div
|
67 |
initial={{ opacity: 0 }}
|
68 |
animate={{ opacity: 1 }}
|
69 |
+
className="text-center relative space-y-6"
|
70 |
>
|
71 |
+
<div className="flex items-center justify-between mb-4">
|
72 |
+
<Button
|
73 |
+
variant="ghost"
|
74 |
+
size="icon"
|
75 |
+
onClick={onBack}
|
76 |
+
className="text-gray-600 hover:text-primary"
|
77 |
+
>
|
78 |
+
<House className="h-5 w-5" />
|
79 |
+
</Button>
|
80 |
+
<div className="bg-primary/10 px-3 py-1 rounded-lg">
|
81 |
+
<span className="text-sm font-medium text-primary">
|
82 |
+
{t.game.round} {currentScore + 1}
|
83 |
+
</span>
|
84 |
+
</div>
|
85 |
+
<div className="w-8" /> {/* Spacer for centering */}
|
86 |
</div>
|
87 |
|
|
|
|
|
88 |
<div>
|
89 |
+
<h2 className="mb-4 text-2xl font-semibold text-gray-900">Think in Sync</h2>
|
|
|
|
|
|
|
|
|
|
|
|
|
90 |
|
91 |
+
<div className="space-y-2">
|
92 |
+
<p className="text-sm text-gray-600">{t.guess.goalDescription}</p>
|
93 |
+
<div className="overflow-hidden rounded-lg bg-secondary/10">
|
94 |
+
<p className="p-4 text-2xl font-bold tracking-wider text-secondary">
|
95 |
+
{currentWord}
|
|
|
96 |
</p>
|
97 |
</div>
|
98 |
</div>
|
99 |
+
</div>
|
100 |
|
101 |
+
<div className="space-y-2">
|
102 |
+
<p className="text-sm text-gray-600">{t.guess.providedDescription}</p>
|
103 |
+
<div className="rounded-lg bg-gray-50">
|
104 |
+
<p className="p-4 text-2xl tracking-wider text-gray-800">
|
105 |
+
{sentence.join(" ")}
|
106 |
+
</p>
|
107 |
+
</div>
|
108 |
+
</div>
|
109 |
+
|
110 |
+
<div className="space-y-2">
|
111 |
+
<p className="text-sm text-gray-600">
|
112 |
+
{isCheating() ? t.guess.cheatingDetected : t.guess.aiGuessedDescription}
|
113 |
+
</p>
|
114 |
+
<div className={`rounded-lg ${isGuessCorrect() ? 'bg-green-50' : 'bg-red-50'}`}>
|
115 |
+
<p className={`p-4 text-2xl font-bold tracking-wider ${isGuessCorrect() ? 'text-green-600' : 'text-red-600'}`}>
|
116 |
+
{aiGuess}
|
117 |
+
</p>
|
118 |
</div>
|
119 |
</div>
|
120 |
|
121 |
+
<div className="flex flex-col gap-4">
|
122 |
{isGuessCorrect() ? (
|
123 |
<Button
|
124 |
onClick={onNextRound}
|
src/components/game/SentenceBuilder.tsx
CHANGED
@@ -42,6 +42,7 @@ export const SentenceBuilder = ({
|
|
42 |
const inputRef = useRef<HTMLInputElement>(null);
|
43 |
const [imageLoaded, setImageLoaded] = useState(false);
|
44 |
const [showConfirmDialog, setShowConfirmDialog] = useState(false);
|
|
|
45 |
const imagePath = `/think_in_sync_assets/${currentWord.toLowerCase()}.jpg`;
|
46 |
const { toast } = useToast();
|
47 |
const t = useTranslation();
|
@@ -67,13 +68,18 @@ export const SentenceBuilder = ({
|
|
67 |
}
|
68 |
}, [isAiThinking, sentence.length]);
|
69 |
|
|
|
|
|
|
|
|
|
|
|
70 |
const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
|
71 |
if (e.shiftKey && e.key === 'Enter') {
|
72 |
e.preventDefault();
|
73 |
-
if
|
74 |
-
|
|
|
75 |
}
|
76 |
-
onMakeGuess();
|
77 |
}
|
78 |
};
|
79 |
|
@@ -82,6 +88,15 @@ export const SentenceBuilder = ({
|
|
82 |
const input = playerInput.trim().toLowerCase();
|
83 |
const target = currentWord.toLowerCase();
|
84 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
85 |
if (!/^[\p{L}]+$/u.test(input)) {
|
86 |
toast({
|
87 |
title: t.game.invalidWord,
|
@@ -169,21 +184,23 @@ export const SentenceBuilder = ({
|
|
169 |
ref={inputRef}
|
170 |
type="text"
|
171 |
value={playerInput}
|
172 |
-
onChange={(e) =>
|
173 |
-
const value = e.target.value.replace(/[^a-zA-ZÀ-ÿ]/g, '');
|
174 |
-
onInputChange(value);
|
175 |
-
}}
|
176 |
onKeyDown={handleKeyDown}
|
177 |
placeholder={t.game.inputPlaceholder}
|
178 |
-
className=
|
179 |
disabled={isAiThinking}
|
180 |
/>
|
|
|
|
|
|
|
|
|
|
|
181 |
</div>
|
182 |
<div className="flex gap-4">
|
183 |
<Button
|
184 |
type="submit"
|
185 |
className="flex-1 bg-primary text-lg hover:bg-primary/90"
|
186 |
-
disabled={!playerInput.trim() || isAiThinking}
|
187 |
>
|
188 |
{isAiThinking ? t.game.aiThinking : `${t.game.addWord} ⏎`}
|
189 |
</Button>
|
@@ -191,7 +208,7 @@ export const SentenceBuilder = ({
|
|
191 |
type="button"
|
192 |
onClick={onMakeGuess}
|
193 |
className="flex-1 bg-secondary text-lg hover:bg-secondary/90"
|
194 |
-
disabled={(!sentence.length && !playerInput.trim()) || isAiThinking}
|
195 |
>
|
196 |
{isAiThinking ? t.game.aiThinking : `${t.game.makeGuess} ⇧⏎`}
|
197 |
</Button>
|
@@ -216,4 +233,4 @@ export const SentenceBuilder = ({
|
|
216 |
</AlertDialog>
|
217 |
</motion.div>
|
218 |
);
|
219 |
-
};
|
|
|
42 |
const inputRef = useRef<HTMLInputElement>(null);
|
43 |
const [imageLoaded, setImageLoaded] = useState(false);
|
44 |
const [showConfirmDialog, setShowConfirmDialog] = useState(false);
|
45 |
+
const [hasMultipleWords, setHasMultipleWords] = useState(false);
|
46 |
const imagePath = `/think_in_sync_assets/${currentWord.toLowerCase()}.jpg`;
|
47 |
const { toast } = useToast();
|
48 |
const t = useTranslation();
|
|
|
68 |
}
|
69 |
}, [isAiThinking, sentence.length]);
|
70 |
|
71 |
+
useEffect(() => {
|
72 |
+
// Check if input contains multiple words
|
73 |
+
setHasMultipleWords(playerInput.trim().split(/\s+/).length > 1);
|
74 |
+
}, [playerInput]);
|
75 |
+
|
76 |
const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
|
77 |
if (e.shiftKey && e.key === 'Enter') {
|
78 |
e.preventDefault();
|
79 |
+
// Only trigger if buttons are not disabled and either we have a sentence or valid input
|
80 |
+
if (!hasMultipleWords && !isAiThinking && (sentence.length > 0 || playerInput.trim())) {
|
81 |
+
onMakeGuess();
|
82 |
}
|
|
|
83 |
}
|
84 |
};
|
85 |
|
|
|
88 |
const input = playerInput.trim().toLowerCase();
|
89 |
const target = currentWord.toLowerCase();
|
90 |
|
91 |
+
if (hasMultipleWords) {
|
92 |
+
toast({
|
93 |
+
title: t.game.invalidWord,
|
94 |
+
description: t.game.singleWordOnly,
|
95 |
+
variant: "destructive",
|
96 |
+
});
|
97 |
+
return;
|
98 |
+
}
|
99 |
+
|
100 |
if (!/^[\p{L}]+$/u.test(input)) {
|
101 |
toast({
|
102 |
title: t.game.invalidWord,
|
|
|
184 |
ref={inputRef}
|
185 |
type="text"
|
186 |
value={playerInput}
|
187 |
+
onChange={(e) => onInputChange(e.target.value)}
|
|
|
|
|
|
|
188 |
onKeyDown={handleKeyDown}
|
189 |
placeholder={t.game.inputPlaceholder}
|
190 |
+
className={`w-full ${hasMultipleWords ? 'border-red-500 focus-visible:ring-red-500' : ''}`}
|
191 |
disabled={isAiThinking}
|
192 |
/>
|
193 |
+
{hasMultipleWords && (
|
194 |
+
<p className="text-sm text-red-500 mt-1">
|
195 |
+
{t.game.singleWordOnly}
|
196 |
+
</p>
|
197 |
+
)}
|
198 |
</div>
|
199 |
<div className="flex gap-4">
|
200 |
<Button
|
201 |
type="submit"
|
202 |
className="flex-1 bg-primary text-lg hover:bg-primary/90"
|
203 |
+
disabled={!playerInput.trim() || isAiThinking || hasMultipleWords}
|
204 |
>
|
205 |
{isAiThinking ? t.game.aiThinking : `${t.game.addWord} ⏎`}
|
206 |
</Button>
|
|
|
208 |
type="button"
|
209 |
onClick={onMakeGuess}
|
210 |
className="flex-1 bg-secondary text-lg hover:bg-secondary/90"
|
211 |
+
disabled={(!sentence.length && !playerInput.trim()) || isAiThinking || hasMultipleWords}
|
212 |
>
|
213 |
{isAiThinking ? t.game.aiThinking : `${t.game.makeGuess} ⇧⏎`}
|
214 |
</Button>
|
|
|
233 |
</AlertDialog>
|
234 |
</motion.div>
|
235 |
);
|
236 |
+
};
|
src/i18n/translations/de.ts
CHANGED
@@ -12,6 +12,7 @@ export const de = {
|
|
12 |
invalidWord: "Ungültiges Wort",
|
13 |
cantUseTargetWord: "Du kannst das Zielwort nicht verwenden",
|
14 |
lettersOnly: "Bitte nur Buchstaben verwenden",
|
|
|
15 |
leaveGameTitle: "Spiel verlassen?",
|
16 |
leaveGameDescription: "Dein aktueller Fortschritt geht verloren. Bist du sicher, dass du das Spiel verlassen möchtest?",
|
17 |
cancel: "Abbrechen",
|
@@ -52,7 +53,8 @@ export const de = {
|
|
52 |
incorrect: "Das ist falsch.",
|
53 |
nextRound: "Nächste Runde",
|
54 |
playAgain: "Erneut spielen",
|
55 |
-
viewLeaderboard: "In Bestenliste eintragen"
|
|
|
56 |
},
|
57 |
themes: {
|
58 |
title: "Wähle ein Thema",
|
@@ -69,12 +71,12 @@ export const de = {
|
|
69 |
},
|
70 |
welcome: {
|
71 |
title: "Think in Sync",
|
72 |
-
subtitle: "
|
73 |
startButton: "Spiel starten",
|
74 |
howToPlay: "Spielanleitung",
|
75 |
leaderboard: "Bestenliste",
|
76 |
credits: "Erstellt während des",
|
77 |
-
helpWin: "Hilf uns gewinnen",
|
78 |
onHuggingface: "mit einem Like auf Huggingface"
|
79 |
},
|
80 |
howToPlay: {
|
@@ -96,5 +98,4 @@ export const de = {
|
|
96 |
]
|
97 |
}
|
98 |
}
|
99 |
-
};
|
100 |
-
|
|
|
12 |
invalidWord: "Ungültiges Wort",
|
13 |
cantUseTargetWord: "Du kannst das Zielwort nicht verwenden",
|
14 |
lettersOnly: "Bitte nur Buchstaben verwenden",
|
15 |
+
singleWordOnly: "Bitte nur ein Wort eingeben",
|
16 |
leaveGameTitle: "Spiel verlassen?",
|
17 |
leaveGameDescription: "Dein aktueller Fortschritt geht verloren. Bist du sicher, dass du das Spiel verlassen möchtest?",
|
18 |
cancel: "Abbrechen",
|
|
|
53 |
incorrect: "Das ist falsch.",
|
54 |
nextRound: "Nächste Runde",
|
55 |
playAgain: "Erneut spielen",
|
56 |
+
viewLeaderboard: "In Bestenliste eintragen",
|
57 |
+
cheatingDetected: "Betrugsversuch erkannt!"
|
58 |
},
|
59 |
themes: {
|
60 |
title: "Wähle ein Thema",
|
|
|
71 |
},
|
72 |
welcome: {
|
73 |
title: "Think in Sync",
|
74 |
+
subtitle: "Arbeite mit einer KI zusammen, um einen Hinweis zu erstellen, und lass eine andere KI dein geheimes Wort erraten!",
|
75 |
startButton: "Spiel starten",
|
76 |
howToPlay: "Spielanleitung",
|
77 |
leaderboard: "Bestenliste",
|
78 |
credits: "Erstellt während des",
|
79 |
+
helpWin: "Hilf uns zu gewinnen",
|
80 |
onHuggingface: "mit einem Like auf Huggingface"
|
81 |
},
|
82 |
howToPlay: {
|
|
|
98 |
]
|
99 |
}
|
100 |
}
|
101 |
+
};
|
|
src/i18n/translations/en.ts
CHANGED
@@ -12,89 +12,90 @@ export const en = {
|
|
12 |
invalidWord: "Invalid Word",
|
13 |
cantUseTargetWord: "You can't use the target word",
|
14 |
lettersOnly: "Please use letters only",
|
|
|
15 |
leaveGameTitle: "Leave Game?",
|
16 |
leaveGameDescription: "Your current progress will be lost. Are you sure you want to leave?",
|
17 |
cancel: "Cancel",
|
18 |
confirm: "Confirm",
|
19 |
describeWord: "Your goal is to describe the word"
|
20 |
},
|
21 |
-
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
|
26 |
-
|
27 |
-
|
28 |
-
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
-
|
33 |
-
|
34 |
-
|
35 |
-
|
36 |
-
|
37 |
-
|
38 |
-
|
39 |
-
|
40 |
-
|
41 |
-
|
42 |
-
|
43 |
-
|
44 |
-
|
45 |
-
|
46 |
-
|
47 |
-
|
48 |
-
|
49 |
-
|
50 |
-
|
51 |
-
|
52 |
-
|
53 |
-
|
54 |
-
|
55 |
-
|
56 |
-
|
57 |
-
|
58 |
-
|
59 |
-
|
60 |
-
|
61 |
-
|
62 |
-
|
63 |
-
|
64 |
-
|
65 |
-
|
66 |
-
|
67 |
-
|
68 |
-
|
69 |
-
|
70 |
-
welcome: {
|
71 |
-
title: "Think in Sync",
|
72 |
-
subtitle: "Build sentences together and let AI guess your word!",
|
73 |
-
startButton: "Start Game",
|
74 |
-
howToPlay: "How to Play",
|
75 |
-
leaderboard: "Leaderboard",
|
76 |
-
credits: "Created during the",
|
77 |
-
helpWin: "Help us win by",
|
78 |
-
onHuggingface: "Liking on Huggingface"
|
79 |
-
},
|
80 |
-
howToPlay: {
|
81 |
-
setup: {
|
82 |
-
title: "Setup",
|
83 |
-
description: "Choose a theme and get a secret word that the AI will try to guess."
|
84 |
},
|
85 |
-
|
86 |
-
title: "
|
87 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
88 |
},
|
89 |
-
|
90 |
-
|
91 |
-
|
92 |
-
"
|
93 |
-
|
94 |
-
|
95 |
-
|
96 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
97 |
}
|
98 |
-
}
|
99 |
};
|
100 |
-
|
|
|
12 |
invalidWord: "Invalid Word",
|
13 |
cantUseTargetWord: "You can't use the target word",
|
14 |
lettersOnly: "Please use letters only",
|
15 |
+
singleWordOnly: "Please enter only one word",
|
16 |
leaveGameTitle: "Leave Game?",
|
17 |
leaveGameDescription: "Your current progress will be lost. Are you sure you want to leave?",
|
18 |
cancel: "Cancel",
|
19 |
confirm: "Confirm",
|
20 |
describeWord: "Your goal is to describe the word"
|
21 |
},
|
22 |
+
leaderboard: {
|
23 |
+
title: "High Scores",
|
24 |
+
yourScore: "Your Score",
|
25 |
+
roundCount: "rounds",
|
26 |
+
wordsPerRound: "words per round",
|
27 |
+
enterName: "Enter your name",
|
28 |
+
submitting: "Submitting...",
|
29 |
+
submit: "Submit Score",
|
30 |
+
rank: "Rank",
|
31 |
+
player: "Player",
|
32 |
+
roundsColumn: "Rounds",
|
33 |
+
avgWords: "Avg. Words",
|
34 |
+
noScores: "No scores yet",
|
35 |
+
previous: "Previous",
|
36 |
+
next: "Next",
|
37 |
+
error: {
|
38 |
+
invalidName: "Please enter a valid name",
|
39 |
+
noRounds: "You need to complete at least one round",
|
40 |
+
alreadySubmitted: "Score already submitted",
|
41 |
+
newHighScore: "New High Score!",
|
42 |
+
beatRecord: "You beat your previous record of {score}!",
|
43 |
+
notHigher: "Score of {current} not higher than your best of {best}",
|
44 |
+
submitError: "Error submitting score"
|
45 |
+
}
|
46 |
+
},
|
47 |
+
guess: {
|
48 |
+
title: "AI's Guess",
|
49 |
+
goalDescription: "Your goal was to describe the word",
|
50 |
+
providedDescription: "You provided the description",
|
51 |
+
aiGuessedDescription: "Based on your description, the AI guessed",
|
52 |
+
correct: "This is right!",
|
53 |
+
incorrect: "This is wrong.",
|
54 |
+
nextRound: "Next Round",
|
55 |
+
playAgain: "Play Again",
|
56 |
+
viewLeaderboard: "Save your score",
|
57 |
+
cheatingDetected: "Cheating detected!"
|
58 |
+
},
|
59 |
+
themes: {
|
60 |
+
title: "Choose a Theme",
|
61 |
+
subtitle: "Select a theme for the word the AI will try to guess",
|
62 |
+
standard: "Standard",
|
63 |
+
technology: "Technology",
|
64 |
+
sports: "Sports",
|
65 |
+
food: "Food",
|
66 |
+
custom: "Custom Theme",
|
67 |
+
customPlaceholder: "Enter your custom theme...",
|
68 |
+
continue: "Continue",
|
69 |
+
generating: "Generating...",
|
70 |
+
pressKey: "Press"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
71 |
},
|
72 |
+
welcome: {
|
73 |
+
title: "Think in Sync",
|
74 |
+
subtitle: "Team up with AI to craft a clue and have a different AI guess your secret word!",
|
75 |
+
startButton: "Start Game",
|
76 |
+
howToPlay: "How to Play",
|
77 |
+
leaderboard: "Leaderboard",
|
78 |
+
credits: "Created during the",
|
79 |
+
helpWin: "Help us win by",
|
80 |
+
onHuggingface: "Liking on Huggingface"
|
81 |
},
|
82 |
+
howToPlay: {
|
83 |
+
setup: {
|
84 |
+
title: "Setup",
|
85 |
+
description: "Choose a theme and get a secret word that the AI will try to guess."
|
86 |
+
},
|
87 |
+
goal: {
|
88 |
+
title: "Goal",
|
89 |
+
description: "Build sentences together with the AI that describe your word without using it directly."
|
90 |
+
},
|
91 |
+
rules: {
|
92 |
+
title: "Rules",
|
93 |
+
items: [
|
94 |
+
"Take turns adding words to build descriptive sentences",
|
95 |
+
"Don't use the secret word or its variations",
|
96 |
+
"Try to be creative and descriptive",
|
97 |
+
"The AI will try to guess your word after each sentence"
|
98 |
+
]
|
99 |
+
}
|
100 |
}
|
|
|
101 |
};
|
|
src/i18n/translations/es.ts
CHANGED
@@ -12,88 +12,90 @@ export const es = {
|
|
12 |
invalidWord: "Palabra inválida",
|
13 |
cantUseTargetWord: "No puedes usar la palabra objetivo",
|
14 |
lettersOnly: "Por favor, usa solo letras",
|
|
|
15 |
leaveGameTitle: "¿Salir del juego?",
|
16 |
leaveGameDescription: "Tu progreso actual se perderá. ¿Estás seguro de que quieres salir?",
|
17 |
cancel: "Cancelar",
|
18 |
confirm: "Confirmar",
|
19 |
describeWord: "Tu objetivo es describir la palabra"
|
20 |
},
|
21 |
-
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
|
26 |
-
|
27 |
-
|
28 |
-
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
-
|
33 |
-
|
34 |
-
|
35 |
-
|
36 |
-
|
37 |
-
|
38 |
-
|
39 |
-
|
40 |
-
|
41 |
-
|
42 |
-
|
43 |
-
|
44 |
-
|
45 |
-
|
46 |
-
|
47 |
-
|
48 |
-
|
49 |
-
|
50 |
-
|
51 |
-
|
52 |
-
|
53 |
-
|
54 |
-
|
55 |
-
|
56 |
-
|
57 |
-
|
58 |
-
|
59 |
-
|
60 |
-
|
61 |
-
|
62 |
-
|
63 |
-
|
64 |
-
|
65 |
-
|
66 |
-
|
67 |
-
|
68 |
-
|
69 |
-
|
70 |
-
welcome: {
|
71 |
-
title: "Think in Sync",
|
72 |
-
subtitle: "¡Construye frases juntos y deja que la IA adivine tu palabra!",
|
73 |
-
startButton: "Comenzar Juego",
|
74 |
-
howToPlay: "Cómo Jugar",
|
75 |
-
leaderboard: "Clasificación",
|
76 |
-
credits: "Creado durante el",
|
77 |
-
helpWin: "Ayúdanos a ganar",
|
78 |
-
onHuggingface: "Dando me gusta en Huggingface"
|
79 |
-
},
|
80 |
-
howToPlay: {
|
81 |
-
setup: {
|
82 |
-
title: "Preparación",
|
83 |
-
description: "Elige un tema y obtén una palabra secreta que la IA intentará adivinar."
|
84 |
},
|
85 |
-
|
86 |
-
title: "
|
87 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
88 |
},
|
89 |
-
|
90 |
-
|
91 |
-
|
92 |
-
"
|
93 |
-
|
94 |
-
|
95 |
-
|
96 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
97 |
}
|
98 |
-
}
|
99 |
};
|
|
|
12 |
invalidWord: "Palabra inválida",
|
13 |
cantUseTargetWord: "No puedes usar la palabra objetivo",
|
14 |
lettersOnly: "Por favor, usa solo letras",
|
15 |
+
singleWordOnly: "Por favor, ingresa solo una palabra",
|
16 |
leaveGameTitle: "¿Salir del juego?",
|
17 |
leaveGameDescription: "Tu progreso actual se perderá. ¿Estás seguro de que quieres salir?",
|
18 |
cancel: "Cancelar",
|
19 |
confirm: "Confirmar",
|
20 |
describeWord: "Tu objetivo es describir la palabra"
|
21 |
},
|
22 |
+
leaderboard: {
|
23 |
+
title: "Puntuaciones Más Altas",
|
24 |
+
yourScore: "Tu Puntuación",
|
25 |
+
roundCount: "rondas",
|
26 |
+
wordsPerRound: "palabras por ronda",
|
27 |
+
enterName: "Ingresa tu nombre",
|
28 |
+
submitting: "Enviando...",
|
29 |
+
submit: "Enviar Puntuación",
|
30 |
+
rank: "Posición",
|
31 |
+
player: "Jugador",
|
32 |
+
roundsColumn: "Rondas",
|
33 |
+
avgWords: "Prom. Palabras",
|
34 |
+
noScores: "Aún no hay puntuaciones",
|
35 |
+
previous: "Anterior",
|
36 |
+
next: "Siguiente",
|
37 |
+
error: {
|
38 |
+
invalidName: "Por favor, ingresa un nombre válido",
|
39 |
+
noRounds: "Debes completar al menos una ronda",
|
40 |
+
alreadySubmitted: "Puntuación ya enviada",
|
41 |
+
newHighScore: "¡Nueva Puntuación Más Alta!",
|
42 |
+
beatRecord: "¡Has superado tu récord anterior de {score}!",
|
43 |
+
notHigher: "Puntuación de {current} no superior a tu mejor de {best}",
|
44 |
+
submitError: "Error al enviar la puntuación"
|
45 |
+
}
|
46 |
+
},
|
47 |
+
guess: {
|
48 |
+
title: "Suposición de la IA",
|
49 |
+
goalDescription: "Tu objetivo era describir la palabra",
|
50 |
+
providedDescription: "Proporcionaste la descripción",
|
51 |
+
aiGuessedDescription: "Basado en tu descripción, la IA adivinó",
|
52 |
+
correct: "¡Esto es correcto!",
|
53 |
+
incorrect: "Esto es incorrecto.",
|
54 |
+
nextRound: "Siguiente Ronda",
|
55 |
+
playAgain: "Jugar de Nuevo",
|
56 |
+
viewLeaderboard: "Ver Clasificación",
|
57 |
+
cheatingDetected: "¡Trampa detectada!"
|
58 |
+
},
|
59 |
+
themes: {
|
60 |
+
title: "Elige un Tema",
|
61 |
+
subtitle: "Selecciona un tema para la palabra que la IA intentará adivinar",
|
62 |
+
standard: "Estándar",
|
63 |
+
technology: "Tecnología",
|
64 |
+
sports: "Deportes",
|
65 |
+
food: "Comida",
|
66 |
+
custom: "Tema Personalizado",
|
67 |
+
customPlaceholder: "Ingresa tu tema personalizado...",
|
68 |
+
continue: "Continuar",
|
69 |
+
generating: "Generando...",
|
70 |
+
pressKey: "Presiona"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
71 |
},
|
72 |
+
welcome: {
|
73 |
+
title: "Think in Sync",
|
74 |
+
subtitle: "¡Haz equipo con una IA para crear una pista y deja que otra IA adivine tu palabra secreta!",
|
75 |
+
startButton: "Comenzar Juego",
|
76 |
+
howToPlay: "Cómo Jugar",
|
77 |
+
leaderboard: "Clasificación",
|
78 |
+
credits: "Creado durante el",
|
79 |
+
helpWin: "Ayúdanos a ganar",
|
80 |
+
onHuggingface: "Dando me gusta en Huggingface"
|
81 |
},
|
82 |
+
howToPlay: {
|
83 |
+
setup: {
|
84 |
+
title: "Preparación",
|
85 |
+
description: "Elige un tema y obtén una palabra secreta que la IA intentará adivinar."
|
86 |
+
},
|
87 |
+
goal: {
|
88 |
+
title: "Objetivo",
|
89 |
+
description: "Construye frases junto con la IA que describan tu palabra sin usarla directamente."
|
90 |
+
},
|
91 |
+
rules: {
|
92 |
+
title: "Reglas",
|
93 |
+
items: [
|
94 |
+
"Añade palabras por turnos para construir frases descriptivas",
|
95 |
+
"No uses la palabra secreta o sus variaciones",
|
96 |
+
"Sé creativo y descriptivo",
|
97 |
+
"La IA intentará adivinar tu palabra después de cada frase"
|
98 |
+
]
|
99 |
+
}
|
100 |
}
|
|
|
101 |
};
|
src/i18n/translations/fr.ts
CHANGED
@@ -11,6 +11,7 @@ export const fr = {
|
|
11 |
invalidWord: "Mot invalide",
|
12 |
cantUseTargetWord: "Vous ne pouvez pas utiliser le mot cible",
|
13 |
lettersOnly: "Veuillez utiliser uniquement des lettres",
|
|
|
14 |
leaveGameTitle: "Quitter le jeu ?",
|
15 |
leaveGameDescription: "Votre progression actuelle sera perdue. Êtes-vous sûr de vouloir quitter ?",
|
16 |
cancel: "Annuler",
|
@@ -51,7 +52,8 @@ export const fr = {
|
|
51 |
incorrect: "C'est incorrect.",
|
52 |
nextRound: "Tour Suivant",
|
53 |
playAgain: "Rejouer",
|
54 |
-
viewLeaderboard: "Voir les Scores"
|
|
|
55 |
},
|
56 |
themes: {
|
57 |
title: "Choisissez un Thème",
|
@@ -68,7 +70,7 @@ export const fr = {
|
|
68 |
},
|
69 |
welcome: {
|
70 |
title: "Think in Sync",
|
71 |
-
subtitle: "
|
72 |
startButton: "Commencer",
|
73 |
howToPlay: "Comment Jouer",
|
74 |
leaderboard: "Classement",
|
@@ -95,4 +97,4 @@ export const fr = {
|
|
95 |
]
|
96 |
}
|
97 |
}
|
98 |
-
};
|
|
|
11 |
invalidWord: "Mot invalide",
|
12 |
cantUseTargetWord: "Vous ne pouvez pas utiliser le mot cible",
|
13 |
lettersOnly: "Veuillez utiliser uniquement des lettres",
|
14 |
+
singleWordOnly: "Veuillez entrer un seul mot",
|
15 |
leaveGameTitle: "Quitter le jeu ?",
|
16 |
leaveGameDescription: "Votre progression actuelle sera perdue. Êtes-vous sûr de vouloir quitter ?",
|
17 |
cancel: "Annuler",
|
|
|
52 |
incorrect: "C'est incorrect.",
|
53 |
nextRound: "Tour Suivant",
|
54 |
playAgain: "Rejouer",
|
55 |
+
viewLeaderboard: "Voir les Scores",
|
56 |
+
cheatingDetected: "Tentative de triche détectée !"
|
57 |
},
|
58 |
themes: {
|
59 |
title: "Choisissez un Thème",
|
|
|
70 |
},
|
71 |
welcome: {
|
72 |
title: "Think in Sync",
|
73 |
+
subtitle: "Faites équipe avec une IA pour créer un indice et laissez une autre IA deviner votre mot secret",
|
74 |
startButton: "Commencer",
|
75 |
howToPlay: "Comment Jouer",
|
76 |
leaderboard: "Classement",
|
|
|
97 |
]
|
98 |
}
|
99 |
}
|
100 |
+
};
|
src/i18n/translations/it.ts
CHANGED
@@ -12,6 +12,7 @@ export const it = {
|
|
12 |
invalidWord: "Parola non valida",
|
13 |
cantUseTargetWord: "Non puoi usare la parola obiettivo",
|
14 |
lettersOnly: "Usa solo lettere",
|
|
|
15 |
leaveGameTitle: "Lasciare il gioco?",
|
16 |
leaveGameDescription: "I tuoi progressi attuali andranno persi. Sei sicuro di voler uscire?",
|
17 |
cancel: "Annulla",
|
@@ -54,7 +55,8 @@ export const it = {
|
|
54 |
incorrect: "Sbagliato. Riprova!",
|
55 |
nextRound: "Prossimo Turno",
|
56 |
playAgain: "Gioca Ancora",
|
57 |
-
viewLeaderboard: "Vedi Classifica"
|
|
|
58 |
},
|
59 |
themes: {
|
60 |
title: "Scegli un Tema",
|
@@ -71,7 +73,7 @@ export const it = {
|
|
71 |
},
|
72 |
welcome: {
|
73 |
title: "Think in Sync",
|
74 |
-
subtitle: "
|
75 |
startButton: "Inizia Gioco",
|
76 |
howToPlay: "Come Giocare",
|
77 |
leaderboard: "Classifica",
|
@@ -98,4 +100,4 @@ export const it = {
|
|
98 |
]
|
99 |
}
|
100 |
}
|
101 |
-
};
|
|
|
12 |
invalidWord: "Parola non valida",
|
13 |
cantUseTargetWord: "Non puoi usare la parola obiettivo",
|
14 |
lettersOnly: "Usa solo lettere",
|
15 |
+
singleWordOnly: "Inserisci una sola parola",
|
16 |
leaveGameTitle: "Lasciare il gioco?",
|
17 |
leaveGameDescription: "I tuoi progressi attuali andranno persi. Sei sicuro di voler uscire?",
|
18 |
cancel: "Annulla",
|
|
|
55 |
incorrect: "Sbagliato. Riprova!",
|
56 |
nextRound: "Prossimo Turno",
|
57 |
playAgain: "Gioca Ancora",
|
58 |
+
viewLeaderboard: "Vedi Classifica",
|
59 |
+
cheatingDetected: "Tentativo di imbroglio rilevato!"
|
60 |
},
|
61 |
themes: {
|
62 |
title: "Scegli un Tema",
|
|
|
73 |
},
|
74 |
welcome: {
|
75 |
title: "Think in Sync",
|
76 |
+
subtitle: "Collabora con un'IA per creare un indizio e lascia che un'altra IA indovini la tua parola segreta!",
|
77 |
startButton: "Inizia Gioco",
|
78 |
howToPlay: "Come Giocare",
|
79 |
leaderboard: "Classifica",
|
|
|
100 |
]
|
101 |
}
|
102 |
}
|
103 |
+
};
|
src/integrations/supabase/types.ts
CHANGED
@@ -9,6 +9,33 @@ export type Json =
|
|
9 |
export type Database = {
|
10 |
public: {
|
11 |
Tables: {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
12 |
high_scores: {
|
13 |
Row: {
|
14 |
avg_words_per_round: number
|
|
|
9 |
export type Database = {
|
10 |
public: {
|
11 |
Tables: {
|
12 |
+
game_results: {
|
13 |
+
Row: {
|
14 |
+
ai_guess: string
|
15 |
+
created_at: string
|
16 |
+
description: string
|
17 |
+
id: string
|
18 |
+
is_correct: boolean
|
19 |
+
target_word: string
|
20 |
+
}
|
21 |
+
Insert: {
|
22 |
+
ai_guess: string
|
23 |
+
created_at?: string
|
24 |
+
description: string
|
25 |
+
id?: string
|
26 |
+
is_correct: boolean
|
27 |
+
target_word: string
|
28 |
+
}
|
29 |
+
Update: {
|
30 |
+
ai_guess?: string
|
31 |
+
created_at?: string
|
32 |
+
description?: string
|
33 |
+
id?: string
|
34 |
+
is_correct?: boolean
|
35 |
+
target_word?: string
|
36 |
+
}
|
37 |
+
Relationships: []
|
38 |
+
}
|
39 |
high_scores: {
|
40 |
Row: {
|
41 |
avg_words_per_round: number
|
src/lib/words.ts
CHANGED
@@ -1,104 +1,106 @@
|
|
1 |
export const words = [
|
2 |
-
"
|
3 |
-
"
|
4 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
5 |
"BUTTERFLY",
|
6 |
-
"
|
7 |
"OCEAN",
|
8 |
-
"
|
9 |
-
"
|
10 |
-
"BREEZE",
|
11 |
-
"DIAMOND",
|
12 |
-
"STARDUST",
|
13 |
-
"MEADOW",
|
14 |
-
"FLOWER",
|
15 |
-
"HORIZON",
|
16 |
-
"JOURNEY",
|
17 |
-
"FEATHER",
|
18 |
-
"TWILIGHT",
|
19 |
-
"CANYON",
|
20 |
-
"WONDER",
|
21 |
"FOREST",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
22 |
"THUNDER",
|
23 |
-
"
|
24 |
-
"
|
25 |
-
"
|
26 |
-
"
|
27 |
-
"
|
28 |
-
"
|
29 |
-
"
|
30 |
-
"
|
31 |
-
"
|
32 |
-
"
|
33 |
-
"
|
34 |
-
"
|
35 |
-
"
|
36 |
-
"
|
37 |
-
"
|
38 |
-
"
|
39 |
-
"
|
|
|
|
|
|
|
|
|
|
|
40 |
"DREAM",
|
41 |
-
"
|
42 |
-
"
|
43 |
-
"
|
|
|
44 |
"WISH",
|
45 |
-
"
|
46 |
-
"MOONLIGHT",
|
47 |
-
"HEARTBEAT",
|
48 |
-
"WANDERLUST",
|
49 |
-
"STARLIGHT",
|
50 |
-
"CRESCENT",
|
51 |
-
"WATERFALL",
|
52 |
-
"CITADEL",
|
53 |
-
"AURORA",
|
54 |
-
"BLISS",
|
55 |
-
"CASCADE",
|
56 |
-
"DAWN",
|
57 |
-
"ECLIPSE",
|
58 |
-
"FIRELIGHT",
|
59 |
-
"GARDEN",
|
60 |
-
"HAVEN",
|
61 |
-
"INFINITY",
|
62 |
-
"JUBILEE",
|
63 |
-
"KALEIDOSCOPE",
|
64 |
-
"LULLABY",
|
65 |
-
"MARVEL",
|
66 |
-
"NEBULA",
|
67 |
-
"OASIS",
|
68 |
-
"PARADISE",
|
69 |
-
"QUIETUDE",
|
70 |
-
"RADIANCE",
|
71 |
-
"SANCTUARY",
|
72 |
-
"TRANQUILITY",
|
73 |
-
"UNIVERSE",
|
74 |
-
"VELVET",
|
75 |
-
"WONDERLAND",
|
76 |
-
"YIELD",
|
77 |
-
"ABYSS",
|
78 |
-
"BALANCE",
|
79 |
-
"CALM",
|
80 |
-
"DAZZLE",
|
81 |
-
"EMBRACE",
|
82 |
-
"FLICKER",
|
83 |
-
"GLIMMER",
|
84 |
-
"HALO",
|
85 |
-
"ILLUMINATE",
|
86 |
-
"JEWEL",
|
87 |
-
"KINDLE",
|
88 |
-
"LUMINOUS",
|
89 |
-
"MYSTIC",
|
90 |
-
"NIRVANA",
|
91 |
-
"OPULENCE",
|
92 |
"PEACE",
|
93 |
-
"
|
94 |
-
"
|
95 |
-
"
|
96 |
-
"
|
97 |
-
"
|
98 |
-
"
|
99 |
-
"
|
100 |
-
"
|
101 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
102 |
];
|
103 |
|
104 |
export const getRandomWord = () => {
|
|
|
1 |
export const words = [
|
2 |
+
"DOG",
|
3 |
+
"CAT",
|
4 |
+
"SUN",
|
5 |
+
"RAIN",
|
6 |
+
"TREE",
|
7 |
+
"STAR",
|
8 |
+
"MOON",
|
9 |
+
"FISH",
|
10 |
+
"BIRD",
|
11 |
+
"CLOUD",
|
12 |
+
"SKY",
|
13 |
+
"WIND",
|
14 |
+
"SNOW",
|
15 |
+
"FLOWER",
|
16 |
"BUTTERFLY",
|
17 |
+
"WATER",
|
18 |
"OCEAN",
|
19 |
+
"RIVER",
|
20 |
+
"MOUNTAIN",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
21 |
"FOREST",
|
22 |
+
"HOUSE",
|
23 |
+
"CANDLE",
|
24 |
+
"GARDEN",
|
25 |
+
"BRIDGE",
|
26 |
+
"ISLAND",
|
27 |
+
"BREEZE",
|
28 |
+
"LIGHT",
|
29 |
"THUNDER",
|
30 |
+
"RAINBOW",
|
31 |
+
"SMILE",
|
32 |
+
"FRIEND",
|
33 |
+
"FAMILY",
|
34 |
+
"APPLE",
|
35 |
+
"BANANA",
|
36 |
+
"CAR",
|
37 |
+
"BOAT",
|
38 |
+
"BALL",
|
39 |
+
"CAKE",
|
40 |
+
"FROG",
|
41 |
+
"HORSE",
|
42 |
+
"LION",
|
43 |
+
"MONKEY",
|
44 |
+
"PANDA",
|
45 |
+
"PLANE",
|
46 |
+
"TRAIN",
|
47 |
+
"CANDY",
|
48 |
+
"JUMP",
|
49 |
+
"PLAY",
|
50 |
+
"SLEEP",
|
51 |
+
"LAUGH",
|
52 |
"DREAM",
|
53 |
+
"HAPPY",
|
54 |
+
"FUN",
|
55 |
+
"COLOR",
|
56 |
+
"BRIGHT",
|
57 |
"WISH",
|
58 |
+
"LOVE",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
59 |
"PEACE",
|
60 |
+
"HUG",
|
61 |
+
"KISS",
|
62 |
+
"ZOO",
|
63 |
+
"PARK",
|
64 |
+
"BEACH",
|
65 |
+
"TOY",
|
66 |
+
"BOOK",
|
67 |
+
"BUBBLE",
|
68 |
+
"SHELL",
|
69 |
+
"PEN",
|
70 |
+
"ICE",
|
71 |
+
"CAKE",
|
72 |
+
"HAT",
|
73 |
+
"SHOE",
|
74 |
+
"CLOCK",
|
75 |
+
"BED",
|
76 |
+
"CUP",
|
77 |
+
"KEY",
|
78 |
+
"DOOR",
|
79 |
+
"CHICKEN",
|
80 |
+
"DUCK",
|
81 |
+
"SHEEP",
|
82 |
+
"COW",
|
83 |
+
"PIG",
|
84 |
+
"GOAT",
|
85 |
+
"FOX",
|
86 |
+
"BEAR",
|
87 |
+
"DEER",
|
88 |
+
"OWL",
|
89 |
+
"EGG",
|
90 |
+
"NEST",
|
91 |
+
"ROCK",
|
92 |
+
"LEAF",
|
93 |
+
"BRUSH",
|
94 |
+
"TOOTH",
|
95 |
+
"HAND",
|
96 |
+
"FEET",
|
97 |
+
"EYE",
|
98 |
+
"NOSE",
|
99 |
+
"EAR",
|
100 |
+
"MOUTH",
|
101 |
+
"CHILD",
|
102 |
+
"KITE",
|
103 |
+
"BALLON"
|
104 |
];
|
105 |
|
106 |
export const getRandomWord = () => {
|
src/services/mistralService.ts
CHANGED
@@ -29,6 +29,34 @@ export const generateAIResponse = async (currentWord: string, currentSentence: s
|
|
29 |
};
|
30 |
|
31 |
export const guessWord = async (sentence: string, language: string): Promise<string> => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
32 |
console.log('Calling guess-word function with sentence:', sentence, 'language:', language);
|
33 |
|
34 |
const { data, error } = await supabase.functions.invoke('guess-word', {
|
|
|
29 |
};
|
30 |
|
31 |
export const guessWord = async (sentence: string, language: string): Promise<string> => {
|
32 |
+
console.log('Processing guess for sentence:', sentence);
|
33 |
+
|
34 |
+
// Check for potential fraud if the sentence has less than 3 words
|
35 |
+
const words = sentence.trim().split(/\s+/);
|
36 |
+
if (words.length < 3) {
|
37 |
+
console.log('Short description detected, checking for fraud...');
|
38 |
+
|
39 |
+
try {
|
40 |
+
const { data: fraudData, error: fraudError } = await supabase.functions.invoke('detect-fraud', {
|
41 |
+
body: {
|
42 |
+
sentence,
|
43 |
+
targetWord: words[0], // First word is usually the target in cheating attempts
|
44 |
+
language
|
45 |
+
}
|
46 |
+
});
|
47 |
+
|
48 |
+
if (fraudError) throw fraudError;
|
49 |
+
|
50 |
+
if (fraudData?.verdict === 'cheating') {
|
51 |
+
console.log('Fraud detected!');
|
52 |
+
return 'CHEATING';
|
53 |
+
}
|
54 |
+
} catch (error) {
|
55 |
+
console.error('Error in fraud detection:', error);
|
56 |
+
// Continue with normal guessing if fraud detection fails
|
57 |
+
}
|
58 |
+
}
|
59 |
+
|
60 |
console.log('Calling guess-word function with sentence:', sentence, 'language:', language);
|
61 |
|
62 |
const { data, error } = await supabase.functions.invoke('guess-word', {
|
supabase/functions/detect-fraud/index.ts
ADDED
@@ -0,0 +1,127 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { serve } from "https://deno.land/[email protected]/http/server.ts";
|
2 |
+
import { Mistral } from "npm:@mistralai/mistralai";
|
3 |
+
|
4 |
+
const corsHeaders = {
|
5 |
+
'Access-Control-Allow-Origin': '*',
|
6 |
+
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
|
7 |
+
};
|
8 |
+
|
9 |
+
serve(async (req) => {
|
10 |
+
// Handle CORS preflight requests
|
11 |
+
if (req.method === 'OPTIONS') {
|
12 |
+
return new Response(null, { headers: corsHeaders });
|
13 |
+
}
|
14 |
+
|
15 |
+
try {
|
16 |
+
const { sentence, targetWord, language } = await req.json();
|
17 |
+
console.log('Checking for fraud:', { sentence, targetWord, language });
|
18 |
+
|
19 |
+
const client = new Mistral({
|
20 |
+
apiKey: Deno.env.get('MISTRAL_API_KEY'),
|
21 |
+
});
|
22 |
+
|
23 |
+
const maxRetries = 3;
|
24 |
+
let retryCount = 0;
|
25 |
+
let lastError = null;
|
26 |
+
|
27 |
+
while (retryCount < maxRetries) {
|
28 |
+
try {
|
29 |
+
const response = await client.chat.complete({
|
30 |
+
model: "mistral-large-latest",
|
31 |
+
messages: [
|
32 |
+
{
|
33 |
+
role: "system",
|
34 |
+
content: `You are a fraud detection system for a word guessing game.
|
35 |
+
The game is being played in ${language}.
|
36 |
+
Your task is to detect if a player is trying to cheat by:
|
37 |
+
1. Using a misspelling of the target word
|
38 |
+
2. Writing a sentence without spaces to bypass word count checks
|
39 |
+
|
40 |
+
Examples for cheating:
|
41 |
+
|
42 |
+
Target word: hand
|
43 |
+
Player's description: hnd
|
44 |
+
Language: en
|
45 |
+
CORRECT ANSWER: cheating
|
46 |
+
|
47 |
+
Target word: barfuß
|
48 |
+
Player's description: germanwordforbarefoot
|
49 |
+
Language: de
|
50 |
+
CORRECT ANSWER: cheating
|
51 |
+
|
52 |
+
Synonyms and names of instances of a class are legitimate descriptions.
|
53 |
+
|
54 |
+
Target word: laptop
|
55 |
+
Player's description: notebook
|
56 |
+
Language: en
|
57 |
+
CORRECT ANSWER: legitimate
|
58 |
+
|
59 |
+
Target word: play
|
60 |
+
Player's description: children often
|
61 |
+
Language: en
|
62 |
+
CORRECT ANSWER: legitimate
|
63 |
+
|
64 |
+
Target word: Pfankuchen
|
65 |
+
Player's description: Berliner
|
66 |
+
Language: de
|
67 |
+
CORRECT ANSWER: legitimate
|
68 |
+
|
69 |
+
Respond with ONLY "cheating" or "legitimate" (no punctuation or explanation).`
|
70 |
+
},
|
71 |
+
{
|
72 |
+
role: "user",
|
73 |
+
content: `Target word: "${targetWord}"
|
74 |
+
Player's description: "${sentence}"
|
75 |
+
Language: ${language}
|
76 |
+
|
77 |
+
Is this a legitimate description or an attempt to cheat?`
|
78 |
+
}
|
79 |
+
],
|
80 |
+
maxTokens: 20,
|
81 |
+
temperature: 0.1
|
82 |
+
});
|
83 |
+
|
84 |
+
const verdict = response.choices[0].message.content.trim().toLowerCase();
|
85 |
+
console.log('Fraud detection verdict:', verdict);
|
86 |
+
|
87 |
+
return new Response(
|
88 |
+
JSON.stringify({ verdict }),
|
89 |
+
{ headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
90 |
+
);
|
91 |
+
} catch (error) {
|
92 |
+
console.error(`Attempt ${retryCount + 1} failed:`, error);
|
93 |
+
lastError = error;
|
94 |
+
|
95 |
+
if (error.message?.includes('rate limit') || error.status === 429) {
|
96 |
+
const waitTime = Math.pow(2, retryCount) * 1000;
|
97 |
+
console.log(`Rate limit hit, waiting ${waitTime}ms before retry`);
|
98 |
+
await new Promise(resolve => setTimeout(resolve, waitTime));
|
99 |
+
retryCount++;
|
100 |
+
continue;
|
101 |
+
}
|
102 |
+
|
103 |
+
throw error;
|
104 |
+
}
|
105 |
+
}
|
106 |
+
|
107 |
+
throw new Error(`Failed after ${maxRetries} attempts. Last error: ${lastError?.message}`);
|
108 |
+
|
109 |
+
} catch (error) {
|
110 |
+
console.error('Error in fraud detection:', error);
|
111 |
+
|
112 |
+
const errorMessage = error.message?.includes('rate limit')
|
113 |
+
? "The AI service is currently busy. Please try again in a few moments."
|
114 |
+
: "Sorry, there was an error checking for fraud. Please try again.";
|
115 |
+
|
116 |
+
return new Response(
|
117 |
+
JSON.stringify({
|
118 |
+
error: errorMessage,
|
119 |
+
details: error.message
|
120 |
+
}),
|
121 |
+
{
|
122 |
+
status: error.message?.includes('rate limit') ? 429 : 500,
|
123 |
+
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
|
124 |
+
}
|
125 |
+
);
|
126 |
+
}
|
127 |
+
});
|
supabase/functions/guess-word/index.ts
CHANGED
@@ -8,28 +8,29 @@ const corsHeaders = {
|
|
8 |
|
9 |
const languagePrompts = {
|
10 |
en: {
|
11 |
-
systemPrompt: "You are
|
12 |
-
instruction: "Based on this description
|
13 |
},
|
14 |
fr: {
|
15 |
-
systemPrompt: "Vous
|
16 |
-
instruction: "D'après cette description
|
17 |
},
|
18 |
de: {
|
19 |
-
systemPrompt: "Sie
|
20 |
-
instruction: "
|
21 |
},
|
22 |
it: {
|
23 |
-
systemPrompt: "Stai
|
24 |
-
instruction: "
|
25 |
},
|
26 |
es: {
|
27 |
-
systemPrompt: "Estás
|
28 |
-
instruction: "
|
29 |
}
|
30 |
};
|
31 |
|
32 |
serve(async (req) => {
|
|
|
33 |
if (req.method === 'OPTIONS') {
|
34 |
return new Response(null, { headers: corsHeaders });
|
35 |
}
|
|
|
8 |
|
9 |
const languagePrompts = {
|
10 |
en: {
|
11 |
+
systemPrompt: "You are helping in a word guessing game. Given a description, guess what single word is being described.",
|
12 |
+
instruction: "Based on this description"
|
13 |
},
|
14 |
fr: {
|
15 |
+
systemPrompt: "Vous aidez dans un jeu de devinettes. À partir d'une description, devinez le mot unique qui est décrit.",
|
16 |
+
instruction: "D'après cette description"
|
17 |
},
|
18 |
de: {
|
19 |
+
systemPrompt: "Sie helfen bei einem Worträtsel. Erraten Sie anhand einer Beschreibung, welches einzelne Wort beschrieben wird.",
|
20 |
+
instruction: "Basierend auf dieser Beschreibung"
|
21 |
},
|
22 |
it: {
|
23 |
+
systemPrompt: "Stai aiutando in un gioco di indovinelli. Data una descrizione, indovina quale singola parola viene descritta.",
|
24 |
+
instruction: "Basandoti su questa descrizione"
|
25 |
},
|
26 |
es: {
|
27 |
+
systemPrompt: "Estás ayudando en un juego de adivinanzas. Dada una descripción, adivina qué palabra única se está describiendo.",
|
28 |
+
instruction: "Basándote en esta descripción"
|
29 |
}
|
30 |
};
|
31 |
|
32 |
serve(async (req) => {
|
33 |
+
// Handle CORS preflight requests
|
34 |
if (req.method === 'OPTIONS') {
|
35 |
return new Response(null, { headers: corsHeaders });
|
36 |
}
|