Felix Zieger
commited on
Commit
·
47fea40
1
Parent(s):
5bd3ab2
larger models again
Browse files- src/components/GameContainer.tsx +2 -2
- src/components/HighScoreBoard.tsx +3 -0
- src/components/game/GuessDisplay.tsx +66 -14
- src/components/game/ThemeSelector.tsx +9 -29
- src/lib/words.ts +232 -3
- src/services/mistralService.ts +10 -3
- supabase/functions/detect-fraud/index.ts +52 -34
- supabase/functions/generate-themed-word/index.ts +57 -16
- supabase/functions/generate-word/index.ts +2 -2
- supabase/functions/guess-word/index.ts +1 -1
- supabase/functions/validate-sentence/index.ts +1 -1
src/components/GameContainer.tsx
CHANGED
|
@@ -78,7 +78,7 @@ export const GameContainer = () => {
|
|
| 78 |
setCurrentTheme(theme);
|
| 79 |
try {
|
| 80 |
const word = theme === "standard" ?
|
| 81 |
-
getRandomWord() :
|
| 82 |
await getThemedWord(theme, usedWords, language);
|
| 83 |
setCurrentWord(word);
|
| 84 |
setGameState("building-sentence");
|
|
@@ -158,7 +158,7 @@ export const GameContainer = () => {
|
|
| 158 |
const getNewWord = async () => {
|
| 159 |
try {
|
| 160 |
const word = currentTheme === "standard" ?
|
| 161 |
-
getRandomWord() :
|
| 162 |
await getThemedWord(currentTheme, usedWords, language);
|
| 163 |
setCurrentWord(word);
|
| 164 |
setGameState("building-sentence");
|
|
|
|
| 78 |
setCurrentTheme(theme);
|
| 79 |
try {
|
| 80 |
const word = theme === "standard" ?
|
| 81 |
+
getRandomWord(language) :
|
| 82 |
await getThemedWord(theme, usedWords, language);
|
| 83 |
setCurrentWord(word);
|
| 84 |
setGameState("building-sentence");
|
|
|
|
| 158 |
const getNewWord = async () => {
|
| 159 |
try {
|
| 160 |
const word = currentTheme === "standard" ?
|
| 161 |
+
getRandomWord(language) :
|
| 162 |
await getThemedWord(currentTheme, usedWords, language);
|
| 163 |
setCurrentWord(word);
|
| 164 |
setGameState("building-sentence");
|
src/components/HighScoreBoard.tsx
CHANGED
|
@@ -37,6 +37,7 @@ interface HighScoreBoardProps {
|
|
| 37 |
onClose: () => void;
|
| 38 |
onPlayAgain: () => void;
|
| 39 |
sessionId: string;
|
|
|
|
| 40 |
}
|
| 41 |
|
| 42 |
const ITEMS_PER_PAGE = 5;
|
|
@@ -60,6 +61,7 @@ export const HighScoreBoard = ({
|
|
| 60 |
avgWordsPerRound,
|
| 61 |
onClose,
|
| 62 |
sessionId,
|
|
|
|
| 63 |
}: HighScoreBoardProps) => {
|
| 64 |
const [playerName, setPlayerName] = useState("");
|
| 65 |
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
@@ -188,6 +190,7 @@ export const HighScoreBoard = ({
|
|
| 188 |
}
|
| 189 |
|
| 190 |
setHasSubmitted(true);
|
|
|
|
| 191 |
setPlayerName("");
|
| 192 |
} catch (error) {
|
| 193 |
console.error("Error submitting score:", error);
|
|
|
|
| 37 |
onClose: () => void;
|
| 38 |
onPlayAgain: () => void;
|
| 39 |
sessionId: string;
|
| 40 |
+
onScoreSubmitted?: () => void;
|
| 41 |
}
|
| 42 |
|
| 43 |
const ITEMS_PER_PAGE = 5;
|
|
|
|
| 61 |
avgWordsPerRound,
|
| 62 |
onClose,
|
| 63 |
sessionId,
|
| 64 |
+
onScoreSubmitted,
|
| 65 |
}: HighScoreBoardProps) => {
|
| 66 |
const [playerName, setPlayerName] = useState("");
|
| 67 |
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
|
|
| 190 |
}
|
| 191 |
|
| 192 |
setHasSubmitted(true);
|
| 193 |
+
onScoreSubmitted?.();
|
| 194 |
setPlayerName("");
|
| 195 |
} catch (error) {
|
| 196 |
console.error("Error submitting score:", error);
|
src/components/game/GuessDisplay.tsx
CHANGED
|
@@ -10,6 +10,16 @@ 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[];
|
|
@@ -37,6 +47,8 @@ export const GuessDisplay = ({
|
|
| 37 |
const isGuessCorrect = () => aiGuess.toLowerCase() === currentWord.toLowerCase();
|
| 38 |
const isCheating = () => aiGuess === 'CHEATING';
|
| 39 |
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
|
|
|
|
|
|
| 40 |
const t = useTranslation();
|
| 41 |
|
| 42 |
useEffect(() => {
|
|
@@ -48,7 +60,8 @@ export const GuessDisplay = ({
|
|
| 48 |
target_word: currentWord,
|
| 49 |
description: sentence.join(' '),
|
| 50 |
ai_guess: aiGuess,
|
| 51 |
-
is_correct: isGuessCorrect()
|
|
|
|
| 52 |
});
|
| 53 |
|
| 54 |
if (error) {
|
|
@@ -64,39 +77,60 @@ export const GuessDisplay = ({
|
|
| 64 |
saveGameResult();
|
| 65 |
}, []);
|
| 66 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 67 |
return (
|
| 68 |
<motion.div
|
| 69 |
initial={{ opacity: 0 }}
|
| 70 |
animate={{ opacity: 1 }}
|
| 71 |
className="text-center relative space-y-6"
|
| 72 |
>
|
| 73 |
-
<div className="flex items-center justify-between
|
| 74 |
<Button
|
| 75 |
variant="ghost"
|
| 76 |
size="icon"
|
| 77 |
-
onClick={
|
| 78 |
className="text-gray-600 hover:text-primary"
|
| 79 |
>
|
| 80 |
<House className="h-5 w-5" />
|
| 81 |
</Button>
|
|
|
|
| 82 |
<div className="bg-primary/10 px-3 py-1 rounded-lg">
|
| 83 |
<span className="text-sm font-medium text-primary">
|
| 84 |
{t.game.round} {currentScore + 1}
|
| 85 |
</span>
|
| 86 |
</div>
|
| 87 |
-
<div className="w-8" /> {/* Spacer for centering */}
|
| 88 |
</div>
|
| 89 |
|
| 90 |
-
<div>
|
| 91 |
-
<
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
<p className="p-4 text-2xl font-bold tracking-wider text-secondary">
|
| 97 |
-
{currentWord}
|
| 98 |
-
</p>
|
| 99 |
-
</div>
|
| 100 |
</div>
|
| 101 |
</div>
|
| 102 |
|
|
@@ -148,6 +182,7 @@ export const GuessDisplay = ({
|
|
| 148 |
onPlayAgain();
|
| 149 |
}}
|
| 150 |
sessionId={sessionId}
|
|
|
|
| 151 |
/>
|
| 152 |
</DialogContent>
|
| 153 |
</Dialog>
|
|
@@ -160,6 +195,23 @@ export const GuessDisplay = ({
|
|
| 160 |
</>
|
| 161 |
)}
|
| 162 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 163 |
</motion.div>
|
| 164 |
);
|
| 165 |
};
|
|
|
|
| 10 |
import { useTranslation } from "@/hooks/useTranslation";
|
| 11 |
import { supabase } from "@/integrations/supabase/client";
|
| 12 |
import { House } from "lucide-react";
|
| 13 |
+
import {
|
| 14 |
+
AlertDialog,
|
| 15 |
+
AlertDialogAction,
|
| 16 |
+
AlertDialogCancel,
|
| 17 |
+
AlertDialogContent,
|
| 18 |
+
AlertDialogDescription,
|
| 19 |
+
AlertDialogFooter,
|
| 20 |
+
AlertDialogHeader,
|
| 21 |
+
AlertDialogTitle,
|
| 22 |
+
} from "@/components/ui/alert-dialog";
|
| 23 |
|
| 24 |
interface GuessDisplayProps {
|
| 25 |
sentence: string[];
|
|
|
|
| 47 |
const isGuessCorrect = () => aiGuess.toLowerCase() === currentWord.toLowerCase();
|
| 48 |
const isCheating = () => aiGuess === 'CHEATING';
|
| 49 |
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
| 50 |
+
const [showConfirmDialog, setShowConfirmDialog] = useState(false);
|
| 51 |
+
const [hasSubmittedScore, setHasSubmittedScore] = useState(false);
|
| 52 |
const t = useTranslation();
|
| 53 |
|
| 54 |
useEffect(() => {
|
|
|
|
| 60 |
target_word: currentWord,
|
| 61 |
description: sentence.join(' '),
|
| 62 |
ai_guess: aiGuess,
|
| 63 |
+
is_correct: isGuessCorrect(),
|
| 64 |
+
session_id: sessionId
|
| 65 |
});
|
| 66 |
|
| 67 |
if (error) {
|
|
|
|
| 77 |
saveGameResult();
|
| 78 |
}, []);
|
| 79 |
|
| 80 |
+
const handleHomeClick = () => {
|
| 81 |
+
console.log('Home button clicked', { currentScore, hasSubmittedScore });
|
| 82 |
+
if (currentScore > 0 && !hasSubmittedScore) {
|
| 83 |
+
setShowConfirmDialog(true);
|
| 84 |
+
} else {
|
| 85 |
+
if (onBack) {
|
| 86 |
+
console.log('Navigating back to welcome screen');
|
| 87 |
+
onBack();
|
| 88 |
+
}
|
| 89 |
+
}
|
| 90 |
+
};
|
| 91 |
+
|
| 92 |
+
const handleScoreSubmitted = () => {
|
| 93 |
+
console.log('Score submitted, updating state');
|
| 94 |
+
setHasSubmittedScore(true);
|
| 95 |
+
};
|
| 96 |
+
|
| 97 |
+
const handleConfirmHome = () => {
|
| 98 |
+
console.log('Confirmed navigation to home');
|
| 99 |
+
setShowConfirmDialog(false);
|
| 100 |
+
if (onBack) {
|
| 101 |
+
onBack();
|
| 102 |
+
}
|
| 103 |
+
};
|
| 104 |
+
|
| 105 |
return (
|
| 106 |
<motion.div
|
| 107 |
initial={{ opacity: 0 }}
|
| 108 |
animate={{ opacity: 1 }}
|
| 109 |
className="text-center relative space-y-6"
|
| 110 |
>
|
| 111 |
+
<div className="flex items-center justify-between">
|
| 112 |
<Button
|
| 113 |
variant="ghost"
|
| 114 |
size="icon"
|
| 115 |
+
onClick={handleHomeClick}
|
| 116 |
className="text-gray-600 hover:text-primary"
|
| 117 |
>
|
| 118 |
<House className="h-5 w-5" />
|
| 119 |
</Button>
|
| 120 |
+
<h2 className="text-2xl font-semibold text-gray-900">Think in Sync</h2>
|
| 121 |
<div className="bg-primary/10 px-3 py-1 rounded-lg">
|
| 122 |
<span className="text-sm font-medium text-primary">
|
| 123 |
{t.game.round} {currentScore + 1}
|
| 124 |
</span>
|
| 125 |
</div>
|
|
|
|
| 126 |
</div>
|
| 127 |
|
| 128 |
+
<div className="space-y-2">
|
| 129 |
+
<p className="text-sm text-gray-600">{t.guess.goalDescription}</p>
|
| 130 |
+
<div className="overflow-hidden rounded-lg bg-secondary/10">
|
| 131 |
+
<p className="p-4 text-2xl font-bold tracking-wider text-secondary">
|
| 132 |
+
{currentWord}
|
| 133 |
+
</p>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 134 |
</div>
|
| 135 |
</div>
|
| 136 |
|
|
|
|
| 182 |
onPlayAgain();
|
| 183 |
}}
|
| 184 |
sessionId={sessionId}
|
| 185 |
+
onScoreSubmitted={handleScoreSubmitted}
|
| 186 |
/>
|
| 187 |
</DialogContent>
|
| 188 |
</Dialog>
|
|
|
|
| 195 |
</>
|
| 196 |
)}
|
| 197 |
</div>
|
| 198 |
+
|
| 199 |
+
<AlertDialog open={showConfirmDialog} onOpenChange={setShowConfirmDialog}>
|
| 200 |
+
<AlertDialogContent>
|
| 201 |
+
<AlertDialogHeader>
|
| 202 |
+
<AlertDialogTitle>{t.game.leaveGameTitle}</AlertDialogTitle>
|
| 203 |
+
<AlertDialogDescription>
|
| 204 |
+
{t.game.leaveGameDescription}
|
| 205 |
+
</AlertDialogDescription>
|
| 206 |
+
</AlertDialogHeader>
|
| 207 |
+
<AlertDialogFooter>
|
| 208 |
+
<AlertDialogCancel>{t.game.cancel}</AlertDialogCancel>
|
| 209 |
+
<AlertDialogAction onClick={handleConfirmHome}>
|
| 210 |
+
{t.game.confirm}
|
| 211 |
+
</AlertDialogAction>
|
| 212 |
+
</AlertDialogFooter>
|
| 213 |
+
</AlertDialogContent>
|
| 214 |
+
</AlertDialog>
|
| 215 |
</motion.div>
|
| 216 |
);
|
| 217 |
};
|
src/components/game/ThemeSelector.tsx
CHANGED
|
@@ -22,23 +22,13 @@ export const ThemeSelector = ({ onThemeSelect, onBack }: ThemeSelectorProps) =>
|
|
| 22 |
const t = useTranslation();
|
| 23 |
const { language } = useContext(LanguageContext);
|
| 24 |
|
| 25 |
-
useEffect(() => {
|
| 26 |
-
if (language !== 'en') {
|
| 27 |
-
setSelectedTheme("technology");
|
| 28 |
-
}
|
| 29 |
-
}, [language]);
|
| 30 |
-
|
| 31 |
useEffect(() => {
|
| 32 |
const handleKeyPress = (e: KeyboardEvent) => {
|
| 33 |
if (e.target instanceof HTMLInputElement) return;
|
| 34 |
|
| 35 |
switch(e.key.toLowerCase()) {
|
| 36 |
case 'a':
|
| 37 |
-
|
| 38 |
-
setSelectedTheme("standard");
|
| 39 |
-
} else {
|
| 40 |
-
setSelectedTheme("technology");
|
| 41 |
-
}
|
| 42 |
break;
|
| 43 |
case 'b':
|
| 44 |
setSelectedTheme("sports");
|
|
@@ -112,24 +102,14 @@ export const ThemeSelector = ({ onThemeSelect, onBack }: ThemeSelectorProps) =>
|
|
| 112 |
|
| 113 |
<p className="text-gray-600 text-center">{t.themes.subtitle}</p>
|
| 114 |
|
| 115 |
-
<div className="space-y-4">
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
>
|
| 122 |
-
|
| 123 |
-
</Button>
|
| 124 |
-
) : (
|
| 125 |
-
<Button
|
| 126 |
-
variant={selectedTheme === "technology" ? "default" : "outline"}
|
| 127 |
-
className="w-full justify-between"
|
| 128 |
-
onClick={() => setSelectedTheme("technology")}
|
| 129 |
-
>
|
| 130 |
-
{t.themes.technology} <span className="text-sm opacity-50">{t.themes.pressKey} A</span>
|
| 131 |
-
</Button>
|
| 132 |
-
)}
|
| 133 |
|
| 134 |
<Button
|
| 135 |
variant={selectedTheme === "sports" ? "default" : "outline"}
|
|
|
|
| 22 |
const t = useTranslation();
|
| 23 |
const { language } = useContext(LanguageContext);
|
| 24 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
useEffect(() => {
|
| 26 |
const handleKeyPress = (e: KeyboardEvent) => {
|
| 27 |
if (e.target instanceof HTMLInputElement) return;
|
| 28 |
|
| 29 |
switch(e.key.toLowerCase()) {
|
| 30 |
case 'a':
|
| 31 |
+
setSelectedTheme("standard");
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
break;
|
| 33 |
case 'b':
|
| 34 |
setSelectedTheme("sports");
|
|
|
|
| 102 |
|
| 103 |
<p className="text-gray-600 text-center">{t.themes.subtitle}</p>
|
| 104 |
|
| 105 |
+
<div className="space-y-4">
|
| 106 |
+
<Button
|
| 107 |
+
variant={selectedTheme === "standard" ? "default" : "outline"}
|
| 108 |
+
className="w-full justify-between"
|
| 109 |
+
onClick={() => setSelectedTheme("standard")}
|
| 110 |
+
>
|
| 111 |
+
{t.themes.standard} <span className="text-sm opacity-50">{t.themes.pressKey} A</span>
|
| 112 |
+
</Button>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 113 |
|
| 114 |
<Button
|
| 115 |
variant={selectedTheme === "sports" ? "default" : "outline"}
|
src/lib/words.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
export const
|
| 2 |
"DOG",
|
| 3 |
"CAT",
|
| 4 |
"SUN",
|
|
@@ -103,6 +103,235 @@ export const words = [
|
|
| 103 |
"BALLON"
|
| 104 |
];
|
| 105 |
|
| 106 |
-
export const
|
| 107 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 108 |
};
|
|
|
|
| 1 |
+
export const englishWords = [
|
| 2 |
"DOG",
|
| 3 |
"CAT",
|
| 4 |
"SUN",
|
|
|
|
| 103 |
"BALLON"
|
| 104 |
];
|
| 105 |
|
| 106 |
+
export const germanWords = [
|
| 107 |
+
"HUND",
|
| 108 |
+
"KATZE",
|
| 109 |
+
"SONNE",
|
| 110 |
+
"REGEN",
|
| 111 |
+
"BAUM",
|
| 112 |
+
"STERN",
|
| 113 |
+
"MOND",
|
| 114 |
+
"FISCH",
|
| 115 |
+
"VOGEL",
|
| 116 |
+
"WOLKE",
|
| 117 |
+
"HIMMEL",
|
| 118 |
+
"WIND",
|
| 119 |
+
"SCHNEE",
|
| 120 |
+
"BLUME",
|
| 121 |
+
"WASSER",
|
| 122 |
+
"OZEAN",
|
| 123 |
+
"FLUSS",
|
| 124 |
+
"BERG",
|
| 125 |
+
"WALD",
|
| 126 |
+
"HAUS",
|
| 127 |
+
"KERZE",
|
| 128 |
+
"GARTEN",
|
| 129 |
+
"BRÜCKE",
|
| 130 |
+
"INSEL",
|
| 131 |
+
"LICHT",
|
| 132 |
+
"DONNER",
|
| 133 |
+
"LÄCHELN",
|
| 134 |
+
"FREUND",
|
| 135 |
+
"FAMILIE",
|
| 136 |
+
"APFEL",
|
| 137 |
+
"BANANE",
|
| 138 |
+
"AUTO",
|
| 139 |
+
"BOOT",
|
| 140 |
+
"BALL",
|
| 141 |
+
"KUCHEN",
|
| 142 |
+
"FROSCH",
|
| 143 |
+
"PFERD",
|
| 144 |
+
"LÖWE",
|
| 145 |
+
"AFFE",
|
| 146 |
+
"PANDA",
|
| 147 |
+
"FLUGZEUG",
|
| 148 |
+
"ZUG",
|
| 149 |
+
"BONBON",
|
| 150 |
+
"SPRINGEN",
|
| 151 |
+
"SPIELEN",
|
| 152 |
+
"SCHLAFEN",
|
| 153 |
+
"LACHEN",
|
| 154 |
+
"TRAUM",
|
| 155 |
+
"GLÜCK",
|
| 156 |
+
"FARBE"
|
| 157 |
+
];
|
| 158 |
+
|
| 159 |
+
export const frenchWords = [
|
| 160 |
+
"CHIEN",
|
| 161 |
+
"CHAT",
|
| 162 |
+
"SOLEIL",
|
| 163 |
+
"PLUIE",
|
| 164 |
+
"ARBRE",
|
| 165 |
+
"ÉTOILE",
|
| 166 |
+
"LUNE",
|
| 167 |
+
"POISSON",
|
| 168 |
+
"OISEAU",
|
| 169 |
+
"NUAGE",
|
| 170 |
+
"CIEL",
|
| 171 |
+
"VENT",
|
| 172 |
+
"NEIGE",
|
| 173 |
+
"FLEUR",
|
| 174 |
+
"EAU",
|
| 175 |
+
"OCÉAN",
|
| 176 |
+
"RIVIÈRE",
|
| 177 |
+
"MONTAGNE",
|
| 178 |
+
"FORÊT",
|
| 179 |
+
"MAISON",
|
| 180 |
+
"BOUGIE",
|
| 181 |
+
"JARDIN",
|
| 182 |
+
"PONT",
|
| 183 |
+
"ÎLE",
|
| 184 |
+
"LUMIÈRE",
|
| 185 |
+
"TONNERRE",
|
| 186 |
+
"SOURIRE",
|
| 187 |
+
"AMI",
|
| 188 |
+
"FAMILLE",
|
| 189 |
+
"POMME",
|
| 190 |
+
"BANANE",
|
| 191 |
+
"VOITURE",
|
| 192 |
+
"BATEAU",
|
| 193 |
+
"BALLON",
|
| 194 |
+
"GÂTEAU",
|
| 195 |
+
"GRENOUILLE",
|
| 196 |
+
"CHEVAL",
|
| 197 |
+
"LION",
|
| 198 |
+
"SINGE",
|
| 199 |
+
"PANDA",
|
| 200 |
+
"AVION",
|
| 201 |
+
"TRAIN",
|
| 202 |
+
"BONBON",
|
| 203 |
+
"SAUTER",
|
| 204 |
+
"JOUER",
|
| 205 |
+
"DORMIR",
|
| 206 |
+
"RIRE",
|
| 207 |
+
"RÊVE",
|
| 208 |
+
"BONHEUR",
|
| 209 |
+
"COULEUR"
|
| 210 |
+
];
|
| 211 |
+
|
| 212 |
+
export const italianWords = [
|
| 213 |
+
"CANE",
|
| 214 |
+
"GATTO",
|
| 215 |
+
"SOLE",
|
| 216 |
+
"PIOGGIA",
|
| 217 |
+
"ALBERO",
|
| 218 |
+
"STELLA",
|
| 219 |
+
"LUNA",
|
| 220 |
+
"PESCE",
|
| 221 |
+
"UCCELLO",
|
| 222 |
+
"NUVOLA",
|
| 223 |
+
"CIELO",
|
| 224 |
+
"VENTO",
|
| 225 |
+
"NEVE",
|
| 226 |
+
"FIORE",
|
| 227 |
+
"ACQUA",
|
| 228 |
+
"OCEANO",
|
| 229 |
+
"FIUME",
|
| 230 |
+
"MONTAGNA",
|
| 231 |
+
"FORESTA",
|
| 232 |
+
"CASA",
|
| 233 |
+
"CANDELA",
|
| 234 |
+
"GIARDINO",
|
| 235 |
+
"PONTE",
|
| 236 |
+
"ISOLA",
|
| 237 |
+
"LUCE",
|
| 238 |
+
"TUONO",
|
| 239 |
+
"SORRISO",
|
| 240 |
+
"AMICO",
|
| 241 |
+
"FAMIGLIA",
|
| 242 |
+
"MELA",
|
| 243 |
+
"BANANA",
|
| 244 |
+
"AUTO",
|
| 245 |
+
"BARCA",
|
| 246 |
+
"PALLA",
|
| 247 |
+
"TORTA",
|
| 248 |
+
"RANA",
|
| 249 |
+
"CAVALLO",
|
| 250 |
+
"LEONE",
|
| 251 |
+
"SCIMMIA",
|
| 252 |
+
"PANDA",
|
| 253 |
+
"AEREO",
|
| 254 |
+
"TRENO",
|
| 255 |
+
"CARAMELLA",
|
| 256 |
+
"SALTARE",
|
| 257 |
+
"GIOCARE",
|
| 258 |
+
"DORMIRE",
|
| 259 |
+
"RIDERE",
|
| 260 |
+
"SOGNO",
|
| 261 |
+
"FELICITÀ",
|
| 262 |
+
"COLORE"
|
| 263 |
+
];
|
| 264 |
+
|
| 265 |
+
export const spanishWords = [
|
| 266 |
+
"PERRO",
|
| 267 |
+
"GATO",
|
| 268 |
+
"SOL",
|
| 269 |
+
"LLUVIA",
|
| 270 |
+
"ÁRBOL",
|
| 271 |
+
"ESTRELLA",
|
| 272 |
+
"LUNA",
|
| 273 |
+
"PEZ",
|
| 274 |
+
"PÁJARO",
|
| 275 |
+
"NUBE",
|
| 276 |
+
"CIELO",
|
| 277 |
+
"VIENTO",
|
| 278 |
+
"NIEVE",
|
| 279 |
+
"FLOR",
|
| 280 |
+
"AGUA",
|
| 281 |
+
"OCÉANO",
|
| 282 |
+
"RÍO",
|
| 283 |
+
"MONTAÑA",
|
| 284 |
+
"BOSQUE",
|
| 285 |
+
"CASA",
|
| 286 |
+
"VELA",
|
| 287 |
+
"JARDÍN",
|
| 288 |
+
"PUENTE",
|
| 289 |
+
"ISLA",
|
| 290 |
+
"LUZ",
|
| 291 |
+
"TRUENO",
|
| 292 |
+
"SONRISA",
|
| 293 |
+
"AMIGO",
|
| 294 |
+
"FAMILIA",
|
| 295 |
+
"MANZANA",
|
| 296 |
+
"PLÁTANO",
|
| 297 |
+
"COCHE",
|
| 298 |
+
"BARCO",
|
| 299 |
+
"PELOTA",
|
| 300 |
+
"PASTEL",
|
| 301 |
+
"RANA",
|
| 302 |
+
"CABALLO",
|
| 303 |
+
"LEÓN",
|
| 304 |
+
"MONO",
|
| 305 |
+
"PANDA",
|
| 306 |
+
"AVIÓN",
|
| 307 |
+
"TREN",
|
| 308 |
+
"CARAMELO",
|
| 309 |
+
"SALTAR",
|
| 310 |
+
"JUGAR",
|
| 311 |
+
"DORMIR",
|
| 312 |
+
"REÍR",
|
| 313 |
+
"SUEÑO",
|
| 314 |
+
"FELICIDAD",
|
| 315 |
+
"COLOR"
|
| 316 |
+
];
|
| 317 |
+
|
| 318 |
+
export const getRandomWord = (language: string = 'en') => {
|
| 319 |
+
let wordList;
|
| 320 |
+
switch (language) {
|
| 321 |
+
case 'de':
|
| 322 |
+
wordList = germanWords;
|
| 323 |
+
break;
|
| 324 |
+
case 'fr':
|
| 325 |
+
wordList = frenchWords;
|
| 326 |
+
break;
|
| 327 |
+
case 'it':
|
| 328 |
+
wordList = italianWords;
|
| 329 |
+
break;
|
| 330 |
+
case 'es':
|
| 331 |
+
wordList = spanishWords;
|
| 332 |
+
break;
|
| 333 |
+
default:
|
| 334 |
+
wordList = englishWords;
|
| 335 |
+
}
|
| 336 |
+
return wordList[Math.floor(Math.random() * wordList.length)];
|
| 337 |
};
|
src/services/mistralService.ts
CHANGED
|
@@ -31,8 +31,11 @@ export const generateAIResponse = async (currentWord: string, currentSentence: s
|
|
| 31 |
export const guessWord = async (sentence: string, language: string): Promise<string> => {
|
| 32 |
console.log('Processing guess for sentence:', sentence);
|
| 33 |
|
| 34 |
-
//
|
| 35 |
const words = sentence.trim().split(/\s+/);
|
|
|
|
|
|
|
|
|
|
| 36 |
if (words.length < 3) {
|
| 37 |
console.log('Short description detected, checking for fraud...');
|
| 38 |
|
|
@@ -40,7 +43,7 @@ export const guessWord = async (sentence: string, language: string): Promise<str
|
|
| 40 |
const { data: fraudData, error: fraudError } = await supabase.functions.invoke('detect-fraud', {
|
| 41 |
body: {
|
| 42 |
sentence,
|
| 43 |
-
targetWord
|
| 44 |
language
|
| 45 |
}
|
| 46 |
});
|
|
@@ -60,7 +63,11 @@ export const guessWord = async (sentence: string, language: string): Promise<str
|
|
| 60 |
console.log('Calling guess-word function with sentence:', sentence, 'language:', language);
|
| 61 |
|
| 62 |
const { data, error } = await supabase.functions.invoke('guess-word', {
|
| 63 |
-
body: {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
});
|
| 65 |
|
| 66 |
if (error) {
|
|
|
|
| 31 |
export const guessWord = async (sentence: string, language: string): Promise<string> => {
|
| 32 |
console.log('Processing guess for sentence:', sentence);
|
| 33 |
|
| 34 |
+
// Extract the target word from the sentence (assuming it's the first word)
|
| 35 |
const words = sentence.trim().split(/\s+/);
|
| 36 |
+
const targetWord = words[0].toLowerCase();
|
| 37 |
+
|
| 38 |
+
// Check for potential fraud if the sentence has less than 3 words
|
| 39 |
if (words.length < 3) {
|
| 40 |
console.log('Short description detected, checking for fraud...');
|
| 41 |
|
|
|
|
| 43 |
const { data: fraudData, error: fraudError } = await supabase.functions.invoke('detect-fraud', {
|
| 44 |
body: {
|
| 45 |
sentence,
|
| 46 |
+
targetWord,
|
| 47 |
language
|
| 48 |
}
|
| 49 |
});
|
|
|
|
| 63 |
console.log('Calling guess-word function with sentence:', sentence, 'language:', language);
|
| 64 |
|
| 65 |
const { data, error } = await supabase.functions.invoke('guess-word', {
|
| 66 |
+
body: {
|
| 67 |
+
sentence,
|
| 68 |
+
targetWord, // Pass the target word to prevent guessing it
|
| 69 |
+
language
|
| 70 |
+
}
|
| 71 |
});
|
| 72 |
|
| 73 |
if (error) {
|
supabase/functions/detect-fraud/index.ts
CHANGED
|
@@ -6,6 +6,8 @@ const corsHeaders = {
|
|
| 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') {
|
|
@@ -26,46 +28,53 @@ serve(async (req) => {
|
|
| 26 |
|
| 27 |
while (retryCount < maxRetries) {
|
| 28 |
try {
|
|
|
|
|
|
|
| 29 |
const response = await client.chat.complete({
|
| 30 |
-
model: "mistral-
|
| 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.
|
| 38 |
-
2.
|
| 39 |
|
| 40 |
Examples for cheating:
|
| 41 |
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
|
| 52 |
Synonyms and names of instances of a class are legitimate descriptions.
|
| 53 |
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
Respond with ONLY "cheating" or "legitimate" (no punctuation or explanation).`
|
| 70 |
},
|
| 71 |
{
|
|
@@ -81,6 +90,10 @@ serve(async (req) => {
|
|
| 81 |
temperature: 0.1
|
| 82 |
});
|
| 83 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
const verdict = response.choices[0].message.content.trim().toLowerCase();
|
| 85 |
console.log('Fraud detection verdict:', verdict);
|
| 86 |
|
|
@@ -92,24 +105,29 @@ serve(async (req) => {
|
|
| 92 |
console.error(`Attempt ${retryCount + 1} failed:`, error);
|
| 93 |
lastError = error;
|
| 94 |
|
| 95 |
-
if
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
|
|
|
|
|
|
|
|
|
| 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 |
|
|
@@ -119,7 +137,7 @@ serve(async (req) => {
|
|
| 119 |
details: error.message
|
| 120 |
}),
|
| 121 |
{
|
| 122 |
-
status: error.message?.includes('rate limit') ? 429 : 500,
|
| 123 |
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
|
| 124 |
}
|
| 125 |
);
|
|
|
|
| 6 |
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
|
| 7 |
};
|
| 8 |
|
| 9 |
+
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
|
| 10 |
+
|
| 11 |
serve(async (req) => {
|
| 12 |
// Handle CORS preflight requests
|
| 13 |
if (req.method === 'OPTIONS') {
|
|
|
|
| 28 |
|
| 29 |
while (retryCount < maxRetries) {
|
| 30 |
try {
|
| 31 |
+
console.log(`Attempt ${retryCount + 1} to check for fraud`);
|
| 32 |
+
|
| 33 |
const response = await client.chat.complete({
|
| 34 |
+
model: "mistral-large-latest",
|
| 35 |
messages: [
|
| 36 |
{
|
| 37 |
role: "system",
|
| 38 |
content: `You are a fraud detection system for a word guessing game.
|
| 39 |
The game is being played in ${language}.
|
| 40 |
+
Your task is to detect if a player is trying to cheat by one of two methods:
|
| 41 |
+
1. The Player's description is a misspelling of the target word
|
| 42 |
+
2. The Player's description is a sentence without spaces
|
| 43 |
|
| 44 |
Examples for cheating:
|
| 45 |
|
| 46 |
+
Target word: hand
|
| 47 |
+
Player's description: hnd
|
| 48 |
+
Language: en
|
| 49 |
+
CORRECT ANSWER: cheating
|
| 50 |
+
|
| 51 |
+
Target word: barfuß
|
| 52 |
+
Player's description: germanwordforbarefoot
|
| 53 |
+
Language: de
|
| 54 |
+
CORRECT ANSWER: cheating
|
| 55 |
|
| 56 |
Synonyms and names of instances of a class are legitimate descriptions.
|
| 57 |
|
| 58 |
+
Target word: laptop
|
| 59 |
+
Player's description: notebook
|
| 60 |
+
Language: en
|
| 61 |
+
CORRECT ANSWER: legitimate
|
| 62 |
+
|
| 63 |
+
Target word: play
|
| 64 |
+
Player's description: children often
|
| 65 |
+
Language: en
|
| 66 |
+
CORRECT ANSWER: legitimate
|
| 67 |
+
|
| 68 |
+
Target word: Pfankuchen
|
| 69 |
+
Player's description: Berliner
|
| 70 |
+
Language: de
|
| 71 |
+
CORRECT ANSWER: legitimate
|
| 72 |
+
|
| 73 |
+
Target word: Burrito
|
| 74 |
+
Player's description: Wrap
|
| 75 |
+
Language: es
|
| 76 |
+
CORRECT ANSWER: legitimate
|
| 77 |
+
|
| 78 |
Respond with ONLY "cheating" or "legitimate" (no punctuation or explanation).`
|
| 79 |
},
|
| 80 |
{
|
|
|
|
| 90 |
temperature: 0.1
|
| 91 |
});
|
| 92 |
|
| 93 |
+
if (!response?.choices?.[0]?.message?.content) {
|
| 94 |
+
throw new Error('Invalid response format from Mistral API');
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
const verdict = response.choices[0].message.content.trim().toLowerCase();
|
| 98 |
console.log('Fraud detection verdict:', verdict);
|
| 99 |
|
|
|
|
| 105 |
console.error(`Attempt ${retryCount + 1} failed:`, error);
|
| 106 |
lastError = error;
|
| 107 |
|
| 108 |
+
// Check if it's a rate limit or service unavailable error
|
| 109 |
+
if (error.message?.includes('rate limit') ||
|
| 110 |
+
error.message?.includes('503') ||
|
| 111 |
+
error.message?.includes('Service unavailable')) {
|
| 112 |
+
const waitTime = Math.pow(2, retryCount) * 1000; // Exponential backoff
|
| 113 |
+
console.log(`Service unavailable or rate limited, waiting ${waitTime}ms before retry`);
|
| 114 |
+
await sleep(waitTime);
|
| 115 |
retryCount++;
|
| 116 |
continue;
|
| 117 |
}
|
| 118 |
|
| 119 |
+
// If it's not a retryable error, throw immediately
|
| 120 |
throw error;
|
| 121 |
}
|
| 122 |
}
|
| 123 |
|
| 124 |
+
// If we've exhausted all retries
|
| 125 |
throw new Error(`Failed after ${maxRetries} attempts. Last error: ${lastError?.message}`);
|
| 126 |
|
| 127 |
} catch (error) {
|
| 128 |
console.error('Error in fraud detection:', error);
|
| 129 |
|
| 130 |
+
const errorMessage = error.message?.includes('rate limit') || error.message?.includes('503')
|
| 131 |
? "The AI service is currently busy. Please try again in a few moments."
|
| 132 |
: "Sorry, there was an error checking for fraud. Please try again.";
|
| 133 |
|
|
|
|
| 137 |
details: error.message
|
| 138 |
}),
|
| 139 |
{
|
| 140 |
+
status: error.message?.includes('rate limit') || error.message?.includes('503') ? 429 : 500,
|
| 141 |
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
|
| 142 |
}
|
| 143 |
);
|
supabase/functions/generate-themed-word/index.ts
CHANGED
|
@@ -38,28 +38,42 @@ const openRouterModels = [
|
|
| 38 |
];
|
| 39 |
|
| 40 |
async function tryMistral(theme: string, usedWords: string[], language: string) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
const client = new Mistral({
|
| 42 |
-
apiKey:
|
| 43 |
});
|
| 44 |
|
| 45 |
const prompts = languagePrompts[language as keyof typeof languagePrompts] || languagePrompts.en;
|
| 46 |
|
| 47 |
const response = await client.chat.complete({
|
| 48 |
-
model: "mistral-
|
| 49 |
messages: [
|
| 50 |
{
|
| 51 |
role: "system",
|
| 52 |
content: `${prompts.systemPrompt} "${theme}".\n${prompts.requirements} ${usedWords.join(', ')}\n\nRespond with just the word in UPPERCASE, nothing else.`
|
| 53 |
}
|
| 54 |
],
|
| 55 |
-
maxTokens:
|
| 56 |
temperature: 0.99
|
| 57 |
});
|
| 58 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
return response.choices[0].message.content.trim();
|
| 60 |
}
|
| 61 |
|
| 62 |
async function tryOpenRouter(theme: string, usedWords: string[], language: string) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
const prompts = languagePrompts[language as keyof typeof languagePrompts] || languagePrompts.en;
|
| 64 |
const randomModel = openRouterModels[Math.floor(Math.random() * openRouterModels.length)];
|
| 65 |
|
|
@@ -68,7 +82,7 @@ async function tryOpenRouter(theme: string, usedWords: string[], language: strin
|
|
| 68 |
const response = await fetch("https://openrouter.ai/api/v1/chat/completions", {
|
| 69 |
method: "POST",
|
| 70 |
headers: {
|
| 71 |
-
"Authorization": `Bearer ${
|
| 72 |
"HTTP-Referer": "https://think-in-sync.com",
|
| 73 |
"X-Title": "Think in Sync",
|
| 74 |
"Content-Type": "application/json"
|
|
@@ -85,10 +99,16 @@ async function tryOpenRouter(theme: string, usedWords: string[], language: strin
|
|
| 85 |
});
|
| 86 |
|
| 87 |
if (!response.ok) {
|
| 88 |
-
|
|
|
|
| 89 |
}
|
| 90 |
|
| 91 |
const data = await response.json();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 92 |
return data.choices[0].message.content.trim();
|
| 93 |
}
|
| 94 |
|
|
@@ -101,33 +121,54 @@ serve(async (req) => {
|
|
| 101 |
const { theme, usedWords = [], language = 'en' } = await req.json();
|
| 102 |
console.log('Generating word for theme:', theme, 'language:', language, 'excluding:', usedWords);
|
| 103 |
|
|
|
|
|
|
|
|
|
|
| 104 |
try {
|
| 105 |
console.log('Attempting with Mistral...');
|
| 106 |
-
|
| 107 |
console.log('Successfully generated word with Mistral:', word);
|
| 108 |
-
return new Response(
|
| 109 |
-
JSON.stringify({ word }),
|
| 110 |
-
{ headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
| 111 |
-
);
|
| 112 |
} catch (mistralError) {
|
| 113 |
console.error('Mistral error:', mistralError);
|
| 114 |
console.log('Falling back to OpenRouter...');
|
| 115 |
|
| 116 |
-
|
| 117 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 118 |
return new Response(
|
| 119 |
-
JSON.stringify({
|
| 120 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 121 |
);
|
| 122 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 123 |
} catch (error) {
|
| 124 |
console.error('Error generating themed word:', error);
|
| 125 |
return new Response(
|
| 126 |
-
JSON.stringify({
|
|
|
|
|
|
|
|
|
|
| 127 |
{
|
| 128 |
status: 500,
|
| 129 |
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
|
| 130 |
}
|
| 131 |
);
|
| 132 |
}
|
| 133 |
-
});
|
|
|
|
| 38 |
];
|
| 39 |
|
| 40 |
async function tryMistral(theme: string, usedWords: string[], language: string) {
|
| 41 |
+
const mistralKey = Deno.env.get('MISTRAL_API_KEY');
|
| 42 |
+
if (!mistralKey) {
|
| 43 |
+
throw new Error('Mistral API key not configured');
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
const client = new Mistral({
|
| 47 |
+
apiKey: mistralKey,
|
| 48 |
});
|
| 49 |
|
| 50 |
const prompts = languagePrompts[language as keyof typeof languagePrompts] || languagePrompts.en;
|
| 51 |
|
| 52 |
const response = await client.chat.complete({
|
| 53 |
+
model: "mistral-large-latest",
|
| 54 |
messages: [
|
| 55 |
{
|
| 56 |
role: "system",
|
| 57 |
content: `${prompts.systemPrompt} "${theme}".\n${prompts.requirements} ${usedWords.join(', ')}\n\nRespond with just the word in UPPERCASE, nothing else.`
|
| 58 |
}
|
| 59 |
],
|
| 60 |
+
maxTokens: 50,
|
| 61 |
temperature: 0.99
|
| 62 |
});
|
| 63 |
|
| 64 |
+
if (!response?.choices?.[0]?.message?.content) {
|
| 65 |
+
throw new Error('Invalid response from Mistral API');
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
return response.choices[0].message.content.trim();
|
| 69 |
}
|
| 70 |
|
| 71 |
async function tryOpenRouter(theme: string, usedWords: string[], language: string) {
|
| 72 |
+
const openRouterKey = Deno.env.get('OPENROUTER_API_KEY');
|
| 73 |
+
if (!openRouterKey) {
|
| 74 |
+
throw new Error('OpenRouter API key not configured');
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
const prompts = languagePrompts[language as keyof typeof languagePrompts] || languagePrompts.en;
|
| 78 |
const randomModel = openRouterModels[Math.floor(Math.random() * openRouterModels.length)];
|
| 79 |
|
|
|
|
| 82 |
const response = await fetch("https://openrouter.ai/api/v1/chat/completions", {
|
| 83 |
method: "POST",
|
| 84 |
headers: {
|
| 85 |
+
"Authorization": `Bearer ${openRouterKey}`,
|
| 86 |
"HTTP-Referer": "https://think-in-sync.com",
|
| 87 |
"X-Title": "Think in Sync",
|
| 88 |
"Content-Type": "application/json"
|
|
|
|
| 99 |
});
|
| 100 |
|
| 101 |
if (!response.ok) {
|
| 102 |
+
const errorText = await response.text();
|
| 103 |
+
throw new Error(`OpenRouter API error: ${response.status} - ${errorText}`);
|
| 104 |
}
|
| 105 |
|
| 106 |
const data = await response.json();
|
| 107 |
+
|
| 108 |
+
if (!data?.choices?.[0]?.message?.content) {
|
| 109 |
+
throw new Error('Invalid response from OpenRouter API');
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
return data.choices[0].message.content.trim();
|
| 113 |
}
|
| 114 |
|
|
|
|
| 121 |
const { theme, usedWords = [], language = 'en' } = await req.json();
|
| 122 |
console.log('Generating word for theme:', theme, 'language:', language, 'excluding:', usedWords);
|
| 123 |
|
| 124 |
+
let word;
|
| 125 |
+
let error;
|
| 126 |
+
|
| 127 |
try {
|
| 128 |
console.log('Attempting with Mistral...');
|
| 129 |
+
word = await tryMistral(theme, usedWords, language);
|
| 130 |
console.log('Successfully generated word with Mistral:', word);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 131 |
} catch (mistralError) {
|
| 132 |
console.error('Mistral error:', mistralError);
|
| 133 |
console.log('Falling back to OpenRouter...');
|
| 134 |
|
| 135 |
+
try {
|
| 136 |
+
word = await tryOpenRouter(theme, usedWords, language);
|
| 137 |
+
console.log('Successfully generated word with OpenRouter:', word);
|
| 138 |
+
} catch (openRouterError) {
|
| 139 |
+
console.error('OpenRouter error:', openRouterError);
|
| 140 |
+
error = openRouterError;
|
| 141 |
+
}
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
if (!word) {
|
| 145 |
return new Response(
|
| 146 |
+
JSON.stringify({
|
| 147 |
+
error: 'Failed to generate word with both Mistral and OpenRouter',
|
| 148 |
+
details: error?.message || 'Unknown error'
|
| 149 |
+
}),
|
| 150 |
+
{
|
| 151 |
+
status: 500,
|
| 152 |
+
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
|
| 153 |
+
}
|
| 154 |
);
|
| 155 |
}
|
| 156 |
+
|
| 157 |
+
return new Response(
|
| 158 |
+
JSON.stringify({ word }),
|
| 159 |
+
{ headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
| 160 |
+
);
|
| 161 |
} catch (error) {
|
| 162 |
console.error('Error generating themed word:', error);
|
| 163 |
return new Response(
|
| 164 |
+
JSON.stringify({
|
| 165 |
+
error: 'Error generating themed word',
|
| 166 |
+
details: error.message
|
| 167 |
+
}),
|
| 168 |
{
|
| 169 |
status: 500,
|
| 170 |
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
|
| 171 |
}
|
| 172 |
);
|
| 173 |
}
|
| 174 |
+
});
|
supabase/functions/generate-word/index.ts
CHANGED
|
@@ -49,14 +49,14 @@ async function tryMistral(currentWord: string, existingSentence: string, languag
|
|
| 49 |
const prompts = languagePrompts[language as keyof typeof languagePrompts] || languagePrompts.en;
|
| 50 |
|
| 51 |
const response = await client.chat.complete({
|
| 52 |
-
model: "mistral-
|
| 53 |
messages: [
|
| 54 |
{
|
| 55 |
role: "system",
|
| 56 |
content: `${prompts.systemPrompt} "${currentWord}". ${prompts.task} ${prompts.instruction} "${existingSentence}". Do not add quotes or backticks. Just answer with the sentence.`
|
| 57 |
}
|
| 58 |
],
|
| 59 |
-
maxTokens:
|
| 60 |
temperature: 0.5
|
| 61 |
});
|
| 62 |
|
|
|
|
| 49 |
const prompts = languagePrompts[language as keyof typeof languagePrompts] || languagePrompts.en;
|
| 50 |
|
| 51 |
const response = await client.chat.complete({
|
| 52 |
+
model: "mistral-large-latest",
|
| 53 |
messages: [
|
| 54 |
{
|
| 55 |
role: "system",
|
| 56 |
content: `${prompts.systemPrompt} "${currentWord}". ${prompts.task} ${prompts.instruction} "${existingSentence}". Do not add quotes or backticks. Just answer with the sentence.`
|
| 57 |
}
|
| 58 |
],
|
| 59 |
+
maxTokens: 50,
|
| 60 |
temperature: 0.5
|
| 61 |
});
|
| 62 |
|
supabase/functions/guess-word/index.ts
CHANGED
|
@@ -44,7 +44,7 @@ async function tryMistral(sentence: string, language: string) {
|
|
| 44 |
const prompts = languagePrompts[language as keyof typeof languagePrompts] || languagePrompts.en;
|
| 45 |
|
| 46 |
const response = await client.chat.complete({
|
| 47 |
-
model: "mistral-
|
| 48 |
messages: [
|
| 49 |
{
|
| 50 |
role: "system",
|
|
|
|
| 44 |
const prompts = languagePrompts[language as keyof typeof languagePrompts] || languagePrompts.en;
|
| 45 |
|
| 46 |
const response = await client.chat.complete({
|
| 47 |
+
model: "mistral-large-latest",
|
| 48 |
messages: [
|
| 49 |
{
|
| 50 |
role: "system",
|
supabase/functions/validate-sentence/index.ts
CHANGED
|
@@ -20,7 +20,7 @@ serve(async (req) => {
|
|
| 20 |
});
|
| 21 |
|
| 22 |
const response = await client.chat.complete({
|
| 23 |
-
model: "mistral-
|
| 24 |
messages: [
|
| 25 |
{
|
| 26 |
role: "system",
|
|
|
|
| 20 |
});
|
| 21 |
|
| 22 |
const response = await client.chat.complete({
|
| 23 |
+
model: "mistral-large-latest",
|
| 24 |
messages: [
|
| 25 |
{
|
| 26 |
role: "system",
|