Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions ui/desktop/src/ChatWindow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import { askAi } from './utils/askAI';
import WingToWing, { Working } from './components/WingToWing';
import { WelcomeScreen } from './components/WelcomeScreen';
import FlappyGoose from './components/FlappyGoose';

// update this when you want to show the welcome screen again - doesn't have to be an actual version, just anything woudln't have been seen before
const CURRENT_VERSION = '0.0.0';
Expand All @@ -38,7 +39,7 @@
chats,
setChats,
selectedChatId,
setSelectedChatId,

Check warning on line 42 in ui/desktop/src/ChatWindow.tsx

View workflow job for this annotation

GitHub Actions / build

'setSelectedChatId' is defined but never used. Allowed unused args must match /^_/u
initialQuery,
setProgressMessage,
setWorking,
Expand All @@ -55,6 +56,7 @@
const [messageMetadata, setMessageMetadata] = useState<Record<string, string[]>>({});
const [hasMessages, setHasMessages] = useState(false);
const [lastInteractionTime, setLastInteractionTime] = useState<number>(Date.now());
const [showGame, setShowGame] = useState(false);

const {
messages,
Expand All @@ -79,7 +81,7 @@
setWorking(Working.Working);
}
},
onFinish: async (message, options) => {

Check warning on line 84 in ui/desktop/src/ChatWindow.tsx

View workflow job for this annotation

GitHub Actions / build

'options' is defined but never used. Allowed unused args must match /^_/u
setProgressMessage('Task finished. Click here to expand.');
setWorking(Working.Idle);

Expand All @@ -102,7 +104,7 @@
c.id === selectedChatId ? { ...c, messages } : c
);
setChats(updatedChats);
}, [messages, selectedChatId]);

Check warning on line 107 in ui/desktop/src/ChatWindow.tsx

View workflow job for this annotation

GitHub Actions / build

React Hook useEffect has missing dependencies: 'chats' and 'setChats'. Either include them or remove the dependency array. If 'setChats' changes too often, find the parent component that defines it and wrap that definition in useCallback

const initialQueryAppended = useRef(false);
useEffect(() => {
Expand All @@ -110,7 +112,7 @@
append({ role: 'user', content: initialQuery });
initialQueryAppended.current = true;
}
}, [initialQuery]);

Check warning on line 115 in ui/desktop/src/ChatWindow.tsx

View workflow job for this annotation

GitHub Actions / build

React Hook useEffect has a missing dependency: 'append'. Either include it or remove the dependency array

useEffect(() => {
if (messages.length > 0) {
Expand Down Expand Up @@ -185,7 +187,6 @@
const updatedMessages = [...messages.slice(0, -1), newLastMessage];
setMessages(updatedMessages);
}

};

return (
Expand Down Expand Up @@ -221,7 +222,9 @@
</div>
{isLoading && (
<div className="flex items-center justify-center p-4">
<LoadingGoose />
<div onClick={() => setShowGame(true)} style={{ cursor: 'pointer' }}>
<LoadingGoose />
</div>
</div>
)}
{error && (
Expand Down Expand Up @@ -260,6 +263,10 @@
<div className="self-stretch h-px bg-black/5 dark:bg-white/5 rounded-sm" />
<BottomMenu hasMessages={hasMessages} />
</Card>

{showGame && (
<FlappyGoose onClose={() => setShowGame(false)} />
)}
</div>
);
}
Expand Down Expand Up @@ -368,7 +375,6 @@
</div>

<WingToWing onExpand={toggleMode} progressMessage={progressMessage} working={working} />

</>
)}
</div>
Expand Down
320 changes: 320 additions & 0 deletions ui/desktop/src/components/FlappyGoose.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,320 @@
import React, { useEffect, useRef, useState } from 'react';

declare var requestAnimationFrame: (callback: FrameRequestCallback) => number;
declare class HTMLCanvasElement {}
declare class HTMLImageElement {}
declare class DOMHighResTimeStamp {}
declare class Image {}
declare type FrameRequestCallback = (time: DOMHighResTimeStamp) => void;
import svg1 from '../images/loading-goose/1.svg';
import svg7 from '../images/loading-goose/7.svg';

interface Obstacle {
x: number;
gapY: number;
passed: boolean;
}

interface FlappyGooseProps {
onClose: () => void;
}

const FlappyGoose: React.FC<FlappyGooseProps> = ({ onClose }) => {
const canvasRef = useRef<HTMLCanvasElement>(null);
const [gameOver, setGameOver] = useState(false);
const [displayScore, setDisplayScore] = useState(0);
const gooseImages = useRef<HTMLImageElement[]>([]);
const framesLoaded = useRef(0);
const [imagesReady, setImagesReady] = useState(false);

// Game state
const gameState = useRef({
gooseY: 200,
velocity: 0,
obstacles: [] as Obstacle[],
gameLoop: 0,
running: false,
score: 0,
isFlapping: false,
flapEndTime: 0
});

// Game settings
const CANVAS_WIDTH = 600;
const CANVAS_HEIGHT = 400;
const GRAVITY = 0.35;
const FLAP_FORCE = -7;
const OBSTACLE_SPEED = 2.5;
const OBSTACLE_GAP = 180;
const GOOSE_SIZE = 35;
const GOOSE_X = 50;
const OBSTACLE_WIDTH = 40;
const FLAP_DURATION = 150;

const safeRequestAnimationFrame = (callback: FrameRequestCallback) => {
if (typeof window !== 'undefined' && typeof requestAnimationFrame !== 'undefined') {
requestAnimationFrame(callback);
}
};

// Load goose images
useEffect(() => {
const frames = [svg1, svg7];
frames.forEach((src, index) => {
const img = new Image();
img.src = src;
img.onload = () => {
framesLoaded.current += 1;
if (framesLoaded.current === frames.length) {
setImagesReady(true);
}
};
gooseImages.current[index] = img;
});
}, []);

const startGame = () => {
if (gameState.current.running || !imagesReady || typeof window === 'undefined') return;

gameState.current = {
gooseY: CANVAS_HEIGHT / 3,
velocity: 0,
obstacles: [],
gameLoop: 0,
running: true,
score: 0,
isFlapping: false,
flapEndTime: 0
};
setGameOver(false);
setDisplayScore(0);
safeRequestAnimationFrame(gameLoop);
};

const flap = () => {
if (gameOver) {
startGame();
return;
}
gameState.current.velocity = FLAP_FORCE;
gameState.current.isFlapping = true;
gameState.current.flapEndTime = Date.now() + FLAP_DURATION;
};

const gameLoop = () => {
if (!gameState.current.running || !imagesReady) return;
const canvas = canvasRef.current;
if (!canvas) return;

const ctx = canvas.getContext('2d');
if (!ctx) return;

// Check if flap animation should end
if (gameState.current.isFlapping && Date.now() >= gameState.current.flapEndTime) {
gameState.current.isFlapping = false;
}

// Update goose position
gameState.current.velocity += GRAVITY;
gameState.current.gooseY += gameState.current.velocity;

// Generate obstacles
if (gameState.current.gameLoop % 120 === 0) {
gameState.current.obstacles.push({
x: CANVAS_WIDTH,
gapY: Math.random() * (CANVAS_HEIGHT - OBSTACLE_GAP - 100) + 50,
passed: false
});
}

// Update obstacles and check for score
gameState.current.obstacles = gameState.current.obstacles.filter(obstacle => {
obstacle.x -= OBSTACLE_SPEED;

// Check for score when the goose passes the middle of the obstacle
const obstacleMiddle = obstacle.x + OBSTACLE_WIDTH / 2;
const gooseMiddle = GOOSE_X + GOOSE_SIZE / 2;

if (!obstacle.passed && obstacleMiddle < gooseMiddle) {
obstacle.passed = true;
gameState.current.score += 1;
setDisplayScore(gameState.current.score);
}

return obstacle.x > -OBSTACLE_WIDTH;
});

// Check collisions
const gooseBox = {
x: GOOSE_X,
y: gameState.current.gooseY,
width: GOOSE_SIZE,
height: GOOSE_SIZE
};

// Collision with ground or ceiling
if (gameState.current.gooseY < 0 || gameState.current.gooseY > CANVAS_HEIGHT - GOOSE_SIZE) {
handleGameOver();
return;
}

// Collision with obstacles
for (const obstacle of gameState.current.obstacles) {
if (
gooseBox.x < obstacle.x + OBSTACLE_WIDTH &&
gooseBox.x + gooseBox.width > obstacle.x
) {
if (
gooseBox.y < obstacle.gapY - OBSTACLE_GAP/2 ||
gooseBox.y + gooseBox.height > obstacle.gapY + OBSTACLE_GAP/2
) {
handleGameOver();
return;
}
}
}

// Draw game
ctx.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);

// Draw rotated goose
ctx.save();
ctx.translate(GOOSE_X + GOOSE_SIZE/2, gameState.current.gooseY + GOOSE_SIZE/2);
const rotation = Math.min(Math.max(gameState.current.velocity * 0.05, -0.5), 0.5);
ctx.rotate(rotation);
ctx.drawImage(
gooseImages.current[gameState.current.isFlapping ? 1 : 0],
-GOOSE_SIZE/2,
-GOOSE_SIZE/2,
GOOSE_SIZE,
GOOSE_SIZE
);
ctx.restore();

// Draw obstacles
ctx.fillStyle = '#4CAF50';
gameState.current.obstacles.forEach(obstacle => {
// Top obstacle
ctx.fillRect(
obstacle.x,
0,
OBSTACLE_WIDTH,
obstacle.gapY - OBSTACLE_GAP/2
);
// Bottom obstacle
ctx.fillRect(
obstacle.x,
obstacle.gapY + OBSTACLE_GAP/2,
OBSTACLE_WIDTH,
CANVAS_HEIGHT - (obstacle.gapY + OBSTACLE_GAP/2)
);
});

// Draw score
ctx.fillStyle = '#000';
ctx.font = '24px Arial';
ctx.fillText(`Score: ${gameState.current.score}`, 10, 30);

gameState.current.gameLoop++;
safeRequestAnimationFrame(gameLoop);
};

const handleGameOver = () => {
gameState.current.running = false;
setGameOver(true);
};

useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;

canvas.width = CANVAS_WIDTH;
canvas.height = CANVAS_HEIGHT;

const handleKeyPress = (e: KeyboardEvent) => {
if (e.code === 'Space') {
e.preventDefault();
flap();
}
};

window.addEventListener('keydown', handleKeyPress);

if (imagesReady) {
startGame();
}

return () => {
window.removeEventListener('keydown', handleKeyPress);
gameState.current.running = false;
};
}, [imagesReady]);

return (
<div
style={{
position: 'fixed',
top: 0,
left: 0,
width: '100%',
height: '100%',
backgroundColor: 'rgba(0, 0, 0, 0.8)',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
zIndex: 1000
}}
onClick={flap}
>
<canvas
ref={canvasRef}
style={{
border: '2px solid #333',
borderRadius: '8px',
backgroundColor: '#87CEEB',
maxWidth: '100%',
maxHeight: '100vh'
}}
/>
{!imagesReady && (
<div style={{ color: 'white', fontSize: '24px' }}>Loading...</div>
)}
{gameOver && (
<div
style={{
position: 'absolute',
color: 'white',
fontSize: '24px',
textAlign: 'center'
}}
>
<p>Game Over!</p>
<p>Score: {displayScore}</p>
<p>Click or press space to play again</p>
</div>
)}
<button
onClick={(e) => {
e.stopPropagation();
onClose();
}}
style={{
position: 'absolute',
top: '20px',
right: '20px',
padding: '8px 16px',
backgroundColor: '#ff4444',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer'
}}
>
Close
</button>
</div>
);
};

export default FlappyGoose;
Loading