Felix Zieger
commited on
Commit
·
6864389
1
Parent(s):
2e860a4
refactor
Browse files- src/components/GameContainer.tsx +5 -17
- src/components/game/GameOver.tsx +0 -45
- src/components/game/GuessDisplay.tsx +29 -166
- src/components/game/SentenceBuilder.tsx +2 -1
- src/components/game/guess-display/ActionButtons.tsx +72 -0
- src/components/game/guess-display/GuessDescription.tsx +26 -0
- src/components/game/guess-display/GuessResult.tsx +14 -0
- src/components/game/sentence-builder/InputForm.tsx +15 -6
- src/components/game/sentence-builder/RoundHeader.tsx +3 -3
- src/services/mistralService.ts +1 -27
- supabase/functions/detect-fraud/index.ts +0 -145
src/components/GameContainer.tsx
CHANGED
|
@@ -8,12 +8,10 @@ import { WelcomeScreen } from "./game/WelcomeScreen";
|
|
| 8 |
import { ThemeSelector } from "./game/ThemeSelector";
|
| 9 |
import { SentenceBuilder } from "./game/SentenceBuilder";
|
| 10 |
import { GuessDisplay } from "./game/GuessDisplay";
|
| 11 |
-
import { GameOver } from "./game/GameOver";
|
| 12 |
import { useTranslation } from "@/hooks/useTranslation";
|
| 13 |
import { LanguageContext } from "@/contexts/LanguageContext";
|
| 14 |
-
import { supabase } from "@/integrations/supabase/client";
|
| 15 |
|
| 16 |
-
type GameState = "welcome" | "theme-selection" | "building-sentence" | "showing-guess"
|
| 17 |
|
| 18 |
export const GameContainer = () => {
|
| 19 |
const [gameState, setGameState] = useState<GameState>("welcome");
|
|
@@ -32,7 +30,6 @@ export const GameContainer = () => {
|
|
| 32 |
const { language } = useContext(LanguageContext);
|
| 33 |
|
| 34 |
useEffect(() => {
|
| 35 |
-
// Generate a new session ID when starting a new game
|
| 36 |
if (gameState === "theme-selection") {
|
| 37 |
setSessionId(crypto.randomUUID());
|
| 38 |
}
|
|
@@ -43,12 +40,10 @@ export const GameContainer = () => {
|
|
| 43 |
if (e.key === 'Enter') {
|
| 44 |
if (gameState === 'welcome') {
|
| 45 |
handleStart();
|
| 46 |
-
} else if (gameState === '
|
| 47 |
const correct = isGuessCorrect();
|
| 48 |
if (correct) {
|
| 49 |
handleNextRound();
|
| 50 |
-
} else {
|
| 51 |
-
setGameState("game-over");
|
| 52 |
}
|
| 53 |
}
|
| 54 |
}
|
|
@@ -176,8 +171,6 @@ export const GameContainer = () => {
|
|
| 176 |
}
|
| 177 |
};
|
| 178 |
getNewWord();
|
| 179 |
-
} else {
|
| 180 |
-
setGameState("game-over");
|
| 181 |
}
|
| 182 |
};
|
| 183 |
|
|
@@ -232,7 +225,7 @@ export const GameContainer = () => {
|
|
| 232 |
onMakeGuess={handleMakeGuess}
|
| 233 |
onBack={handleBack}
|
| 234 |
/>
|
| 235 |
-
) :
|
| 236 |
<GuessDisplay
|
| 237 |
sentence={sentence}
|
| 238 |
aiGuess={aiGuess}
|
|
@@ -243,13 +236,8 @@ export const GameContainer = () => {
|
|
| 243 |
avgWordsPerRound={getAverageWordsPerRound()}
|
| 244 |
sessionId={sessionId}
|
| 245 |
/>
|
| 246 |
-
)
|
| 247 |
-
<GameOver
|
| 248 |
-
successfulRounds={successfulRounds}
|
| 249 |
-
onPlayAgain={handlePlayAgain}
|
| 250 |
-
/>
|
| 251 |
-
) : null}
|
| 252 |
</motion.div>
|
| 253 |
</div>
|
| 254 |
);
|
| 255 |
-
};
|
|
|
|
| 8 |
import { ThemeSelector } from "./game/ThemeSelector";
|
| 9 |
import { SentenceBuilder } from "./game/SentenceBuilder";
|
| 10 |
import { GuessDisplay } from "./game/GuessDisplay";
|
|
|
|
| 11 |
import { useTranslation } from "@/hooks/useTranslation";
|
| 12 |
import { LanguageContext } from "@/contexts/LanguageContext";
|
|
|
|
| 13 |
|
| 14 |
+
type GameState = "welcome" | "theme-selection" | "building-sentence" | "showing-guess";
|
| 15 |
|
| 16 |
export const GameContainer = () => {
|
| 17 |
const [gameState, setGameState] = useState<GameState>("welcome");
|
|
|
|
| 30 |
const { language } = useContext(LanguageContext);
|
| 31 |
|
| 32 |
useEffect(() => {
|
|
|
|
| 33 |
if (gameState === "theme-selection") {
|
| 34 |
setSessionId(crypto.randomUUID());
|
| 35 |
}
|
|
|
|
| 40 |
if (e.key === 'Enter') {
|
| 41 |
if (gameState === 'welcome') {
|
| 42 |
handleStart();
|
| 43 |
+
} else if (gameState === 'showing-guess') {
|
| 44 |
const correct = isGuessCorrect();
|
| 45 |
if (correct) {
|
| 46 |
handleNextRound();
|
|
|
|
|
|
|
| 47 |
}
|
| 48 |
}
|
| 49 |
}
|
|
|
|
| 171 |
}
|
| 172 |
};
|
| 173 |
getNewWord();
|
|
|
|
|
|
|
| 174 |
}
|
| 175 |
};
|
| 176 |
|
|
|
|
| 225 |
onMakeGuess={handleMakeGuess}
|
| 226 |
onBack={handleBack}
|
| 227 |
/>
|
| 228 |
+
) : (
|
| 229 |
<GuessDisplay
|
| 230 |
sentence={sentence}
|
| 231 |
aiGuess={aiGuess}
|
|
|
|
| 236 |
avgWordsPerRound={getAverageWordsPerRound()}
|
| 237 |
sessionId={sessionId}
|
| 238 |
/>
|
| 239 |
+
)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 240 |
</motion.div>
|
| 241 |
</div>
|
| 242 |
);
|
| 243 |
+
};
|
src/components/game/GameOver.tsx
DELETED
|
@@ -1,45 +0,0 @@
|
|
| 1 |
-
import { Button } from "@/components/ui/button";
|
| 2 |
-
import { motion } from "framer-motion";
|
| 3 |
-
import { useEffect } from "react";
|
| 4 |
-
|
| 5 |
-
interface GameOverProps {
|
| 6 |
-
successfulRounds: number;
|
| 7 |
-
onPlayAgain: () => void;
|
| 8 |
-
}
|
| 9 |
-
|
| 10 |
-
export const GameOver = ({
|
| 11 |
-
successfulRounds,
|
| 12 |
-
onPlayAgain,
|
| 13 |
-
}: GameOverProps) => {
|
| 14 |
-
useEffect(() => {
|
| 15 |
-
const handleKeyPress = (e: KeyboardEvent) => {
|
| 16 |
-
if (e.key.toLowerCase() === 'enter') {
|
| 17 |
-
onPlayAgain();
|
| 18 |
-
}
|
| 19 |
-
};
|
| 20 |
-
|
| 21 |
-
window.addEventListener('keydown', handleKeyPress);
|
| 22 |
-
return () => window.removeEventListener('keydown', handleKeyPress);
|
| 23 |
-
}, [onPlayAgain]);
|
| 24 |
-
|
| 25 |
-
return (
|
| 26 |
-
<motion.div
|
| 27 |
-
initial={{ opacity: 0 }}
|
| 28 |
-
animate={{ opacity: 1 }}
|
| 29 |
-
className="text-center"
|
| 30 |
-
>
|
| 31 |
-
<h2 className="mb-4 text-2xl font-semibold text-gray-900">Game Over!</h2>
|
| 32 |
-
<p className="mb-6 text-lg text-gray-800">
|
| 33 |
-
You completed {successfulRounds} rounds successfully!
|
| 34 |
-
</p>
|
| 35 |
-
<div className="flex gap-4">
|
| 36 |
-
<Button
|
| 37 |
-
onClick={onPlayAgain}
|
| 38 |
-
className="flex-1 bg-primary text-lg hover:bg-primary/90"
|
| 39 |
-
>
|
| 40 |
-
Play Again ⏎
|
| 41 |
-
</Button>
|
| 42 |
-
</div>
|
| 43 |
-
</motion.div>
|
| 44 |
-
);
|
| 45 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/components/game/GuessDisplay.tsx
CHANGED
|
@@ -1,25 +1,18 @@
|
|
| 1 |
-
import { Button } from "@/components/ui/button";
|
| 2 |
import { motion } from "framer-motion";
|
|
|
|
|
|
|
|
|
|
| 3 |
import {
|
| 4 |
Dialog,
|
| 5 |
DialogContent,
|
| 6 |
DialogTrigger,
|
| 7 |
} from "@/components/ui/dialog";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
import { HighScoreBoard } from "@/components/HighScoreBoard";
|
| 9 |
-
import { useState, useEffect } from "react";
|
| 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[];
|
|
@@ -44,174 +37,44 @@ export const GuessDisplay = ({
|
|
| 44 |
avgWordsPerRound,
|
| 45 |
sessionId,
|
| 46 |
}: GuessDisplayProps) => {
|
| 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(() => {
|
| 55 |
-
const saveGameResult = async () => {
|
| 56 |
-
try {
|
| 57 |
-
const { error } = await supabase
|
| 58 |
-
.from('game_results')
|
| 59 |
-
.insert({
|
| 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) {
|
| 68 |
-
console.error('Error saving game result:', error);
|
| 69 |
-
} else {
|
| 70 |
-
console.log('Game result saved successfully');
|
| 71 |
-
}
|
| 72 |
-
} catch (error) {
|
| 73 |
-
console.error('Error in saveGameResult:', error);
|
| 74 |
-
}
|
| 75 |
-
};
|
| 76 |
-
|
| 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 |
-
<
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 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 |
-
|
| 137 |
-
<div className="space-y-2">
|
| 138 |
-
<p className="text-sm text-gray-600">{t.guess.providedDescription}</p>
|
| 139 |
-
<div className="rounded-lg bg-gray-50">
|
| 140 |
-
<p className="p-4 text-2xl tracking-wider text-gray-800">
|
| 141 |
-
{sentence.join(" ")}
|
| 142 |
-
</p>
|
| 143 |
-
</div>
|
| 144 |
-
</div>
|
| 145 |
-
|
| 146 |
-
<div className="space-y-2">
|
| 147 |
-
<p className="text-sm text-gray-600">
|
| 148 |
-
{isCheating() ? t.guess.cheatingDetected : t.guess.aiGuessedDescription}
|
| 149 |
-
</p>
|
| 150 |
-
<div className={`rounded-lg ${isGuessCorrect() ? 'bg-green-50' : 'bg-red-50'}`}>
|
| 151 |
-
<p className={`p-4 text-2xl font-bold tracking-wider ${isGuessCorrect() ? 'text-green-600' : 'text-red-600'}`}>
|
| 152 |
-
{aiGuess}
|
| 153 |
-
</p>
|
| 154 |
-
</div>
|
| 155 |
-
</div>
|
| 156 |
|
| 157 |
-
<
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
>
|
| 163 |
-
{t.guess.nextRound} ⏎
|
| 164 |
-
</Button>
|
| 165 |
-
) : (
|
| 166 |
-
<>
|
| 167 |
-
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
| 168 |
-
<DialogTrigger asChild>
|
| 169 |
-
<Button
|
| 170 |
-
className="w-full bg-secondary text-lg hover:bg-secondary/90"
|
| 171 |
-
>
|
| 172 |
-
{t.guess.viewLeaderboard} 🏆
|
| 173 |
-
</Button>
|
| 174 |
-
</DialogTrigger>
|
| 175 |
-
<DialogContent className="max-w-md bg-white">
|
| 176 |
-
<HighScoreBoard
|
| 177 |
-
currentScore={currentScore}
|
| 178 |
-
avgWordsPerRound={avgWordsPerRound}
|
| 179 |
-
onClose={() => setIsDialogOpen(false)}
|
| 180 |
-
onPlayAgain={() => {
|
| 181 |
-
setIsDialogOpen(false);
|
| 182 |
-
onPlayAgain();
|
| 183 |
-
}}
|
| 184 |
-
sessionId={sessionId}
|
| 185 |
-
onScoreSubmitted={handleScoreSubmitted}
|
| 186 |
-
/>
|
| 187 |
-
</DialogContent>
|
| 188 |
-
</Dialog>
|
| 189 |
-
<Button
|
| 190 |
-
onClick={onPlayAgain}
|
| 191 |
-
className="w-full bg-primary text-lg hover:bg-primary/90"
|
| 192 |
-
>
|
| 193 |
-
{t.guess.playAgain} ⏎
|
| 194 |
-
</Button>
|
| 195 |
-
</>
|
| 196 |
-
)}
|
| 197 |
-
</div>
|
| 198 |
|
| 199 |
-
<
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 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 |
};
|
|
|
|
|
|
|
| 1 |
import { motion } from "framer-motion";
|
| 2 |
+
import { useState } from "react";
|
| 3 |
+
import { useTranslation } from "@/hooks/useTranslation";
|
| 4 |
+
import { Button } from "@/components/ui/button";
|
| 5 |
import {
|
| 6 |
Dialog,
|
| 7 |
DialogContent,
|
| 8 |
DialogTrigger,
|
| 9 |
} from "@/components/ui/dialog";
|
| 10 |
+
import { RoundHeader } from "./sentence-builder/RoundHeader";
|
| 11 |
+
import { WordDisplay } from "./sentence-builder/WordDisplay";
|
| 12 |
+
import { GuessDescription } from "./guess-display/GuessDescription";
|
| 13 |
+
import { GuessResult } from "./guess-display/GuessResult";
|
| 14 |
+
import { ActionButtons } from "./guess-display/ActionButtons";
|
| 15 |
import { HighScoreBoard } from "@/components/HighScoreBoard";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
|
| 17 |
interface GuessDisplayProps {
|
| 18 |
sentence: string[];
|
|
|
|
| 37 |
avgWordsPerRound,
|
| 38 |
sessionId,
|
| 39 |
}: GuessDisplayProps) => {
|
|
|
|
|
|
|
|
|
|
| 40 |
const [showConfirmDialog, setShowConfirmDialog] = useState(false);
|
| 41 |
const [hasSubmittedScore, setHasSubmittedScore] = useState(false);
|
| 42 |
const t = useTranslation();
|
| 43 |
+
const isGuessCorrect = () => aiGuess.toLowerCase() === currentWord.toLowerCase();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
|
| 45 |
const handleScoreSubmitted = () => {
|
| 46 |
console.log('Score submitted, updating state');
|
| 47 |
setHasSubmittedScore(true);
|
| 48 |
};
|
| 49 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
return (
|
| 51 |
<motion.div
|
| 52 |
initial={{ opacity: 0 }}
|
| 53 |
animate={{ opacity: 1 }}
|
| 54 |
className="text-center relative space-y-6"
|
| 55 |
>
|
| 56 |
+
<RoundHeader
|
| 57 |
+
successfulRounds={currentScore}
|
| 58 |
+
goToWelcomeScreen={onBack}
|
| 59 |
+
showConfirmDialog={showConfirmDialog}
|
| 60 |
+
setShowConfirmDialog={setShowConfirmDialog}
|
| 61 |
+
/>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
|
| 63 |
+
<WordDisplay currentWord={currentWord} />
|
| 64 |
+
|
| 65 |
+
<GuessDescription sentence={sentence} aiGuess={aiGuess} />
|
| 66 |
+
|
| 67 |
+
<GuessResult aiGuess={aiGuess} isCorrect={isGuessCorrect()} />
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
|
| 69 |
+
<ActionButtons
|
| 70 |
+
isCorrect={isGuessCorrect()}
|
| 71 |
+
onNextRound={onNextRound}
|
| 72 |
+
onPlayAgain={onPlayAgain}
|
| 73 |
+
currentScore={currentScore}
|
| 74 |
+
avgWordsPerRound={avgWordsPerRound}
|
| 75 |
+
sessionId={sessionId}
|
| 76 |
+
onScoreSubmitted={handleScoreSubmitted}
|
| 77 |
+
/>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
</motion.div>
|
| 79 |
);
|
| 80 |
};
|
src/components/game/SentenceBuilder.tsx
CHANGED
|
@@ -67,7 +67,7 @@ export const SentenceBuilder = ({
|
|
| 67 |
>
|
| 68 |
<RoundHeader
|
| 69 |
successfulRounds={successfulRounds}
|
| 70 |
-
|
| 71 |
showConfirmDialog={showConfirmDialog}
|
| 72 |
setShowConfirmDialog={setShowConfirmDialog}
|
| 73 |
/>
|
|
@@ -85,6 +85,7 @@ export const SentenceBuilder = ({
|
|
| 85 |
hasMultipleWords={hasMultipleWords}
|
| 86 |
containsTargetWord={containsTargetWord}
|
| 87 |
isValidInput={isValidInput}
|
|
|
|
| 88 |
/>
|
| 89 |
|
| 90 |
<AlertDialog open={showConfirmDialog} onOpenChange={setShowConfirmDialog}>
|
|
|
|
| 67 |
>
|
| 68 |
<RoundHeader
|
| 69 |
successfulRounds={successfulRounds}
|
| 70 |
+
goToWelcomeScreen={onBack}
|
| 71 |
showConfirmDialog={showConfirmDialog}
|
| 72 |
setShowConfirmDialog={setShowConfirmDialog}
|
| 73 |
/>
|
|
|
|
| 85 |
hasMultipleWords={hasMultipleWords}
|
| 86 |
containsTargetWord={containsTargetWord}
|
| 87 |
isValidInput={isValidInput}
|
| 88 |
+
sentence={sentence}
|
| 89 |
/>
|
| 90 |
|
| 91 |
<AlertDialog open={showConfirmDialog} onOpenChange={setShowConfirmDialog}>
|
src/components/game/guess-display/ActionButtons.tsx
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Button } from "@/components/ui/button";
|
| 2 |
+
import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
|
| 3 |
+
import { HighScoreBoard } from "@/components/HighScoreBoard";
|
| 4 |
+
import { useState } from "react";
|
| 5 |
+
import { useTranslation } from "@/hooks/useTranslation";
|
| 6 |
+
|
| 7 |
+
interface ActionButtonsProps {
|
| 8 |
+
isCorrect: boolean;
|
| 9 |
+
onNextRound: () => void;
|
| 10 |
+
onPlayAgain: () => void;
|
| 11 |
+
currentScore: number;
|
| 12 |
+
avgWordsPerRound: number;
|
| 13 |
+
sessionId: string;
|
| 14 |
+
onScoreSubmitted: () => void;
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
export const ActionButtons = ({
|
| 18 |
+
isCorrect,
|
| 19 |
+
onNextRound,
|
| 20 |
+
onPlayAgain,
|
| 21 |
+
currentScore,
|
| 22 |
+
avgWordsPerRound,
|
| 23 |
+
sessionId,
|
| 24 |
+
onScoreSubmitted,
|
| 25 |
+
}: ActionButtonsProps) => {
|
| 26 |
+
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
| 27 |
+
const t = useTranslation();
|
| 28 |
+
|
| 29 |
+
return (
|
| 30 |
+
<div className="flex flex-col gap-4">
|
| 31 |
+
{isCorrect ? (
|
| 32 |
+
<Button
|
| 33 |
+
onClick={onNextRound}
|
| 34 |
+
className="w-full bg-primary text-lg hover:bg-primary/90"
|
| 35 |
+
>
|
| 36 |
+
{t.guess.nextRound} ⏎
|
| 37 |
+
</Button>
|
| 38 |
+
) : (
|
| 39 |
+
<>
|
| 40 |
+
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
| 41 |
+
<DialogTrigger asChild>
|
| 42 |
+
<Button
|
| 43 |
+
className="w-full bg-secondary text-lg hover:bg-secondary/90"
|
| 44 |
+
>
|
| 45 |
+
{t.guess.viewLeaderboard} 🏆
|
| 46 |
+
</Button>
|
| 47 |
+
</DialogTrigger>
|
| 48 |
+
<DialogContent className="max-w-md bg-white">
|
| 49 |
+
<HighScoreBoard
|
| 50 |
+
currentScore={currentScore}
|
| 51 |
+
avgWordsPerRound={avgWordsPerRound}
|
| 52 |
+
onClose={() => setIsDialogOpen(false)}
|
| 53 |
+
onPlayAgain={() => {
|
| 54 |
+
setIsDialogOpen(false);
|
| 55 |
+
onPlayAgain();
|
| 56 |
+
}}
|
| 57 |
+
sessionId={sessionId}
|
| 58 |
+
onScoreSubmitted={onScoreSubmitted}
|
| 59 |
+
/>
|
| 60 |
+
</DialogContent>
|
| 61 |
+
</Dialog>
|
| 62 |
+
<Button
|
| 63 |
+
onClick={onPlayAgain}
|
| 64 |
+
className="w-full bg-primary text-lg hover:bg-primary/90"
|
| 65 |
+
>
|
| 66 |
+
{t.guess.playAgain} ⏎
|
| 67 |
+
</Button>
|
| 68 |
+
</>
|
| 69 |
+
)}
|
| 70 |
+
</div>
|
| 71 |
+
);
|
| 72 |
+
};
|
src/components/game/guess-display/GuessDescription.tsx
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useTranslation } from "@/hooks/useTranslation";
|
| 2 |
+
|
| 3 |
+
interface GuessDescriptionProps {
|
| 4 |
+
sentence: string[];
|
| 5 |
+
aiGuess: string;
|
| 6 |
+
}
|
| 7 |
+
|
| 8 |
+
export const GuessDescription = ({ sentence, aiGuess }: GuessDescriptionProps) => {
|
| 9 |
+
const t = useTranslation();
|
| 10 |
+
const isCheating = () => aiGuess === 'CHEATING';
|
| 11 |
+
|
| 12 |
+
return (
|
| 13 |
+
<div className="space-y-2">
|
| 14 |
+
<p className="text-sm text-gray-600">{t.guess.providedDescription}</p>
|
| 15 |
+
<div className="rounded-lg bg-gray-50">
|
| 16 |
+
<p className="p-4 text-2xl tracking-wider text-gray-800">
|
| 17 |
+
{sentence.join(" ")}
|
| 18 |
+
</p>
|
| 19 |
+
</div>
|
| 20 |
+
|
| 21 |
+
<p className="text-sm text-gray-600">
|
| 22 |
+
{isCheating() ? t.guess.cheatingDetected : t.guess.aiGuessedDescription}
|
| 23 |
+
</p>
|
| 24 |
+
</div>
|
| 25 |
+
);
|
| 26 |
+
};
|
src/components/game/guess-display/GuessResult.tsx
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
interface GuessResultProps {
|
| 2 |
+
aiGuess: string;
|
| 3 |
+
isCorrect: boolean;
|
| 4 |
+
}
|
| 5 |
+
|
| 6 |
+
export const GuessResult = ({ aiGuess, isCorrect }: GuessResultProps) => {
|
| 7 |
+
return (
|
| 8 |
+
<div className={`rounded-lg ${isCorrect ? 'bg-green-50' : 'bg-red-50'}`}>
|
| 9 |
+
<p className={`p-4 text-2xl font-bold tracking-wider ${isCorrect ? 'text-green-600' : 'text-red-600'}`}>
|
| 10 |
+
{aiGuess}
|
| 11 |
+
</p>
|
| 12 |
+
</div>
|
| 13 |
+
);
|
| 14 |
+
};
|
src/components/game/sentence-builder/InputForm.tsx
CHANGED
|
@@ -12,6 +12,7 @@ interface InputFormProps {
|
|
| 12 |
hasMultipleWords: boolean;
|
| 13 |
containsTargetWord: boolean;
|
| 14 |
isValidInput: boolean;
|
|
|
|
| 15 |
}
|
| 16 |
|
| 17 |
export const InputForm = ({
|
|
@@ -22,16 +23,20 @@ export const InputForm = ({
|
|
| 22 |
isAiThinking,
|
| 23 |
hasMultipleWords,
|
| 24 |
containsTargetWord,
|
| 25 |
-
isValidInput
|
|
|
|
| 26 |
}: InputFormProps) => {
|
| 27 |
const inputRef = useRef<HTMLInputElement>(null);
|
| 28 |
const t = useTranslation();
|
| 29 |
|
|
|
|
| 30 |
useEffect(() => {
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
|
|
|
|
|
|
| 35 |
|
| 36 |
const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
|
| 37 |
if (e.shiftKey && e.key === 'Enter') {
|
|
@@ -51,6 +56,10 @@ export const InputForm = ({
|
|
| 51 |
|
| 52 |
const error = getInputError();
|
| 53 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
return (
|
| 55 |
<form onSubmit={onSubmitWord} className="mb-4">
|
| 56 |
<div className="relative mb-4">
|
|
@@ -82,7 +91,7 @@ export const InputForm = ({
|
|
| 82 |
type="button"
|
| 83 |
onClick={onMakeGuess}
|
| 84 |
className="flex-1 bg-secondary text-lg hover:bg-secondary/90"
|
| 85 |
-
disabled={
|
| 86 |
>
|
| 87 |
{isAiThinking ? t.game.aiThinking : `${t.game.makeGuess} ⇧⏎`}
|
| 88 |
</Button>
|
|
|
|
| 12 |
hasMultipleWords: boolean;
|
| 13 |
containsTargetWord: boolean;
|
| 14 |
isValidInput: boolean;
|
| 15 |
+
sentence: string[];
|
| 16 |
}
|
| 17 |
|
| 18 |
export const InputForm = ({
|
|
|
|
| 23 |
isAiThinking,
|
| 24 |
hasMultipleWords,
|
| 25 |
containsTargetWord,
|
| 26 |
+
isValidInput,
|
| 27 |
+
sentence
|
| 28 |
}: InputFormProps) => {
|
| 29 |
const inputRef = useRef<HTMLInputElement>(null);
|
| 30 |
const t = useTranslation();
|
| 31 |
|
| 32 |
+
// Focus input on mount and after AI response
|
| 33 |
useEffect(() => {
|
| 34 |
+
if (!isAiThinking) {
|
| 35 |
+
setTimeout(() => {
|
| 36 |
+
inputRef.current?.focus();
|
| 37 |
+
}, 100);
|
| 38 |
+
}
|
| 39 |
+
}, [isAiThinking]);
|
| 40 |
|
| 41 |
const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
|
| 42 |
if (e.shiftKey && e.key === 'Enter') {
|
|
|
|
| 56 |
|
| 57 |
const error = getInputError();
|
| 58 |
|
| 59 |
+
// Check if there's either something in the sentence or in the input box
|
| 60 |
+
const canMakeGuess = (sentence.length > 0 || playerInput.trim().length > 0) &&
|
| 61 |
+
!hasMultipleWords && !containsTargetWord && isValidInput && !isAiThinking;
|
| 62 |
+
|
| 63 |
return (
|
| 64 |
<form onSubmit={onSubmitWord} className="mb-4">
|
| 65 |
<div className="relative mb-4">
|
|
|
|
| 91 |
type="button"
|
| 92 |
onClick={onMakeGuess}
|
| 93 |
className="flex-1 bg-secondary text-lg hover:bg-secondary/90"
|
| 94 |
+
disabled={!canMakeGuess}
|
| 95 |
>
|
| 96 |
{isAiThinking ? t.game.aiThinking : `${t.game.makeGuess} ⇧⏎`}
|
| 97 |
</Button>
|
src/components/game/sentence-builder/RoundHeader.tsx
CHANGED
|
@@ -4,14 +4,14 @@ import { useTranslation } from "@/hooks/useTranslation";
|
|
| 4 |
|
| 5 |
interface RoundHeaderProps {
|
| 6 |
successfulRounds: number;
|
| 7 |
-
|
| 8 |
showConfirmDialog: boolean;
|
| 9 |
setShowConfirmDialog: (show: boolean) => void;
|
| 10 |
}
|
| 11 |
|
| 12 |
export const RoundHeader = ({
|
| 13 |
successfulRounds,
|
| 14 |
-
|
| 15 |
showConfirmDialog,
|
| 16 |
setShowConfirmDialog
|
| 17 |
}: RoundHeaderProps) => {
|
|
@@ -21,7 +21,7 @@ export const RoundHeader = ({
|
|
| 21 |
if (successfulRounds > 0) {
|
| 22 |
setShowConfirmDialog(true);
|
| 23 |
} else {
|
| 24 |
-
|
| 25 |
}
|
| 26 |
};
|
| 27 |
|
|
|
|
| 4 |
|
| 5 |
interface RoundHeaderProps {
|
| 6 |
successfulRounds: number;
|
| 7 |
+
goToWelcomeScreen?: () => void;
|
| 8 |
showConfirmDialog: boolean;
|
| 9 |
setShowConfirmDialog: (show: boolean) => void;
|
| 10 |
}
|
| 11 |
|
| 12 |
export const RoundHeader = ({
|
| 13 |
successfulRounds,
|
| 14 |
+
goToWelcomeScreen,
|
| 15 |
showConfirmDialog,
|
| 16 |
setShowConfirmDialog
|
| 17 |
}: RoundHeaderProps) => {
|
|
|
|
| 21 |
if (successfulRounds > 0) {
|
| 22 |
setShowConfirmDialog(true);
|
| 23 |
} else {
|
| 24 |
+
goToWelcomeScreen?.();
|
| 25 |
}
|
| 26 |
};
|
| 27 |
|
src/services/mistralService.ts
CHANGED
|
@@ -31,41 +31,15 @@ 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 |
-
// 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 |
-
|
| 42 |
-
try {
|
| 43 |
-
const { data: fraudData, error: fraudError } = await supabase.functions.invoke('detect-fraud', {
|
| 44 |
-
body: {
|
| 45 |
-
sentence,
|
| 46 |
-
targetWord,
|
| 47 |
-
language
|
| 48 |
-
}
|
| 49 |
-
});
|
| 50 |
-
|
| 51 |
-
if (fraudError) throw fraudError;
|
| 52 |
-
|
| 53 |
-
if (fraudData?.verdict === 'cheating') {
|
| 54 |
-
console.log('Fraud detected!');
|
| 55 |
-
return 'CHEATING';
|
| 56 |
-
}
|
| 57 |
-
} catch (error) {
|
| 58 |
-
console.error('Error in fraud detection:', error);
|
| 59 |
-
// Continue with normal guessing if fraud detection fails
|
| 60 |
-
}
|
| 61 |
-
}
|
| 62 |
-
|
| 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,
|
| 69 |
language
|
| 70 |
}
|
| 71 |
});
|
|
|
|
| 31 |
export const guessWord = async (sentence: string, language: string): Promise<string> => {
|
| 32 |
console.log('Processing guess for sentence:', sentence);
|
| 33 |
|
|
|
|
| 34 |
const words = sentence.trim().split(/\s+/);
|
| 35 |
const targetWord = words[0].toLowerCase();
|
| 36 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
console.log('Calling guess-word function with sentence:', sentence, 'language:', language);
|
| 38 |
|
| 39 |
const { data, error } = await supabase.functions.invoke('guess-word', {
|
| 40 |
body: {
|
| 41 |
sentence,
|
| 42 |
+
targetWord,
|
| 43 |
language
|
| 44 |
}
|
| 45 |
});
|
supabase/functions/detect-fraud/index.ts
DELETED
|
@@ -1,145 +0,0 @@
|
|
| 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 |
-
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') {
|
| 14 |
-
return new Response(null, { headers: corsHeaders });
|
| 15 |
-
}
|
| 16 |
-
|
| 17 |
-
try {
|
| 18 |
-
const { sentence, targetWord, language } = await req.json();
|
| 19 |
-
console.log('Checking for fraud:', { sentence, targetWord, language });
|
| 20 |
-
|
| 21 |
-
const client = new Mistral({
|
| 22 |
-
apiKey: Deno.env.get('MISTRAL_API_KEY'),
|
| 23 |
-
});
|
| 24 |
-
|
| 25 |
-
const maxRetries = 3;
|
| 26 |
-
let retryCount = 0;
|
| 27 |
-
let lastError = null;
|
| 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 |
-
{
|
| 81 |
-
role: "user",
|
| 82 |
-
content: `Target word: "${targetWord}"
|
| 83 |
-
Player's description: "${sentence}"
|
| 84 |
-
Language: ${language}
|
| 85 |
-
|
| 86 |
-
Is this a legitimate description or an attempt to cheat?`
|
| 87 |
-
}
|
| 88 |
-
],
|
| 89 |
-
maxTokens: 20,
|
| 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 |
-
|
| 100 |
-
return new Response(
|
| 101 |
-
JSON.stringify({ verdict }),
|
| 102 |
-
{ headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
| 103 |
-
);
|
| 104 |
-
} catch (error) {
|
| 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 |
-
|
| 134 |
-
return new Response(
|
| 135 |
-
JSON.stringify({
|
| 136 |
-
error: errorMessage,
|
| 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 |
-
);
|
| 144 |
-
}
|
| 145 |
-
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|