From 6c6a9cf4ad91786438f68bda10cc0fa01518590c Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Mon, 8 Dec 2025 12:19:53 +0000 Subject: [PATCH] Changes --- src/components/HumanVerification.tsx | 258 +++++++++++++++++++-------- src/contexts/AchievementsContext.tsx | 23 +++ src/contexts/SettingsContext.tsx | 72 +++++++- src/pages/AIChat.tsx | 6 + src/pages/Index.tsx | 51 +++++- 5 files changed, 324 insertions(+), 86 deletions(-) diff --git a/src/components/HumanVerification.tsx b/src/components/HumanVerification.tsx index ea2a565..01b2cd4 100644 --- a/src/components/HumanVerification.tsx +++ b/src/components/HumanVerification.tsx @@ -1,25 +1,5 @@ -import { useState, useEffect } from 'react'; +import { useState, useEffect, useRef } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; -import GlitchText from './GlitchText'; - -// Hacker-themed questions that any human would know -const QUESTIONS = [ - { q: "What key do you press to open a terminal? (Hint: between D and G)", answers: ["f", "f key"] }, - { q: "Complete: The Matrix has you, ___", answers: ["neo"] }, - { q: "What does 'www' stand for in a URL?", answers: ["world wide web", "worldwideweb"] }, - { q: "What color is the Matrix rain?", answers: ["green"] }, - { q: "What does 'ctrl+c' do?", answers: ["copy", "cancel", "stop", "interrupt"] }, - { q: "What number comes after 0 in binary?", answers: ["1", "one"] }, - { q: "What does 'IP' stand for in networking?", answers: ["internet protocol"] }, - { q: "What key exits most programs? (3 letters)", answers: ["esc", "escape"] }, - { q: "Red pill or blue pill - which reveals the truth?", answers: ["red", "red pill"] }, - { q: "What symbol starts most terminal commands?", answers: ["$", "/", ">", "dollar", "slash"] }, - { q: "What does 'USB' stand for?", answers: ["universal serial bus"] }, - { q: "What is localhost's IP address?", answers: ["127.0.0.1", "localhost"] }, - { q: "Complete: Hello, _____ (classic first program output)", answers: ["world"] }, - { q: "What animal is Linux's mascot?", answers: ["penguin", "tux"] }, - { q: "What does 'CPU' stand for?", answers: ["central processing unit"] }, -]; const BYPASS_KEY = 'bypass-human-check'; const VERIFIED_KEY = 'human-verified'; @@ -28,11 +8,40 @@ interface HumanVerificationProps { onVerified: () => void; } +// Generate a simple math equation +const generateEquation = () => { + const operators = ['+', '-', '*'] as const; + const operator = operators[Math.floor(Math.random() * operators.length)]; + + let a: number, b: number, answer: number; + + switch (operator) { + case '+': + a = Math.floor(Math.random() * 50) + 1; + b = Math.floor(Math.random() * 50) + 1; + answer = a + b; + break; + case '-': + a = Math.floor(Math.random() * 50) + 20; + b = Math.floor(Math.random() * (a - 1)) + 1; + answer = a - b; + break; + case '*': + a = Math.floor(Math.random() * 12) + 2; + b = Math.floor(Math.random() * 12) + 2; + answer = a * b; + break; + } + + return { equation: `${a} ${operator} ${b}`, answer }; +}; + const HumanVerification = ({ onVerified }: HumanVerificationProps) => { - const [question] = useState(() => QUESTIONS[Math.floor(Math.random() * QUESTIONS.length)]); - const [answer, setAnswer] = useState(''); + const [{ equation, answer }, setEquation] = useState(generateEquation); + const [userAnswer, setUserAnswer] = useState(''); const [error, setError] = useState(false); - const [showHint, setShowHint] = useState(false); + const [attempts, setAttempts] = useState(0); + const canvasRef = useRef(null); // Check for bypass in URL useEffect(() => { @@ -43,101 +52,208 @@ const HumanVerification = ({ onVerified }: HumanVerificationProps) => { } }, [onVerified]); + // Draw equation on canvas for added security + useEffect(() => { + const canvas = canvasRef.current; + if (!canvas) return; + + const ctx = canvas.getContext('2d'); + if (!ctx) return; + + // Get computed styles for theming + const computedStyle = getComputedStyle(document.documentElement); + const primaryColor = computedStyle.getPropertyValue('--primary').trim(); + const hslMatch = primaryColor.match(/[\d.]+/g); + const primaryRGB = hslMatch + ? `hsl(${hslMatch[0]}, ${hslMatch[1]}%, ${hslMatch[2]}%)` + : '#00ff00'; + + // Clear canvas + ctx.fillStyle = 'rgba(0, 0, 0, 0.8)'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + // Draw grid lines for terminal feel + ctx.strokeStyle = 'rgba(0, 255, 0, 0.1)'; + ctx.lineWidth = 1; + for (let i = 0; i < canvas.width; i += 20) { + ctx.beginPath(); + ctx.moveTo(i, 0); + ctx.lineTo(i, canvas.height); + ctx.stroke(); + } + for (let i = 0; i < canvas.height; i += 20) { + ctx.beginPath(); + ctx.moveTo(0, i); + ctx.lineTo(canvas.width, i); + ctx.stroke(); + } + + // Draw border + ctx.strokeStyle = primaryRGB; + ctx.lineWidth = 2; + ctx.strokeRect(2, 2, canvas.width - 4, canvas.height - 4); + + // Draw equation with slight random positioning for anti-bot + const offsetX = Math.random() * 10 - 5; + const offsetY = Math.random() * 6 - 3; + + ctx.font = 'bold 48px monospace'; + ctx.fillStyle = primaryRGB; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + + // Add subtle noise/distortion + const chars = equation.split(''); + let xPos = canvas.width / 2 - (chars.length * 15) + offsetX; + + chars.forEach((char) => { + const yOffset = Math.random() * 4 - 2; + ctx.fillText(char, xPos, canvas.height / 2 + offsetY + yOffset); + xPos += 30; + }); + + // Draw "= ?" at the end + ctx.fillText('= ?', xPos + 20, canvas.height / 2 + offsetY); + + // Add scanline effect + for (let y = 0; y < canvas.height; y += 3) { + ctx.fillStyle = 'rgba(0, 0, 0, 0.15)'; + ctx.fillRect(0, y, canvas.width, 1); + } + }, [equation]); + const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); - const normalizedAnswer = answer.toLowerCase().trim(); + const parsed = parseInt(userAnswer, 10); - if (question.answers.some(a => normalizedAnswer === a.toLowerCase())) { + if (!isNaN(parsed) && parsed === answer) { localStorage.setItem(VERIFIED_KEY, 'true'); onVerified(); } else { setError(true); - setShowHint(true); - setTimeout(() => setError(false), 500); + setAttempts(prev => prev + 1); + setTimeout(() => { + setError(false); + // Generate new equation after 3 failed attempts + if (attempts >= 2) { + setEquation(generateEquation()); + setAttempts(0); + } + }, 600); + setUserAnswer(''); } }; + const handleNewEquation = () => { + setEquation(generateEquation()); + setUserAnswer(''); + setError(false); + }; + return ( -
+
- {/* ASCII Art Header */} -
-{`╔═══════════════════════════════════════╗
-║     HUMAN VERIFICATION REQUIRED       ║
-╚═══════════════════════════════════════╝`}
-          
- + {/* Header */}
- -

- Prove you are human to continue +

+{`┌─────────────────────────────────────┐
+│     SECURITY VERIFICATION v2.0      │
+│         ANTI-BOT PROTOCOL           │
+└─────────────────────────────────────┘`}
+            
+

+ {'>'} Solve the equation to verify humanity

-
-
-

> QUERY:

-

{question.q}

-
+ {/* Canvas with equation */} +
+ +
-
+ {/* Input form */} + +
+ + {'>'}_ + setAnswer(e.target.value)} - placeholder="> Enter response..." + inputMode="numeric" + pattern="-?[0-9]*" + value={userAnswer} + onChange={(e) => setUserAnswer(e.target.value.replace(/[^0-9-]/g, ''))} + placeholder="Enter answer" autoFocus className={`w-full bg-background border-2 ${ error ? 'border-destructive animate-pulse' : 'border-primary/50' - } p-3 font-pixel text-sm text-foreground placeholder:text-foreground/30 focus:outline-none focus:border-primary transition-colors`} + } p-3 pl-12 font-mono text-lg text-foreground placeholder:text-foreground/30 focus:outline-none focus:border-primary transition-colors`} />
- {showHint && ( - - ERROR: Invalid response. Retry required. - + + INCORRECT - {3 - attempts} attempts remaining + )} - +
+ + +
-
-

- // Anti-bot verification protocol v1.0 + {/* Footer */} +

+
+ Protocol: MATH-VERIFY-2.0 + Encryption: ACTIVE +
+

+ // This verification helps protect against automated access

- {/* Decorative terminal lines */} -
-

> Awaiting human verification...

-

> _

+ {/* Terminal decoration */} +
+

{'>'} Awaiting verification input...

+

{'>'} _

@@ -145,4 +261,4 @@ const HumanVerification = ({ onVerified }: HumanVerificationProps) => { }; export default HumanVerification; -export { VERIFIED_KEY, BYPASS_KEY }; +export { VERIFIED_KEY, BYPASS_KEY }; \ No newline at end of file diff --git a/src/contexts/AchievementsContext.tsx b/src/contexts/AchievementsContext.tsx index 19db60b..f96f762 100644 --- a/src/contexts/AchievementsContext.tsx +++ b/src/contexts/AchievementsContext.tsx @@ -11,12 +11,27 @@ export interface Achievement { secret?: boolean; } +// Track AI message count in localStorage +const AI_MESSAGE_COUNT_KEY = 'ai-message-count'; + +export const incrementAiMessageCount = (): number => { + const current = parseInt(localStorage.getItem(AI_MESSAGE_COUNT_KEY) || '0', 10); + const newCount = current + 1; + localStorage.setItem(AI_MESSAGE_COUNT_KEY, newCount.toString()); + return newCount; +}; + +export const getAiMessageCount = (): number => { + return parseInt(localStorage.getItem(AI_MESSAGE_COUNT_KEY) || '0', 10); +}; + interface AchievementsContextType { achievements: Achievement[]; unlockAchievement: (id: string) => void; getUnlockedCount: () => number; getTotalCount: () => number; timeOnSite: number; + checkAiAchievements: () => void; } const defaultAchievements: Achievement[] = [ @@ -214,6 +229,13 @@ export const AchievementsProvider = ({ children }: { children: ReactNode }) => { return achievements.length; }, [achievements]); + // Check AI message achievements + const checkAiAchievements = useCallback(() => { + const count = getAiMessageCount(); + if (count >= 5) unlockAchievement('ai_conversation'); + if (count >= 20) unlockAchievement('ai_long_chat'); + }, [unlockAchievement]); + // Save achievements useEffect(() => { localStorage.setItem('achievements', JSON.stringify(achievements)); @@ -227,6 +249,7 @@ export const AchievementsProvider = ({ children }: { children: ReactNode }) => { getUnlockedCount, getTotalCount, timeOnSite, + checkAiAchievements, }} > {children} diff --git a/src/contexts/SettingsContext.tsx b/src/contexts/SettingsContext.tsx index ca4a230..a9805d5 100644 --- a/src/contexts/SettingsContext.tsx +++ b/src/contexts/SettingsContext.tsx @@ -7,7 +7,7 @@ interface SettingsContextType { setCrtEnabled: (enabled: boolean) => void; soundEnabled: boolean; setSoundEnabled: (enabled: boolean) => void; - cryptoConsent: boolean; + cryptoConsent: boolean | null; // null = never asked this session setCryptoConsent: (consent: boolean) => void; playSound: (type: SoundType) => void; hashrate: number; @@ -16,6 +16,8 @@ interface SettingsContextType { setTotalHashes: (hashes: number) => void; acceptedHashes: number; setAcceptedHashes: (hashes: number) => void; + audioBlocked: boolean; + resetAudioContext: () => void; } const SettingsContext = createContext(undefined); @@ -31,11 +33,30 @@ export const SettingsProvider = ({ children }: { children: ReactNode }) => { return saved !== null ? JSON.parse(saved) : true; }); - const [cryptoConsent, setCryptoConsent] = useState(() => { + // Crypto consent: null = needs prompt, true = accepted, false = declined + // On session start, if user previously declined, we reset to null to re-prompt + const [cryptoConsent, setCryptoConsentState] = useState(() => { const saved = localStorage.getItem('cryptoConsent'); - return saved !== null ? JSON.parse(saved) : false; + if (saved === null) return null; // Never set + const parsed = JSON.parse(saved); + // If declined (false), return null to re-prompt on new session + // We use sessionStorage to track if we've already shown the prompt this session + const sessionPrompted = sessionStorage.getItem('cryptoConsentPrompted'); + if (parsed === false && !sessionPrompted) { + return null; // Re-prompt + } + return parsed; }); + const setCryptoConsent = (consent: boolean) => { + setCryptoConsentState(consent); + localStorage.setItem('cryptoConsent', JSON.stringify(consent)); + sessionStorage.setItem('cryptoConsentPrompted', 'true'); + }; + + const [audioBlocked, setAudioBlocked] = useState(false); + const [audioFailCount, setAudioFailCount] = useState(0); + const [hashrate, setHashrate] = useState(0); const [totalHashes, setTotalHashes] = useState(0); const [acceptedHashes, setAcceptedHashes] = useState(0); @@ -81,9 +102,15 @@ export const SettingsProvider = ({ children }: { children: ReactNode }) => { localStorage.setItem('soundEnabled', JSON.stringify(soundEnabled)); }, [soundEnabled]); - useEffect(() => { - localStorage.setItem('cryptoConsent', JSON.stringify(cryptoConsent)); - }, [cryptoConsent]); + // Reset audio context to try again after user interaction + const resetAudioContext = useCallback(() => { + if (audioContextRef.current) { + audioContextRef.current.close(); + audioContextRef.current = null; + } + setAudioBlocked(false); + setAudioFailCount(0); + }, []); const playSound = useCallback((type: SoundType) => { // Use ref to ensure we always have the latest value @@ -91,6 +118,21 @@ export const SettingsProvider = ({ children }: { children: ReactNode }) => { try { const audioContext = getAudioContext(); + + // Check if context is blocked + if (audioContext.state === 'suspended') { + audioContext.resume().catch(() => { + setAudioFailCount(prev => { + const newCount = prev + 1; + if (newCount >= 3) { + setAudioBlocked(true); + } + return newCount; + }); + }); + return; + } + const oscillator = audioContext.createOscillator(); const gainNode = audioContext.createGain(); @@ -142,11 +184,23 @@ export const SettingsProvider = ({ children }: { children: ReactNode }) => { oscillator.stop(now + 0.15); break; } + + // Reset fail count on success + if (audioFailCount > 0) { + setAudioFailCount(0); + setAudioBlocked(false); + } } catch (e) { - // Silently fail if audio context has issues console.warn('Audio playback failed:', e); + setAudioFailCount(prev => { + const newCount = prev + 1; + if (newCount >= 3) { + setAudioBlocked(true); + } + return newCount; + }); } - }, [getAudioContext]); + }, [getAudioContext, audioFailCount]); return ( { setTotalHashes, acceptedHashes, setAcceptedHashes, + audioBlocked, + resetAudioContext, }} > {children} diff --git a/src/pages/AIChat.tsx b/src/pages/AIChat.tsx index 3601aaa..fad3735 100644 --- a/src/pages/AIChat.tsx +++ b/src/pages/AIChat.tsx @@ -5,6 +5,7 @@ import { Button } from '@/components/ui/button'; import { Textarea } from '@/components/ui/textarea'; import { ScrollArea } from '@/components/ui/scroll-area'; import { useSettings } from '@/contexts/SettingsContext'; +import { useAchievements, incrementAiMessageCount } from '@/contexts/AchievementsContext'; import { useToast } from '@/hooks/use-toast'; import GlitchText from '@/components/GlitchText'; import MessageContent from '@/components/MessageContent'; @@ -79,6 +80,7 @@ const AIChat = () => { const [tempCustomConfig, setTempCustomConfig] = useState({ endpoint: '', apiKey: '', model: '' }); const scrollRef = useRef(null); const { playSound } = useSettings(); + const { checkAiAchievements } = useAchievements(); const { toast } = useToast(); // Persist messages to localStorage @@ -134,6 +136,10 @@ const AIChat = () => { setInput(''); setIsLoading(true); playSound('click'); + + // Track AI message count for achievements + incrementAiMessageCount(); + checkAiAchievements(); try { // Build chat history, filtering out empty messages, system notices, and ensuring proper alternation diff --git a/src/pages/Index.tsx b/src/pages/Index.tsx index 79b85ff..5bd6f58 100644 --- a/src/pages/Index.tsx +++ b/src/pages/Index.tsx @@ -33,7 +33,8 @@ const Index = () => { }); const [showConsentModal, setShowConsentModal] = useState(false); const [konamiActive, setKonamiActive] = useState(false); - const { crtEnabled, playSound } = useSettings(); + const [audioPromptDismissed, setAudioPromptDismissed] = useState(false); + const { crtEnabled, playSound, cryptoConsent, audioBlocked, resetAudioContext, setSoundEnabled } = useSettings(); const { unlockAchievement } = useAchievements(); const { activated: konamiActivated, reset: resetKonami } = useKonamiCode(); const location = useLocation(); @@ -77,15 +78,47 @@ const Index = () => { useEffect(() => { const timer = setTimeout(() => { setIsLoading(false); - // Show consent modal after loading if user hasn't made a choice yet - const hasSeenConsent = localStorage.getItem('cryptoConsentSeen'); - if (!hasSeenConsent) { + // Show consent modal after loading if user hasn't accepted yet (null = needs prompt) + if (cryptoConsent === null) { setShowConsentModal(true); } }, 3000); // Extended to 3s for boot sequence return () => clearTimeout(timer); - }, []); + }, [cryptoConsent]); + + // Show audio blocked prompt + useEffect(() => { + if (audioBlocked && !audioPromptDismissed) { + toast({ + title: "Sound Effects Blocked", + description: "Click anywhere or disable sounds in settings", + action: ( +
+ + +
+ ), + duration: 10000, + }); + } + }, [audioBlocked, audioPromptDismissed, resetAudioContext, setSoundEnabled]); useEffect(() => { if (isRedTheme) { @@ -104,17 +137,21 @@ const Index = () => { }; const handleConsentClose = () => { - localStorage.setItem('cryptoConsentSeen', 'true'); setShowConsentModal(false); }; + const handleVerified = () => { + setIsVerified(true); + unlockAchievement('verified_human'); + }; + // Show verification gate if not verified if (!isVerified) { return (
- setIsVerified(true)} /> +
);