Changes
This commit is contained in:
+57
-24
@@ -9,6 +9,7 @@ import { useAchievements, incrementAiMessageCount } from '@/contexts/Achievement
|
||||
import { useToast } from '@/hooks/use-toast';
|
||||
import GlitchText from '@/components/GlitchText';
|
||||
import MessageContent from '@/components/MessageContent';
|
||||
import { FullscreenPortal } from '@/components/FullscreenPortal';
|
||||
import { AI_PROVIDERS, AIProvider, getProvider, CUSTOM_API_STORAGE_KEY } from '@/lib/aiProviders';
|
||||
import {
|
||||
DropdownMenu,
|
||||
@@ -372,16 +373,14 @@ const AIChat = () => {
|
||||
sendMessage();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
const content = (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
className={`flex flex-col ${
|
||||
isFullscreen
|
||||
? 'fixed inset-0 z-50 bg-background p-4 md:p-8'
|
||||
isFullscreen
|
||||
? 'fixed inset-0 z-50 bg-background p-4 md:p-8'
|
||||
: 'h-full'
|
||||
}`}
|
||||
>
|
||||
@@ -408,14 +407,31 @@ const AIChat = () => {
|
||||
<>
|
||||
<div className="flex items-center gap-2 px-2 py-1 border border-primary/30 rounded text-xs font-pixel">
|
||||
<span className="text-muted-foreground">Memory:</span>
|
||||
<span className={`${
|
||||
(() => {
|
||||
const totalChars = messages.filter(m => m.role !== 'system').reduce((acc, m) => acc + m.content.length, 0);
|
||||
const percent = (totalChars / 5000) * 100;
|
||||
return percent > 80 ? 'text-red-400' : percent > 50 ? 'text-yellow-400' : 'text-green-400';
|
||||
})()
|
||||
}`}>
|
||||
{Math.min(100, Math.round((messages.filter(m => m.role !== 'system').reduce((acc, m) => acc + m.content.length, 0) / 5000) * 100))}%
|
||||
<span
|
||||
className={`${
|
||||
(() => {
|
||||
const totalChars = messages
|
||||
.filter(m => m.role !== 'system')
|
||||
.reduce((acc, m) => acc + m.content.length, 0);
|
||||
const percent = (totalChars / 5000) * 100;
|
||||
return percent > 80
|
||||
? 'text-red-400'
|
||||
: percent > 50
|
||||
? 'text-yellow-400'
|
||||
: 'text-green-400';
|
||||
})()
|
||||
}`}
|
||||
>
|
||||
{Math.min(
|
||||
100,
|
||||
Math.round(
|
||||
(messages
|
||||
.filter(m => m.role !== 'system')
|
||||
.reduce((acc, m) => acc + m.content.length, 0) /
|
||||
5000) *
|
||||
100,
|
||||
),
|
||||
)}%
|
||||
</span>
|
||||
</div>
|
||||
<Button
|
||||
@@ -491,7 +507,10 @@ const AIChat = () => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<ScrollArea className="flex-1 border border-primary/30 rounded-lg p-4 mb-4 bg-background/50" ref={scrollRef}>
|
||||
<ScrollArea
|
||||
className="flex-1 border border-primary/30 rounded-lg p-4 mb-4 bg-background/50"
|
||||
ref={scrollRef}
|
||||
>
|
||||
{messages.length === 0 ? (
|
||||
<div className="h-full flex items-center justify-center text-muted-foreground">
|
||||
<div className="text-center">
|
||||
@@ -502,7 +521,7 @@ const AIChat = () => {
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{messages.map((message, index) => (
|
||||
{messages.map((message, index) =>
|
||||
message.role === 'system' ? (
|
||||
<motion.div
|
||||
key={message.id}
|
||||
@@ -521,7 +540,9 @@ const AIChat = () => {
|
||||
initial={{ opacity: 0, x: message.role === 'user' ? 20 : -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ delay: index * 0.05 }}
|
||||
className={`flex gap-3 ${message.role === 'user' ? 'justify-end' : 'justify-start'}`}
|
||||
className={`flex gap-3 ${
|
||||
message.role === 'user' ? 'justify-end' : 'justify-start'
|
||||
}`}
|
||||
>
|
||||
{message.role === 'assistant' && (
|
||||
<div className="w-8 h-8 rounded border border-primary/50 flex items-center justify-center bg-primary/10 flex-shrink-0">
|
||||
@@ -535,9 +556,13 @@ const AIChat = () => {
|
||||
: 'bg-secondary/50 border border-primary/30'
|
||||
}`}
|
||||
>
|
||||
<MessageContent
|
||||
content={message.content}
|
||||
isLoading={isLoading && message.role === 'assistant' && message.content === ''}
|
||||
<MessageContent
|
||||
content={message.content}
|
||||
isLoading={
|
||||
isLoading &&
|
||||
message.role === 'assistant' &&
|
||||
message.content === ''
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
{message.role === 'user' && (
|
||||
@@ -546,8 +571,8 @@ const AIChat = () => {
|
||||
</div>
|
||||
)}
|
||||
</motion.div>
|
||||
)
|
||||
))}
|
||||
),
|
||||
)}
|
||||
{isLoading && messages[messages.length - 1]?.role === 'user' && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
@@ -602,7 +627,9 @@ const AIChat = () => {
|
||||
id="endpoint"
|
||||
placeholder="https://api.example.com/v1/chat/completions"
|
||||
value={tempCustomConfig.endpoint}
|
||||
onChange={(e) => setTempCustomConfig(prev => ({ ...prev, endpoint: e.target.value }))}
|
||||
onChange={(e) =>
|
||||
setTempCustomConfig(prev => ({ ...prev, endpoint: e.target.value }))
|
||||
}
|
||||
className="font-pixel text-sm bg-background/50 border-primary/50"
|
||||
/>
|
||||
</div>
|
||||
@@ -613,7 +640,9 @@ const AIChat = () => {
|
||||
type="password"
|
||||
placeholder="sk-..."
|
||||
value={tempCustomConfig.apiKey}
|
||||
onChange={(e) => setTempCustomConfig(prev => ({ ...prev, apiKey: e.target.value }))}
|
||||
onChange={(e) =>
|
||||
setTempCustomConfig(prev => ({ ...prev, apiKey: e.target.value }))
|
||||
}
|
||||
className="font-pixel text-sm bg-background/50 border-primary/50"
|
||||
/>
|
||||
</div>
|
||||
@@ -623,7 +652,9 @@ const AIChat = () => {
|
||||
id="model"
|
||||
placeholder="gpt-4, claude-3, etc."
|
||||
value={tempCustomConfig.model}
|
||||
onChange={(e) => setTempCustomConfig(prev => ({ ...prev, model: e.target.value }))}
|
||||
onChange={(e) =>
|
||||
setTempCustomConfig(prev => ({ ...prev, model: e.target.value }))
|
||||
}
|
||||
className="font-pixel text-sm bg-background/50 border-primary/50"
|
||||
/>
|
||||
</div>
|
||||
@@ -658,6 +689,8 @@ const AIChat = () => {
|
||||
</Dialog>
|
||||
</motion.div>
|
||||
);
|
||||
|
||||
return isFullscreen ? <FullscreenPortal>{content}</FullscreenPortal> : content;
|
||||
};
|
||||
|
||||
export default AIChat;
|
||||
|
||||
+7
-181
@@ -8,6 +8,7 @@ import { Maximize2, Minimize2, Users, User } from 'lucide-react';
|
||||
import { useBrowserFullscreen } from '@/hooks/useGameDimensions';
|
||||
import GameTouchButton from '@/components/GameTouchButton';
|
||||
import { MobileControlsDock, MobileControlsSpacer } from '@/components/MobileControlsDock';
|
||||
import { FullscreenPortal } from '@/components/FullscreenPortal';
|
||||
|
||||
const BOARD_WIDTH = 10;
|
||||
const BOARD_HEIGHT = 20;
|
||||
@@ -583,7 +584,7 @@ const Tetris = () => {
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
const content = (
|
||||
<motion.div ref={gameRef} tabIndex={0} initial={{ opacity: 0 }} animate={{ opacity: 1 }} transition={{ duration: 0.5 }}
|
||||
className={`flex flex-col outline-none ${isFullscreen ? 'fixed inset-0 z-50 bg-background p-4' : 'h-full'}`}>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
@@ -619,197 +620,22 @@ const Tetris = () => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Score Panel */}
|
||||
<div className="border-2 border-primary/50 p-4 bg-background/60">
|
||||
<p className="font-pixel text-[10px] text-foreground/50 uppercase tracking-wider">Score</p>
|
||||
<p className="font-minecraft text-2xl text-primary text-glow-strong">{score.toLocaleString()}</p>
|
||||
</div>
|
||||
|
||||
{/* High Score */}
|
||||
<div className="border border-primary/30 p-3 bg-background/40">
|
||||
<p className="font-pixel text-[10px] text-foreground/40 uppercase tracking-wider">High Score</p>
|
||||
<p className="font-minecraft text-lg text-primary/80">{highScore.toLocaleString()}</p>
|
||||
<p className="font-pixel text-[8px] text-foreground/30 mt-1">max: 4,294,967,296</p>
|
||||
</div>
|
||||
|
||||
{/* Lines */}
|
||||
<div className="border border-primary/30 p-3 bg-background/40">
|
||||
<p className="font-pixel text-[10px] text-foreground/40 uppercase tracking-wider">Lines</p>
|
||||
<p className="font-minecraft text-xl text-primary">{lines}</p>
|
||||
</div>
|
||||
|
||||
{/* Controls */}
|
||||
<div className="border border-primary/20 p-3 bg-background/30">
|
||||
<p className="font-pixel text-[10px] text-foreground/40 uppercase tracking-wider mb-2">Controls</p>
|
||||
<div className="space-y-1">
|
||||
<p className="font-pixel text-[10px] text-foreground/60">← → ↑ ↓ / WASD</p>
|
||||
<p className="font-pixel text-[10px] text-foreground/60">Space: Hard Drop</p>
|
||||
<p className="font-pixel text-[10px] text-foreground/60">P: Pause</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Action Button */}
|
||||
{!gameStarted || gameOver || gameComplete ? (
|
||||
<button onClick={() => startGame('1p')} className="font-minecraft text-sm py-3 px-4 border-2 border-primary bg-primary/20 text-primary hover:bg-primary/40 transition-all box-glow">
|
||||
{gameComplete ? 'PERFECT!' : gameOver ? 'RETRY' : 'START GAME'}
|
||||
</button>
|
||||
) : (
|
||||
<button onClick={togglePause} className="font-minecraft text-sm py-3 px-4 border-2 border-primary/60 bg-background/50 text-primary hover:bg-primary/20 transition-all">
|
||||
{isPaused ? 'RESUME' : 'PAUSE'}
|
||||
</button>
|
||||
)}
|
||||
{/* ... keep existing code (rest of sidebar / stats / UI) */}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{/* P1 Board */}
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-3 h-3 rounded-sm bg-primary" />
|
||||
<span className="font-pixel text-xs text-primary">P1 (WASD/Q)</span>
|
||||
</div>
|
||||
<div className="border-2 border-primary box-glow p-1 bg-background/80">{renderBoard2P(player1, 'border-primary/20')}</div>
|
||||
<div className="flex gap-2 text-center">
|
||||
<div><p className="font-pixel text-[8px] text-foreground/60">SCORE</p><p className="font-minecraft text-sm text-primary">{player1.score}</p></div>
|
||||
<div><p className="font-pixel text-[8px] text-foreground/60">LINES</p><p className="font-minecraft text-sm text-primary">{player1.lines}</p></div>
|
||||
</div>
|
||||
{player1.gameOver && <span className="font-pixel text-xs text-destructive">GAME OVER</span>}
|
||||
</div>
|
||||
|
||||
{/* P1 Next Piece */}
|
||||
<div className="border-2 border-primary/50 p-4 bg-background/60">
|
||||
<p className="font-pixel text-[10px] text-foreground/50 uppercase tracking-wider mb-3">Next</p>
|
||||
<div className="flex justify-center">
|
||||
<div className="flex flex-col gap-1">
|
||||
{player1.nextPiece.shape.map((row, y) => (
|
||||
<div key={y} className="flex gap-1">
|
||||
{row.map((val, x) => (
|
||||
<div key={`${y}-${x}`} className={`w-4 h-4 ${val ? 'bg-primary box-glow' : 'bg-transparent'}`} />
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Center controls */}
|
||||
<div className="flex flex-col gap-2 min-w-[100px] items-center">
|
||||
<div className="border border-primary/50 p-2 bg-background/50 text-center">
|
||||
<p className="font-pixel text-[8px] text-foreground/60 mb-1">CONTROLS</p>
|
||||
<p className="font-pixel text-[8px] text-primary">P1: WASD Q</p>
|
||||
<p className="font-pixel text-[8px] text-purple-400">P2: ↑↓←→ /</p>
|
||||
<p className="font-pixel text-[8px] text-foreground/60 mt-1">P: Pause</p>
|
||||
</div>
|
||||
{!gameStarted || gameOver ? (
|
||||
<button onClick={() => startGame('2p')} className="font-minecraft text-xs py-2 px-3 border border-primary bg-primary/20 text-primary hover:bg-primary/40 transition-all box-glow">
|
||||
{gameOver ? 'AGAIN' : 'START'}
|
||||
</button>
|
||||
) : (
|
||||
<button onClick={togglePause} className="font-minecraft text-xs py-2 px-3 border border-primary bg-primary/20 text-primary hover:bg-primary/40 transition-all">
|
||||
{isPaused ? 'RESUME' : 'PAUSE'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* P2 Board */}
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-3 h-3 rounded-sm" style={{ backgroundColor: 'hsl(280 70% 50%)' }} />
|
||||
<span className="font-pixel text-xs text-purple-400">P2 (↑↓←→/)</span>
|
||||
</div>
|
||||
<div className="border-2 border-purple-500 p-1 bg-background/80" style={{ boxShadow: '0 0 10px hsl(280 70% 50% / 0.5)' }}>{renderBoard2P(player2, 'border-purple-500/20')}</div>
|
||||
<div className="flex gap-2 text-center">
|
||||
<div><p className="font-pixel text-[8px] text-foreground/60">SCORE</p><p className="font-minecraft text-sm text-purple-400">{player2.score}</p></div>
|
||||
<div><p className="font-pixel text-[8px] text-foreground/60">LINES</p><p className="font-minecraft text-sm text-purple-400">{player2.lines}</p></div>
|
||||
</div>
|
||||
{player2.gameOver && <span className="font-pixel text-xs text-destructive">GAME OVER</span>}
|
||||
</div>
|
||||
|
||||
{/* P2 Next Piece */}
|
||||
<div className="border-2 border-purple-500/50 p-4 bg-background/60">
|
||||
<p className="font-pixel text-[10px] text-purple-400/70 uppercase tracking-wider mb-3">Next</p>
|
||||
<div className="flex justify-center">
|
||||
<div className="flex flex-col gap-1">
|
||||
{player2.nextPiece.shape.map((row, y) => (
|
||||
<div key={y} className="flex gap-1">
|
||||
{row.map((val, x) => (
|
||||
<div key={`${y}-${x}`} className={`w-4 h-4 ${val ? 'bg-purple-400' : 'bg-transparent'}`} style={{ boxShadow: val ? '0 0 6px hsl(280 70% 50%)' : 'none' }} />
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{isMobile && gameMode === '1p' && (
|
||||
<>
|
||||
<MobileControlsSpacer />
|
||||
<MobileControlsDock>
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<div className="flex gap-4 text-center">
|
||||
<div><p className="font-pixel text-[8px] text-foreground/60">SCORE</p><p className="font-minecraft text-sm text-primary">{score.toLocaleString()}</p></div>
|
||||
<div><p className="font-pixel text-[8px] text-foreground/60">HIGH</p><p className="font-minecraft text-sm text-primary">{highScore.toLocaleString()}</p></div>
|
||||
<div><p className="font-pixel text-[8px] text-foreground/60">LINES</p><p className="font-minecraft text-sm text-primary">{lines}</p></div>
|
||||
</div>
|
||||
|
||||
{gameStarted && !gameOver && !gameComplete ? (
|
||||
<div className="grid grid-cols-3 gap-1 mt-2">
|
||||
<div />
|
||||
<GameTouchButton onAction={rotatePiece} className="p-4 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-lg" interval={200}>↑</GameTouchButton>
|
||||
<div />
|
||||
<GameTouchButton onAction={moveLeft} className="p-4 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-lg">←</GameTouchButton>
|
||||
<GameTouchButton onAction={hardDrop} className="p-4 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-[10px]" interval={500}>DROP</GameTouchButton>
|
||||
<GameTouchButton onAction={moveRight} className="p-4 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-lg">→</GameTouchButton>
|
||||
<button onClick={togglePause} className="p-4 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-[10px] select-none">{isPaused ? '▶' : '❚❚'}</button>
|
||||
<GameTouchButton onAction={moveDown} className="p-4 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-lg">↓</GameTouchButton>
|
||||
<div />
|
||||
</div>
|
||||
) : (
|
||||
<button onClick={() => startGame('1p')} className="font-minecraft text-sm py-3 px-8 border border-primary bg-primary/20 text-primary hover:bg-primary/40 transition-all box-glow">{gameComplete ? 'PERFECT!' : gameOver ? 'RETRY' : 'START'}</button>
|
||||
)}
|
||||
</div>
|
||||
</MobileControlsDock>
|
||||
{/* ... keep existing code (2P layout) */}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Overlays */}
|
||||
{!isMobile && (gameOver || isPaused || gameComplete) && gameStarted && (
|
||||
<div className="fixed inset-0 bg-background/80 flex items-center justify-center z-50">
|
||||
<div className="border-2 border-primary box-glow-strong p-6 bg-background text-center">
|
||||
<h2 className="font-minecraft text-2xl text-primary text-glow-strong mb-3">
|
||||
{gameMode === '2p' && gameOver ? winner : gameComplete ? 'GAME COMPLETE!' : gameOver ? 'GAME OVER' : 'PAUSED'}
|
||||
</h2>
|
||||
{gameMode === '1p' && (gameOver || gameComplete) && (
|
||||
<>
|
||||
<p className="font-pixel text-sm text-foreground/80 mb-1">Final Score: {score.toLocaleString()}</p>
|
||||
<p className="font-pixel text-xs text-foreground/60 mb-3">Lines: {lines}</p>
|
||||
</>
|
||||
)}
|
||||
{gameMode === '2p' && gameOver && (
|
||||
<div className="flex gap-4 justify-center mb-3">
|
||||
<div>
|
||||
<p className="font-pixel text-[10px] text-foreground/60">P1 Score</p>
|
||||
<p className="font-minecraft text-lg text-primary">{player1.score}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-pixel text-[10px] text-foreground/60">P2 Score</p>
|
||||
<p className="font-minecraft text-lg text-purple-400">{player2.score}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<button onClick={(gameOver || gameComplete) ? () => startGame(gameMode!) : () => setIsPaused(false)} className="font-minecraft text-sm py-2 px-6 border border-primary bg-primary/20 text-primary hover:bg-primary/40 transition-all box-glow">
|
||||
{(gameOver || gameComplete) ? 'PLAY AGAIN' : 'RESUME'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/* ... keep existing code (mobile controls + overlays) */}
|
||||
</motion.div>
|
||||
);
|
||||
|
||||
return isFullscreen ? <FullscreenPortal>{content}</FullscreenPortal> : content;
|
||||
};
|
||||
|
||||
export default Tetris;
|
||||
Reference in New Issue
Block a user