From 791510200663812ea2b814404d793cc8676cee34 Mon Sep 17 00:00:00 2001 From: MentatBot <160964065+MentatBot@users.noreply.github.com> Date: Tue, 7 Oct 2025 22:42:21 +0000 Subject: [PATCH 01/82] Replace landing page with basic chat app MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add chat interface with "mentat party 🥳" header - Implement username and message input fields - Add persistent message storage in server/src/messages.json - Create GET /api/messages and POST /api/messages endpoints - Add auto-refresh polling every 2 seconds - Include auto-scroll to latest messages Mentat precommit script failed. Log: https://mentat.ai/gh/AbanteAI/party/log/dad74228-76d3-48d9-9979-d69016674a61 Co-authored-by: granawkins <50287275+granawkins@users.noreply.github.com> --- client/src/App.tsx | 232 +++++++++++++++++++++++---------------- server/src/app.ts | 47 +++++++- server/src/messages.json | 20 ++++ 3 files changed, 205 insertions(+), 94 deletions(-) create mode 100644 server/src/messages.json diff --git a/client/src/App.tsx b/client/src/App.tsx index 43b4b68..ab08d38 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -1,37 +1,75 @@ -import { useState, useEffect } from 'react'; -import mentatLogo from '/mentat.png'; +import { useState, useEffect, useRef } from 'react'; + +interface Message { + id: number; + username: string; + message: string; + timestamp: string; +} function App() { - const [message, setMessage] = useState(null); + const [messages, setMessages] = useState([]); + const [username, setUsername] = useState(''); + const [messageText, setMessageText] = useState(''); const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); + const messagesEndRef = useRef(null); - useEffect(() => { - const fetchBackendMessage = async () => { - setLoading(true); - setError(null); + const scrollToBottom = () => { + messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); + }; - try { - const response = await fetch('/api'); + useEffect(() => { + scrollToBottom(); + }, [messages]); - if (!response.ok) { - throw new Error(`HTTP error ${response.status}`); - } + useEffect(() => { + fetchMessages(); + const interval = setInterval(fetchMessages, 2000); + return () => clearInterval(interval); + }, []); + const fetchMessages = async () => { + try { + const response = await fetch('/api/messages'); + if (response.ok) { const data = await response.json(); - setMessage(data.message); - } catch (err) { - console.error('Error fetching data:', err); - setError( - err instanceof Error ? err.message : 'An unknown error occurred' - ); - } finally { - setLoading(false); + setMessages(data); } - }; + } catch (err) { + console.error('Error fetching messages:', err); + } + }; - fetchBackendMessage(); - }, []); + const sendMessage = async (e: React.FormEvent) => { + e.preventDefault(); + + if (!username.trim() || !messageText.trim()) { + return; + } + + setLoading(true); + try { + const response = await fetch('/api/messages', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + username: username.trim(), + message: messageText.trim(), + }), + }); + + if (response.ok) { + setMessageText(''); + await fetchMessages(); + } + } catch (err) { + console.error('Error sending message:', err); + } finally { + setLoading(false); + } + }; return (
- {/* Logo */} -
- - Mentat Logo - -
- - {/* Main content */}
-

Mentat Template JS

+

mentat party 🥳

- {/* Tech stack */} + {/* Messages area */}
- {[ - ['Frontend', 'React, Vite, Vitest'], - ['Backend', 'Node.js, Express, Jest'], - ['Utilities', 'TypeScript, ESLint, Prettier'], - ].map(([title, techs]) => ( -
+ {messages.map((msg) => ( +
- {title} + {msg.username} +
+
+ {msg.message} +
+
+ {new Date(msg.timestamp).toLocaleTimeString()}
-
{techs}
))} +
- {/* Server message */} -
-
+ setUsername(e.target.value)} style={{ + padding: '10px', + borderRadius: '6px', + border: '1px solid #d1d5db', fontSize: '14px', - fontWeight: '500', - color: '#1f2937', - marginBottom: '8px', + width: '150px', }} - > - Message from server: -
-
- {loading ? ( - 'Loading message from server...' - ) : error ? ( - Error: {error} - ) : message ? ( - message - ) : ( - - No message from server - - )} -
-
- - {/* Call to action */} -
- Create a new GitHub issue and tag{' '} - + setMessageText(e.target.value)} + style={{ + flex: 1, + padding: '10px', + borderRadius: '6px', + border: '1px solid #d1d5db', + fontSize: '14px', + }} + /> +
+ Send + +
); diff --git a/server/src/app.ts b/server/src/app.ts index 2cbac38..19f02f3 100644 --- a/server/src/app.ts +++ b/server/src/app.ts @@ -1,7 +1,7 @@ import express, { Request, Response } from 'express'; import cors from 'cors'; import path from 'path'; -import { existsSync } from 'fs'; +import { existsSync, readFileSync, writeFileSync } from 'fs'; export const app = express(); export const PORT = process.env.PORT || 5000; @@ -12,11 +12,56 @@ app.use(cors()); // Enable CORS for frontend communication app.use(express.json()); // Parse JSON bodies app.use(express.static(CLIENT_DIST_PATH)); // Serve static files from client/dist +// Messages file path +const MESSAGES_FILE = path.join(__dirname, 'messages.json'); + +// Helper functions for message persistence +function readMessages() { + try { + const data = readFileSync(MESSAGES_FILE, 'utf-8'); + return JSON.parse(data); + } catch { + return []; + } +} + +function writeMessages(messages: any[]) { + writeFileSync(MESSAGES_FILE, JSON.stringify(messages, null, 2)); +} + // Basic route app.get('/api', (req: Request, res: Response) => { res.json({ message: 'Welcome to the Mentat API!' }); }); +// Get all messages +app.get('/api/messages', (req: Request, res: Response) => { + const messages = readMessages(); + res.json(messages); +}); + +// Post a new message +app.post('/api/messages', (req: Request, res: Response) => { + const { username, message } = req.body; + + if (!username || !message) { + return res.status(400).json({ error: 'Username and message are required' }); + } + + const messages = readMessages(); + const newMessage = { + id: Date.now(), + username, + message, + timestamp: new Date().toISOString(), + }; + + messages.push(newMessage); + writeMessages(messages); + + res.json(newMessage); +}); + // Serve React app or fallback page app.get('*', (req: Request, res: Response) => { const indexPath = path.join(CLIENT_DIST_PATH, 'index.html'); diff --git a/server/src/messages.json b/server/src/messages.json new file mode 100644 index 0000000..8847e02 --- /dev/null +++ b/server/src/messages.json @@ -0,0 +1,20 @@ +[ + { + "id": 1759876897238, + "username": "jake", + "message": "hi!", + "timestamp": "2025-10-07T22:41:37.238Z" + }, + { + "id": 1759876899207, + "username": "Scott", + "message": "First", + "timestamp": "2025-10-07T22:41:39.207Z" + }, + { + "id": 1759876924131, + "username": "bonesaw", + "message": "i'm ready!!", + "timestamp": "2025-10-07T22:42:04.131Z" + } +] From 0463b2ce536d95ee9f5face28defb59600c29ec7 Mon Sep 17 00:00:00 2001 From: MentatBot <160964065+MentatBot@users.noreply.github.com> Date: Tue, 7 Oct 2025 22:47:57 +0000 Subject: [PATCH 02/82] Add reactions feature and snake game MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Chat improvements: - Add reaction system with emoji buttons (👍 ❤️ 😂 🎉) - Add POST /api/messages/:id/react endpoint - Persist username in localStorage - Move messages to data/ directory (added to .gitignore) - Fix TypeScript lint errors with proper Message interface - Add error handling for file operations - Add server-side input validation and trimming Snake game: - Create standalone snake game server on port 5174 - Classic snake gameplay with score tracking - High score persistence in localStorage - Arrow keys and WASD controls - Add preview script to auto-start snake server Mentat precommit script failed. Log: https://mentat.ai/gh/AbanteAI/party/log/9397bbc7-dd7f-45db-8a86-c2f0fd6e2621 Co-authored-by: granawkins <50287275+granawkins@users.noreply.github.com> --- .gitignore | 1 + .mentat/preview/2-snake.sh | 6 + client/src/App.tsx | 97 +++++++++++++ server/src/app.ts | 111 +++++++++++--- server/src/messages.json | 20 --- snake/index.html | 290 +++++++++++++++++++++++++++++++++++++ snake/package.json | 12 ++ snake/server.js | 17 +++ 8 files changed, 517 insertions(+), 37 deletions(-) create mode 100755 .mentat/preview/2-snake.sh delete mode 100644 server/src/messages.json create mode 100644 snake/index.html create mode 100644 snake/package.json create mode 100644 snake/server.js diff --git a/.gitignore b/.gitignore index 4e04de0..a34d381 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ **/dist .mentat/logs **/package-lock.json +data/ diff --git a/.mentat/preview/2-snake.sh b/.mentat/preview/2-snake.sh new file mode 100755 index 0000000..1c4f263 --- /dev/null +++ b/.mentat/preview/2-snake.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +# Start the snake game server +cd snake +npm install --silent +npm start diff --git a/client/src/App.tsx b/client/src/App.tsx index ab08d38..b252959 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -5,6 +5,7 @@ interface Message { username: string; message: string; timestamp: string; + reactions: { [emoji: string]: string[] }; } function App() { @@ -14,6 +15,21 @@ function App() { const [loading, setLoading] = useState(false); const messagesEndRef = useRef(null); + // Load username from localStorage + useEffect(() => { + const savedUsername = localStorage.getItem('chatUsername'); + if (savedUsername) { + setUsername(savedUsername); + } + }, []); + + // Save username to localStorage when it changes + useEffect(() => { + if (username) { + localStorage.setItem('chatUsername', username); + } + }, [username]); + const scrollToBottom = () => { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }; @@ -71,6 +87,31 @@ function App() { } }; + const addReaction = async (messageId: number, emoji: string) => { + if (!username.trim()) { + return; + } + + try { + const response = await fetch(`/api/messages/${messageId}/react`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + emoji, + username: username.trim(), + }), + }); + + if (response.ok) { + await fetchMessages(); + } + } catch (err) { + console.error('Error adding reaction:', err); + } + }; + return (
{msg.message}
+
+ {/* Existing reactions */} + {msg.reactions && + Object.entries(msg.reactions).map(([emoji, users]) => ( + + ))} + {/* Quick reaction buttons */} + {['👍', '❤️', '😂', '🎉'].map((emoji) => ( + + ))} +
{ app.post('/api/messages', (req: Request, res: Response) => { const { username, message } = req.body; - if (!username || !message) { + const usernameSafe = String(username || '').trim(); + const messageSafe = String(message || '').trim(); + + if (!usernameSafe || !messageSafe) { return res.status(400).json({ error: 'Username and message are required' }); } - const messages = readMessages(); - const newMessage = { - id: Date.now(), - username, - message, - timestamp: new Date().toISOString(), - }; + try { + const messages = readMessages(); + const newMessage: Message = { + id: Date.now(), + username: usernameSafe, + message: messageSafe, + timestamp: new Date().toISOString(), + reactions: {}, + }; - messages.push(newMessage); - writeMessages(messages); + messages.push(newMessage); + writeMessages(messages); + + res.json(newMessage); + } catch (error) { + console.error('Error posting message:', error); + res.status(500).json({ error: 'Failed to save message' }); + } +}); - res.json(newMessage); +// Add a reaction to a message +app.post('/api/messages/:id/react', (req: Request, res: Response) => { + const { id } = req.params; + const { emoji, username } = req.body; + + const emojiSafe = String(emoji || '').trim(); + const usernameSafe = String(username || '').trim(); + + if (!emojiSafe || !usernameSafe) { + return res.status(400).json({ error: 'Emoji and username are required' }); + } + + try { + const messages = readMessages(); + const message = messages.find((m) => m.id === parseInt(id)); + + if (!message) { + return res.status(404).json({ error: 'Message not found' }); + } + + if (!message.reactions) { + message.reactions = {}; + } + + if (!message.reactions[emojiSafe]) { + message.reactions[emojiSafe] = []; + } + + // Toggle reaction - remove if already exists, add if not + const userIndex = message.reactions[emojiSafe].indexOf(usernameSafe); + if (userIndex > -1) { + message.reactions[emojiSafe].splice(userIndex, 1); + // Remove emoji key if no users left + if (message.reactions[emojiSafe].length === 0) { + delete message.reactions[emojiSafe]; + } + } else { + message.reactions[emojiSafe].push(usernameSafe); + } + + writeMessages(messages); + res.json(message); + } catch (error) { + console.error('Error adding reaction:', error); + res.status(500).json({ error: 'Failed to add reaction' }); + } }); // Serve React app or fallback page diff --git a/server/src/messages.json b/server/src/messages.json deleted file mode 100644 index 8847e02..0000000 --- a/server/src/messages.json +++ /dev/null @@ -1,20 +0,0 @@ -[ - { - "id": 1759876897238, - "username": "jake", - "message": "hi!", - "timestamp": "2025-10-07T22:41:37.238Z" - }, - { - "id": 1759876899207, - "username": "Scott", - "message": "First", - "timestamp": "2025-10-07T22:41:39.207Z" - }, - { - "id": 1759876924131, - "username": "bonesaw", - "message": "i'm ready!!", - "timestamp": "2025-10-07T22:42:04.131Z" - } -] diff --git a/snake/index.html b/snake/index.html new file mode 100644 index 0000000..44b5751 --- /dev/null +++ b/snake/index.html @@ -0,0 +1,290 @@ + + + + + + Snake Game 🐍 + + + +

🐍 Snake Game 🐍

+
+ +
+
Score: 0
+
High Score: 0
+
+
Use Arrow Keys or WASD to move
+
+ +
+

Game Over!

+

Final Score: 0

+ +
+ + + + diff --git a/snake/package.json b/snake/package.json new file mode 100644 index 0000000..1d41cae --- /dev/null +++ b/snake/package.json @@ -0,0 +1,12 @@ +{ + "name": "snake-game", + "version": "1.0.0", + "description": "Simple snake game server", + "main": "server.js", + "scripts": { + "start": "node server.js" + }, + "dependencies": { + "express": "^4.21.2" + } +} diff --git a/snake/server.js b/snake/server.js new file mode 100644 index 0000000..7fc8516 --- /dev/null +++ b/snake/server.js @@ -0,0 +1,17 @@ +const express = require('express'); +const path = require('path'); + +const app = express(); +const PORT = 5174; + +// Serve static files +app.use(express.static(__dirname)); + +// Serve the snake game +app.get('/', (req, res) => { + res.sendFile(path.join(__dirname, 'index.html')); +}); + +app.listen(PORT, () => { + console.log(`Snake game running on http://localhost:${PORT}`); +}); From 41db8768c0fd37727fcca7899224fa311c14093b Mon Sep 17 00:00:00 2001 From: MentatBot <160964065+MentatBot@users.noreply.github.com> Date: Tue, 7 Oct 2025 22:53:45 +0000 Subject: [PATCH 03/82] Fix message persistence and adjust snake speed Message persistence fixes: - Fix data directory path to use project root (../data from server dir) - Server now correctly reads/writes to data/messages.json - Prevents messages from being lost on refresh Snake game adjustments: - Reduce speed to 2x faster (50ms) instead of 4x (25ms) - Convert to ES6 imports (import instead of require) - Add "type": "module" to package.json Reaction improvements: - Add alert when user tries to react without username - Remove opacity from quick reaction buttons - Hide quick reaction buttons once they're used Mentat precommit script passed. Log: https://mentat.ai/gh/AbanteAI/party/log/c06deb87-1422-44af-8ebb-44f652a4e2e6 Co-authored-by: granawkins <50287275+granawkins@users.noreply.github.com> --- client/src/App.tsx | 48 +++++++++++++++++++++++++++------------------- server/src/app.ts | 3 ++- snake/index.html | 2 +- snake/package.json | 1 + snake/server.js | 8 ++++++-- 5 files changed, 38 insertions(+), 24 deletions(-) diff --git a/client/src/App.tsx b/client/src/App.tsx index b252959..3784986 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -88,7 +88,9 @@ function App() { }; const addReaction = async (messageId: number, emoji: string) => { - if (!username.trim()) { + const trimmedUsername = username.trim(); + if (!trimmedUsername) { + alert('Please enter a username first!'); return; } @@ -100,7 +102,7 @@ function App() { }, body: JSON.stringify({ emoji, - username: username.trim(), + username: trimmedUsername, }), }); @@ -209,24 +211,30 @@ function App() { ))} {/* Quick reaction buttons */} - {['👍', '❤️', '😂', '🎉'].map((emoji) => ( - - ))} + {['👍', '❤️', '😂', '🎉'] + .filter( + (emoji) => + !msg.reactions || + !msg.reactions[emoji] || + msg.reactions[emoji].length === 0 + ) + .map((emoji) => ( + + ))}
Game Over! function startGame() { if (!gameRunning) { gameRunning = true; - gameLoop = setInterval(drawGame, 100); + gameLoop = setInterval(drawGame, 50); } } diff --git a/snake/package.json b/snake/package.json index 1d41cae..79a6a87 100644 --- a/snake/package.json +++ b/snake/package.json @@ -1,6 +1,7 @@ { "name": "snake-game", "version": "1.0.0", + "type": "module", "description": "Simple snake game server", "main": "server.js", "scripts": { diff --git a/snake/server.js b/snake/server.js index 7fc8516..08ccb2f 100644 --- a/snake/server.js +++ b/snake/server.js @@ -1,5 +1,9 @@ -const express = require('express'); -const path = require('path'); +import express from 'express'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); const app = express(); const PORT = 5174; From 0307e7e55badef80213a5b4fb658311aa5b529f3 Mon Sep 17 00:00:00 2001 From: MentatBot <160964065+MentatBot@users.noreply.github.com> Date: Tue, 7 Oct 2025 22:58:44 +0000 Subject: [PATCH 04/82] Fix CI tests for chat app - Update tests to match new chat interface instead of old landing page - Add scrollIntoView mock for jsdom test environment - Test chat rendering, message loading, and message sending - All tests now passing Mentat precommit script passed. Log: https://mentat.ai/gh/AbanteAI/party/log/a01da16b-1015-4c02-8128-8be34edfb85b Co-authored-by: granawkins <50287275+granawkins@users.noreply.github.com> --- client/src/__tests__/App.test.tsx | 103 +++++++++++++++++++++++------- client/src/__tests__/setup.ts | 4 +- 2 files changed, 83 insertions(+), 24 deletions(-) diff --git a/client/src/__tests__/App.test.tsx b/client/src/__tests__/App.test.tsx index 3741e7b..243b5be 100644 --- a/client/src/__tests__/App.test.tsx +++ b/client/src/__tests__/App.test.tsx @@ -1,16 +1,21 @@ import { describe, it, expect, vi, beforeEach, Mock } from 'vitest'; import { render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import App from '../App'; // Define types -type ApiResponse = { +interface Message { + id: number; + username: string; message: string; -}; + timestamp: string; + reactions: { [emoji: string]: string[] }; +} // Mock the fetch API globalThis.fetch = vi.fn() as unknown as typeof fetch; -function mockFetchResponse(data: ApiResponse) { +function mockFetchResponse(data: Message[]) { return { json: vi.fn().mockResolvedValue(data), ok: true, @@ -20,47 +25,99 @@ function mockFetchResponse(data: ApiResponse) { describe('App Component', () => { beforeEach(() => { vi.clearAllMocks(); - // Default mock implementation + localStorage.clear(); + // Default mock implementation - empty messages (globalThis.fetch as unknown as Mock).mockResolvedValue( - mockFetchResponse({ message: 'Test Message from API' }) + mockFetchResponse([]) ); }); - it('renders App component correctly', () => { + it('renders chat interface correctly', () => { render(); - expect(screen.getByText('Mentat Template JS')).toBeInTheDocument(); - expect(screen.getByText(/Frontend: React, Vite/)).toBeInTheDocument(); - expect(screen.getByText(/Backend: Node.js, Express/)).toBeInTheDocument(); + expect(screen.getByText('mentat party 🥳')).toBeInTheDocument(); + expect(screen.getByPlaceholderText('Username')).toBeInTheDocument(); expect( - screen.getByText(/Utilities: Typescript, ESLint, Prettier/) + screen.getByPlaceholderText('Type a message...') ).toBeInTheDocument(); + expect(screen.getByText('Send')).toBeInTheDocument(); }); - it('loads and displays API message', async () => { - render(); + it('loads and displays messages', async () => { + const mockMessages: Message[] = [ + { + id: 1, + username: 'TestUser', + message: 'Hello World', + timestamp: new Date().toISOString(), + reactions: {}, + }, + ]; + + (globalThis.fetch as unknown as Mock).mockResolvedValue( + mockFetchResponse(mockMessages) + ); - // Should initially show loading message - expect(screen.getByText(/Loading message from server/)).toBeInTheDocument(); + render(); - // Wait for the fetch to resolve and check if the message is displayed + // Wait for messages to load await waitFor(() => { - expect(screen.getByText('Test Message from API')).toBeInTheDocument(); + expect(screen.getByText('TestUser')).toBeInTheDocument(); + expect(screen.getByText('Hello World')).toBeInTheDocument(); }); - expect(globalThis.fetch).toHaveBeenCalledWith('/api'); + expect(globalThis.fetch).toHaveBeenCalledWith('/api/messages'); }); - it('handles API error', async () => { - // Mock a failed API call - (globalThis.fetch as unknown as Mock).mockRejectedValue( - new Error('API Error') + it('sends a message when form is submitted', async () => { + const user = userEvent.setup(); + + // Mock successful message post and get + (globalThis.fetch as unknown as Mock).mockImplementation( + (url: string, options?: RequestInit) => { + if (options?.method === 'POST') { + // POST request - return single message + return Promise.resolve({ + json: vi.fn().mockResolvedValue({ + id: 2, + username: 'TestUser', + message: 'New message', + timestamp: new Date().toISOString(), + reactions: {}, + }), + ok: true, + }); + } + // GET request - return array of messages + return Promise.resolve(mockFetchResponse([])); + } ); render(); - // Wait for the error message to appear + const usernameInput = screen.getByPlaceholderText('Username'); + const messageInput = screen.getByPlaceholderText('Type a message...'); + const sendButton = screen.getByText('Send'); + + // Type username and message + await user.type(usernameInput, 'TestUser'); + await user.type(messageInput, 'New message'); + + // Submit form + await user.click(sendButton); + + // Verify fetch was called with correct data await waitFor(() => { - expect(screen.getByText(/Error: API Error/)).toBeInTheDocument(); + expect(globalThis.fetch).toHaveBeenCalledWith( + '/api/messages', + expect.objectContaining({ + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + username: 'TestUser', + message: 'New message', + }), + }) + ); }); }); }); diff --git a/client/src/__tests__/setup.ts b/client/src/__tests__/setup.ts index d950866..9990f46 100644 --- a/client/src/__tests__/setup.ts +++ b/client/src/__tests__/setup.ts @@ -1,3 +1,5 @@ import '@testing-library/jest-dom'; +import { vi } from 'vitest'; -// Global test setup can go here +// Mock scrollIntoView which is not available in jsdom +Element.prototype.scrollIntoView = vi.fn(); From 43e43e62c8f301f5722224464758f52024b61b97 Mon Sep 17 00:00:00 2001 From: MentatBot <160964065+MentatBot@users.noreply.github.com> Date: Tue, 7 Oct 2025 23:00:37 +0000 Subject: [PATCH 05/82] Fix CI build error and add cache-busting headers - Fix unused variable warning in test file - Add cache-busting headers to /api/messages endpoint to prevent browser caching - This should help ensure users always see the latest messages Mentat precommit script passed. Log: https://mentat.ai/gh/AbanteAI/party/log/b960fcca-ba78-4d4c-9cdf-8b86cc98537a Co-authored-by: granawkins <50287275+granawkins@users.noreply.github.com> --- client/src/__tests__/App.test.tsx | 2 +- server/src/app.ts | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/client/src/__tests__/App.test.tsx b/client/src/__tests__/App.test.tsx index 243b5be..055e2a7 100644 --- a/client/src/__tests__/App.test.tsx +++ b/client/src/__tests__/App.test.tsx @@ -73,7 +73,7 @@ describe('App Component', () => { // Mock successful message post and get (globalThis.fetch as unknown as Mock).mockImplementation( - (url: string, options?: RequestInit) => { + (_url: string, options?: RequestInit) => { if (options?.method === 'POST') { // POST request - return single message return Promise.resolve({ diff --git a/server/src/app.ts b/server/src/app.ts index ebc440e..53578ae 100644 --- a/server/src/app.ts +++ b/server/src/app.ts @@ -57,6 +57,11 @@ app.get('/api', (req: Request, res: Response) => { // Get all messages app.get('/api/messages', (req: Request, res: Response) => { + // Prevent caching + res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate'); + res.setHeader('Pragma', 'no-cache'); + res.setHeader('Expires', '0'); + const messages = readMessages(); res.json(messages); }); From b1206322e06a391f9798c67b7cda3b85bae3716e Mon Sep 17 00:00:00 2001 From: MentatBot <160964065+MentatBot@users.noreply.github.com> Date: Tue, 7 Oct 2025 23:01:38 +0000 Subject: [PATCH 06/82] Fix message fetching with useCallback - Wrap fetchMessages in useCallback to ensure stable function reference - Add fetchMessages to useEffect dependency array - This should fix issues with initial message loading and polling Mentat precommit script passed. Log: https://mentat.ai/gh/AbanteAI/party/log/d6c16d51-45a2-4a0f-959e-21b22590f513 Co-authored-by: granawkins <50287275+granawkins@users.noreply.github.com> --- client/src/App.tsx | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/client/src/App.tsx b/client/src/App.tsx index 3784986..0a66348 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useRef } from 'react'; +import { useState, useEffect, useRef, useCallback } from 'react'; interface Message { id: number; @@ -34,17 +34,7 @@ function App() { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }; - useEffect(() => { - scrollToBottom(); - }, [messages]); - - useEffect(() => { - fetchMessages(); - const interval = setInterval(fetchMessages, 2000); - return () => clearInterval(interval); - }, []); - - const fetchMessages = async () => { + const fetchMessages = useCallback(async () => { try { const response = await fetch('/api/messages'); if (response.ok) { @@ -54,7 +44,17 @@ function App() { } catch (err) { console.error('Error fetching messages:', err); } - }; + }, []); + + useEffect(() => { + scrollToBottom(); + }, [messages]); + + useEffect(() => { + fetchMessages(); + const interval = setInterval(fetchMessages, 2000); + return () => clearInterval(interval); + }, [fetchMessages]); const sendMessage = async (e: React.FormEvent) => { e.preventDefault(); From a3fcfab6e3a262abf1a34d6c7dafe997b128df35 Mon Sep 17 00:00:00 2001 From: MentatBot <160964065+MentatBot@users.noreply.github.com> Date: Tue, 7 Oct 2025 23:04:40 +0000 Subject: [PATCH 07/82] Add progressive acceleration to snake game - Snake game now speeds up as score increases - Starts at 50ms interval, decreases by 2ms per point - Caps at 20ms minimum (maximum speed) - Speed resets on new game - Makes game progressively more challenging Mentat precommit script passed. Log: https://mentat.ai/gh/AbanteAI/party/log/2a730e95-8d9e-459e-9a0d-c34f5c2e635d Co-authored-by: granawkins <50287275+granawkins@users.noreply.github.com> --- snake/index.html | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/snake/index.html b/snake/index.html index 29f3649..fa77711 100644 --- a/snake/index.html +++ b/snake/index.html @@ -134,6 +134,9 @@

Game Over!

let highScore = localStorage.getItem('snakeHighScore') || 0; let gameRunning = false; let gameLoop; + let gameSpeed = 50; // Starting speed in ms + const minSpeed = 20; // Minimum speed (maximum difficulty) + const speedIncrement = 2; // Speed increase per point highScoreElement.textContent = highScore; @@ -157,6 +160,14 @@

Game Over!

localStorage.setItem('snakeHighScore', highScore); } placeFood(); + + // Increase speed (decrease interval) + if (gameSpeed > minSpeed) { + gameSpeed = Math.max(minSpeed, gameSpeed - speedIncrement); + // Restart game loop with new speed + clearInterval(gameLoop); + gameLoop = setInterval(drawGame, gameSpeed); + } } else { snake.pop(); } @@ -225,6 +236,7 @@

Game Over!

dx = 0; dy = 0; score = 0; + gameSpeed = 50; // Reset speed scoreElement.textContent = score; gameOverElement.style.display = 'none'; placeFood(); @@ -234,7 +246,7 @@

Game Over!

function startGame() { if (!gameRunning) { gameRunning = true; - gameLoop = setInterval(drawGame, 50); + gameLoop = setInterval(drawGame, gameSpeed); } } From f41ba8d1c5ecf44051f998343c504976795e65f1 Mon Sep 17 00:00:00 2001 From: MentatBot <160964065+MentatBot@users.noreply.github.com> Date: Tue, 7 Oct 2025 23:06:17 +0000 Subject: [PATCH 08/82] Add request logging to server - Add simple middleware to log all incoming requests - Helps debug issues with message sending and fetching - Shows timestamp, method, and path for each request Mentat precommit script passed. Log: https://mentat.ai/gh/AbanteAI/party/log/4b0571f3-7aa1-4651-90f2-d41cb6ccf999 Co-authored-by: granawkins <50287275+granawkins@users.noreply.github.com> --- server/src/app.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/server/src/app.ts b/server/src/app.ts index 53578ae..d6ace01 100644 --- a/server/src/app.ts +++ b/server/src/app.ts @@ -12,6 +12,12 @@ app.use(cors()); // Enable CORS for frontend communication app.use(express.json()); // Parse JSON bodies app.use(express.static(CLIENT_DIST_PATH)); // Serve static files from client/dist +// Simple request logging +app.use((req: Request, res: Response, next) => { + console.log(`${new Date().toISOString()} ${req.method} ${req.path}`); + next(); +}); + // Message type definition interface Message { id: number; From 525553c1214e2748071493bfd8b60be9125defa9 Mon Sep 17 00:00:00 2001 From: MentatBot <160964065+MentatBot@users.noreply.github.com> Date: Tue, 7 Oct 2025 23:10:02 +0000 Subject: [PATCH 09/82] Add fire and rocket reaction emojis MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add 🔥 (fire) and 🚀 (rocket) to quick reaction buttons - Requested by users for more expressive reactions - Now have 6 quick reaction options: 👍 ❤️ 😂 🎉 🔥 🚀 Mentat precommit script passed. Log: https://mentat.ai/gh/AbanteAI/party/log/7e630876-deca-4446-96e8-7766101fafdd Co-authored-by: granawkins <50287275+granawkins@users.noreply.github.com> --- client/src/App.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/App.tsx b/client/src/App.tsx index 0a66348..d5cccd1 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -211,7 +211,7 @@ function App() { ))} {/* Quick reaction buttons */} - {['👍', '❤️', '😂', '🎉'] + {['👍', '❤️', '😂', '🎉', '🔥', '🚀'] .filter( (emoji) => !msg.reactions || From 87558d4b2a6916183aabfd5f6129a58f4af69946 Mon Sep 17 00:00:00 2001 From: MentatBot <160964065+MentatBot@users.noreply.github.com> Date: Tue, 7 Oct 2025 23:13:11 +0000 Subject: [PATCH 10/82] Add leaderboard to snake game - Add top 10 high scores leaderboard with player names - Name input on game over screen - Remembers player name in localStorage - Gold highlight for # 1 score - Side-by-side layout with game and leaderboard - Press Enter to submit score - Only saves scores > 0 Mentat precommit script passed. Log: https://mentat.ai/gh/AbanteAI/party/log/8cea6084-f93b-4d56-9bc1-1dddb6cb1617 Co-authored-by: granawkins <50287275+granawkins@users.noreply.github.com> --- snake/index.html | 166 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 159 insertions(+), 7 deletions(-) diff --git a/snake/index.html b/snake/index.html index fa77711..50848ec 100644 --- a/snake/index.html +++ b/snake/index.html @@ -96,23 +96,98 @@ .game-over button:hover { background: #5568d3; } + + .game-over input { + width: 100%; + padding: 10px; + margin-bottom: 15px; + border: 2px solid #667eea; + border-radius: 6px; + font-size: 16px; + text-align: center; + } + + .leaderboard { + margin-top: 20px; + background: white; + border-radius: 16px; + padding: 20px; + box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3); + min-width: 300px; + } + + .leaderboard h2 { + color: #333; + margin-bottom: 15px; + text-align: center; + } + + .leaderboard-entry { + display: flex; + justify-content: space-between; + padding: 8px 12px; + margin-bottom: 5px; + background: #f8fafc; + border-radius: 6px; + font-size: 14px; + } + + .leaderboard-entry.top-score { + background: linear-gradient(135deg, #ffd700 0%, #ffed4e 100%); + font-weight: 600; + } + + .leaderboard-rank { + color: #666; + font-weight: 600; + min-width: 30px; + } + + .leaderboard-name { + flex: 1; + color: #333; + } + + .leaderboard-score { + color: #667eea; + font-weight: 600; + } + + .container { + display: flex; + gap: 20px; + align-items: flex-start; + }

🐍 Snake Game 🐍

-
- -
-
Score: 0
-
High Score: 0
+
+
+ +
+
Score: 0
+
High Score: 0
+
+
Use Arrow Keys or WASD to move
+
+ +
+

🏆 Leaderboard

+
-
Use Arrow Keys or WASD to move

Game Over!

Final Score: 0

- + +
+ + diff --git a/typeracer/package.json b/typeracer/package.json new file mode 100644 index 0000000..f7f00b9 --- /dev/null +++ b/typeracer/package.json @@ -0,0 +1,16 @@ +{ + "name": "typeracer-game", + "version": "1.0.0", + "type": "module", + "description": "Code typeracer game", + "main": "server.js", + "scripts": { + "start": "node server.js" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "express": "^5.1.0" + } +} diff --git a/typeracer/server.js b/typeracer/server.js new file mode 100644 index 0000000..33cf15a --- /dev/null +++ b/typeracer/server.js @@ -0,0 +1,53 @@ +import express from 'express'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const app = express(); +const PORT = 5175; + +console.log('Setting up typeracer server with manual proxy...'); + +// Manual proxy for API requests +app.use('/api', express.json(), async (req, res) => { + console.log('Proxying request:', req.method, req.url); + try { + const targetUrl = `http://localhost:5000/api${req.url}`; + console.log('Target URL:', targetUrl); + + // Filter out problematic headers + const headers = {}; + if (req.headers['content-type']) { + headers['content-type'] = req.headers['content-type']; + } + + const response = await fetch(targetUrl, { + method: req.method, + headers: headers, + body: + req.method !== 'GET' && req.method !== 'HEAD' + ? JSON.stringify(req.body) + : undefined, + }); + + const data = await response.text(); + res.status(response.status).send(data); + } catch (error) { + console.error('Proxy error:', error); + res.status(500).json({ error: 'Proxy error' }); + } +}); + +// Serve static files +app.use(express.static(__dirname)); + +// Serve the typeracer game +app.get('/', (req, res) => { + res.sendFile(path.join(__dirname, 'index.html')); +}); + +app.listen(PORT, () => { + console.log(`Typeracer game running on http://localhost:${PORT}`); +}); From 6d77724fceb3fd5b1555adba1d6be1adb95a3df3 Mon Sep 17 00:00:00 2001 From: MentatBot <160964065+MentatBot@users.noreply.github.com> Date: Tue, 7 Oct 2025 23:58:12 +0000 Subject: [PATCH 22/82] Fix typeracer input handling for multi-line code - Change from multi-character to single-character input - Prevent default behavior for Enter and Tab keys - Auto-advance through newlines - Clear input after each character - Fixes issue where typing gets stuck on second line - Update version to v1.0.1 Mentat precommit script passed. Log: https://mentat.ai/gh/AbanteAI/party/log/c28fc50a-1dbe-4dc4-bd83-6b316b234ee9 Co-authored-by: granawkins <50287275+granawkins@users.noreply.github.com> --- typeracer/index.html | 41 ++++++++++++++++++++++++++++------------- 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/typeracer/index.html b/typeracer/index.html index 988353b..98ee3d6 100644 --- a/typeracer/index.html +++ b/typeracer/index.html @@ -206,7 +206,7 @@

🏎️ Code Typeracer 🏎️

-
v1.0.0
+
v1.0.1
@@ -383,6 +383,15 @@

🏆 Leaderboard

); } + inputArea.addEventListener('keydown', (e) => { + if (!isRacing) return; + + // Prevent default for Enter and Tab + if (e.key === 'Enter' || e.key === 'Tab') { + e.preventDefault(); + } + }); + inputArea.addEventListener('input', (e) => { if (!isRacing) return; @@ -392,28 +401,34 @@

🏆 Leaderboard

} const typed = e.target.value; - const expected = currentSnippet.slice( - 0, - currentPosition + typed.length - ); + if (typed.length === 0) return; - if (typed === expected.slice(currentPosition)) { - // Correct so far - currentPosition += typed.length; - correctChars += typed.length; - totalChars += typed.length; + const lastChar = typed[typed.length - 1]; + const expectedChar = currentSnippet[currentPosition]; + + // Check if the character is correct + if (lastChar === expectedChar) { + correctChars++; + currentPosition++; e.target.value = ''; + // Auto-advance on newline + if (expectedChar === '\n') { + // Already advanced position + } + if (currentPosition >= currentSnippet.length) { finishRace(); } else { updateDisplay(); } } else { - // Incorrect - totalChars++; - updateStats(); + // Incorrect character - don't advance + e.target.value = ''; } + + totalChars++; + updateStats(); }); // Initialize From 7ee38b1709dd5804a78b93dd6d1e2bcf3ed1f72b Mon Sep 17 00:00:00 2001 From: MentatBot <160964065+MentatBot@users.noreply.github.com> Date: Tue, 7 Oct 2025 23:59:57 +0000 Subject: [PATCH 23/82] Make Enter key work for newlines in typeracer - Handle Enter key in keydown event - Enter advances through newlines in code - Removed auto-skip newlines (user must press Enter) - Gives better "feel" for typing code - Update version to v1.0.3 Mentat precommit script passed. Log: https://mentat.ai/gh/AbanteAI/party/log/db31fe4f-a454-4488-a663-95cbb4e24a47 Co-authored-by: granawkins <50287275+granawkins@users.noreply.github.com> --- typeracer/index.html | 516 ++++++++++++++++++++++--------------------- 1 file changed, 269 insertions(+), 247 deletions(-) diff --git a/typeracer/index.html b/typeracer/index.html index 98ee3d6..00cb3ec 100644 --- a/typeracer/index.html +++ b/typeracer/index.html @@ -206,7 +206,7 @@

🏎️ Code Typeracer 🏎️

-
v1.0.1
+
v1.0.3
@@ -247,255 +247,277 @@

🏆 Leaderboard

From 53f85afed79c729155b83fcdbda4f32d96a83416 Mon Sep 17 00:00:00 2001 From: MentatBot <160964065+MentatBot@users.noreply.github.com> Date: Wed, 8 Oct 2025 00:02:05 +0000 Subject: [PATCH 24/82] Fix JavaScript syntax error in typeracer - Remove stray closing tag that broke the script - Fixes "startRace is not defined" error - Update version to v1.0.4 Mentat precommit script passed. Log: https://mentat.ai/gh/AbanteAI/party/log/13332e93-c9b2-4440-80f9-3e11d9e0bac4 Co-authored-by: granawkins <50287275+granawkins@users.noreply.github.com> --- typeracer/index.html | 423 +++++++++++++++++++++---------------------- 1 file changed, 211 insertions(+), 212 deletions(-) diff --git a/typeracer/index.html b/typeracer/index.html index 00cb3ec..157ff38 100644 --- a/typeracer/index.html +++ b/typeracer/index.html @@ -206,7 +206,7 @@

🏎️ Code Typeracer 🏎️

-
v1.0.3
+
v1.0.4
@@ -247,9 +247,9 @@

🏆 Leaderboard

From 675bf9b0f498b3e551d6536b9a5f06367ead97e4 Mon Sep 17 00:00:00 2001 From: MentatBot <160964065+MentatBot@users.noreply.github.com> Date: Wed, 8 Oct 2025 00:03:58 +0000 Subject: [PATCH 25/82] Add @techfren credit and manually add biobootloader's score MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add "❤️ @techfren" above leaderboards in both games - Manually add biobootloader's score of 24 to snake leaderboard - Update snake to v1.0.9 - Update typeracer to v1.0.5 Mentat precommit script passed. Log: https://mentat.ai/gh/AbanteAI/party/log/c8d39fba-d110-4cff-ab7b-cce1eeded02a Co-authored-by: granawkins <50287275+granawkins@users.noreply.github.com> --- snake/index.html | 12 +++++++++++- typeracer/index.html | 12 +++++++++++- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/snake/index.html b/snake/index.html index 4a1afa2..c63d977 100644 --- a/snake/index.html +++ b/snake/index.html @@ -175,7 +175,7 @@

🐍 Snake Game 🐍

font-family: monospace; " > - v1.0.8 + v1.0.9
@@ -230,6 +230,16 @@

🐍 Snake Game 🐍

+
+ ❤️ @techfren +

🏆 Leaderboard

diff --git a/typeracer/index.html b/typeracer/index.html index 157ff38..23319dd 100644 --- a/typeracer/index.html +++ b/typeracer/index.html @@ -206,7 +206,7 @@

🏎️ Code Typeracer 🏎️

-
v1.0.4
+
v1.0.5
@@ -241,6 +241,16 @@

🏎️ Code Typeracer 🏎️

+
+ ❤️ @techfren +

🏆 Leaderboard

From 4a681fe6855eb6137edfe294d188a72806f7a248 Mon Sep 17 00:00:00 2001 From: MentatBot <160964065+MentatBot@users.noreply.github.com> Date: Wed, 8 Oct 2025 00:07:22 +0000 Subject: [PATCH 26/82] Add Tab key support to typeracer game - Tab key now works for typing indentation - Accepts Tab for both tab characters and spaces - Similar handling to Enter key for newlines - Update version to v1.0.6 Mentat precommit script passed. Log: https://mentat.ai/gh/AbanteAI/party/log/36216a3d-a665-4d33-944f-27a8297e1011 Co-authored-by: granawkins <50287275+granawkins@users.noreply.github.com> --- typeracer/index.html | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/typeracer/index.html b/typeracer/index.html index 23319dd..43a5939 100644 --- a/typeracer/index.html +++ b/typeracer/index.html @@ -206,7 +206,7 @@

🏎️ Code Typeracer 🏎️

-
v1.0.5
+
v1.0.6
@@ -422,9 +422,31 @@

🏆 Leaderboard

updateStats(); } - // Prevent Tab + // Handle Tab key for indentation if (e.key === 'Tab') { e.preventDefault(); + + if (!startTime) { + startTime = Date.now(); + timerInterval = setInterval(updateStats, 100); + } + + const expectedChar = currentSnippet[currentPosition]; + + if (expectedChar === '\t' || expectedChar === ' ') { + // Accept tab for either tab character or spaces + correctChars++; + currentPosition++; + + if (currentPosition >= currentSnippet.length) { + finishRace(); + } else { + updateDisplay(); + } + } + + totalChars++; + updateStats(); } }); From 152e407b98393f8b35369502660e1ec6a74fe9af Mon Sep 17 00:00:00 2001 From: MentatBot <160964065+MentatBot@users.noreply.github.com> Date: Wed, 8 Oct 2025 00:11:04 +0000 Subject: [PATCH 27/82] Add toggle button and responsive layout to both games - Add floating toggle button to show/hide leaderboard - Button positioned at bottom right - Responsive layout: stacks vertically on mobile (<768px) - Improves usability on small screens - Snake: v1.0.10 - Typeracer: v1.0.7 Mentat precommit script passed. Log: https://mentat.ai/gh/AbanteAI/party/log/9c1acdcc-72bb-42cc-a257-541f0b49160f Co-authored-by: granawkins <50287275+granawkins@users.noreply.github.com> --- snake/index.html | 61 ++++++++++++++++++++++++++++++++++++++++++-- typeracer/index.html | 61 ++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 118 insertions(+), 4 deletions(-) diff --git a/snake/index.html b/snake/index.html index c63d977..5b22145 100644 --- a/snake/index.html +++ b/snake/index.html @@ -158,6 +158,41 @@ gap: 20px; align-items: flex-start; } + + .leaderboard.hidden { + display: none; + } + + .toggle-button { + position: fixed; + bottom: 20px; + right: 20px; + background: #667eea; + color: white; + border: none; + padding: 12px 20px; + border-radius: 25px; + font-size: 14px; + font-weight: 600; + cursor: pointer; + box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4); + z-index: 1000; + } + + .toggle-button:hover { + background: #5568d3; + } + + @media (max-width: 768px) { + .container { + flex-direction: column; + } + + .leaderboard { + width: 100%; + max-width: 500px; + } + } @@ -175,7 +210,7 @@

🐍 Snake Game 🐍

font-family: monospace; " > - v1.0.9 + v1.0.10
@@ -229,7 +264,7 @@

🐍 Snake Game 🐍

Use Arrow Keys or WASD to move
-
+
+ Hide Leaderboard + +

Game Over!

Final Score: 0

@@ -354,6 +397,20 @@

Game Over!

// Initialize leaderboard display updateLeaderboardDisplay(); + // Toggle leaderboard visibility + function toggleLeaderboard() { + const leaderboard = document.getElementById('leaderboard'); + const button = document.getElementById('toggleButton'); + + if (leaderboard.classList.contains('hidden')) { + leaderboard.classList.remove('hidden'); + button.textContent = 'Hide Leaderboard'; + } else { + leaderboard.classList.add('hidden'); + button.textContent = 'Show Leaderboard'; + } + } + // Difficulty slider const speedSlider = document.getElementById('speedSlider'); const difficultyLabel = document.getElementById('difficultyLabel'); diff --git a/typeracer/index.html b/typeracer/index.html index 43a5939..e1cbb30 100644 --- a/typeracer/index.html +++ b/typeracer/index.html @@ -202,11 +202,46 @@ color: #667eea; font-weight: 600; } + + .leaderboard.hidden { + display: none; + } + + .toggle-button { + position: fixed; + bottom: 20px; + right: 20px; + background: #667eea; + color: white; + border: none; + padding: 12px 20px; + border-radius: 25px; + font-size: 14px; + font-weight: 600; + cursor: pointer; + box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4); + z-index: 1000; + } + + .toggle-button:hover { + background: #5568d3; + } + + @media (max-width: 768px) { + .container { + flex-direction: column; + } + + .leaderboard { + width: 100%; + max-width: 500px; + } + }

🏎️ Code Typeracer 🏎️

-
v1.0.6
+
v1.0.7
@@ -240,7 +275,7 @@

🏎️ Code Typeracer 🏎️

-
+
+ Hide Leaderboard + + + From 15ef3ecc9ce46c8592e585820c73b45a09c7dda4 Mon Sep 17 00:00:00 2001 From: MentatBot <160964065+MentatBot@users.noreply.github.com> Date: Thu, 9 Oct 2025 17:35:45 +0000 Subject: [PATCH 72/82] Fix critical bug: call drawEnemySnakes() instead of drawEnemySnake() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Bug:** Function name mismatch causing JavaScript error - Function was renamed to `drawEnemySnakes()` (plural) - But one call site still used `drawEnemySnake()` (singular) - This caused a "function not defined" error - Prevented the game from rendering properly **Fix:** Updated the function call to use the correct name **Now the game will:** - Render all 3 enemy snakes - Display food properly - Work correctly on page load - Start when arrow keys are pressed This was preventing the entire game from working! 🐛→✅ Mentat precommit script passed. Log: https://mentat.ai/gh/AbanteAI/party/log/a4bed31e-8ece-4e9d-ae67-ed04e7956ce6 Co-authored-by: granawkins <50287275+granawkins@users.noreply.github.com> --- snake/index.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/snake/index.html b/snake/index.html index 1f2960f..ed4066f 100644 --- a/snake/index.html +++ b/snake/index.html @@ -2092,8 +2092,8 @@

${lobby.name} Date: Thu, 9 Oct 2025 17:39:45 +0000 Subject: [PATCH 73/82] Fix Socket.io to use relative URLs instead of localhost MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Issue:** Hardcoded `localhost:5000` breaks in forwarded/production environments **Fix:** Changed Socket.io connections to use relative URLs: - `io('http://localhost:5000')` → `io()` - Socket.io automatically uses current host when no URL provided **Benefits:** - Works in local development - Works in forwarded environments (Mentat interface) - Works in production - No hardcoded URLs **Affected features:** - Multiplayer lobby list - Multiplayer game connection Now multiplayer will work properly in the Mentat chat interface! 🎮✨ Mentat precommit script passed. Log: https://mentat.ai/gh/AbanteAI/party/log/c75eef75-8891-49a2-83d3-6e33a96f1a63 Co-authored-by: granawkins <50287275+granawkins@users.noreply.github.com> --- snake/index.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/snake/index.html b/snake/index.html index ed4066f..c2b3a46 100644 --- a/snake/index.html +++ b/snake/index.html @@ -1424,7 +1424,7 @@

Game Over!

function requestLobbyList() { // Connect temporarily to get lobby list if (!socket) { - socket = io('http://localhost:5000'); + socket = io(); socket.on('connect', () => { socket.emit('get_lobbies'); }); @@ -1496,7 +1496,7 @@

${lobby.name} { From ebf936a5a70b66524356d77a8562033912c7bd25 Mon Sep 17 00:00:00 2001 From: MentatBot <160964065+MentatBot@users.noreply.github.com> Date: Thu, 9 Oct 2025 20:02:45 +0000 Subject: [PATCH 74/82] Make enemies aggressive: spawn spread out, chase snakes, cut tails MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Major Changes:** ### 1. Spread Out Spawning - Enemies spawn in different corners/areas - Positions: top-left, top-right, bottom-left, bottom-right, top-center - No more spawning in same spot ### 2. Aggressive AI - Each enemy has random aggression level (0-1) - High aggression (>0.6): chase nearest snake - Targets player OR other enemies - Switches to food if no snakes nearby - Low aggression: chase food (passive) - Creates dynamic, unpredictable behavior ### 3. Tail-Cutting Combat **Enemies can cut player's tail:** - Player loses tail segments when enemy crosses - Makes game more challenging **Enemies can kill each other:** - Enemy dies when another crosses its tail - Respawns after 2 seconds in random corner - Creates enemy vs enemy combat ### 4. Territorial Behavior - Enemies compete for space - Chase each other aggressively - Try to eliminate competition **Result:** Much more dynamic, challenging, and exciting gameplay! Enemies now actively hunt the player and each other, creating a chaotic battle arena! 🐍💥⚔️ Mentat precommit script passed. Log: https://mentat.ai/gh/AbanteAI/party/log/265f9106-a93f-4d74-94dd-2aa3d7d20e65 Co-authored-by: granawkins <50287275+granawkins@users.noreply.github.com> --- snake/index.html | 136 ++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 116 insertions(+), 20 deletions(-) diff --git a/snake/index.html b/snake/index.html index c2b3a46..b31e4ad 100644 --- a/snake/index.html +++ b/snake/index.html @@ -1008,19 +1008,25 @@

Game Over!

function initializeEnemies() { enemySnakes = []; + // Spawn enemies in different corners/areas + const spawnPositions = [ + { x: 2, y: 2 }, // Top-left + { x: tileCount - 3, y: 2 }, // Top-right + { x: 2, y: tileCount - 3 }, // Bottom-left + { x: tileCount - 3, y: tileCount - 3 }, // Bottom-right + { x: Math.floor(tileCount / 2), y: 2 }, // Top-center + ]; + for (let i = 0; i < NUM_ENEMIES; i++) { + const spawnPos = spawnPositions[i % spawnPositions.length]; enemySnakes.push({ - snake: [ - { - x: Math.floor(Math.random() * tileCount), - y: Math.floor(Math.random() * tileCount), - }, - ], + snake: [{ x: spawnPos.x, y: spawnPos.y }], dx: 1, dy: 0, moveCounter: 0, color: ENEMY_COLORS[i % ENEMY_COLORS.length], alive: true, + aggressionLevel: Math.random(), // 0-1, higher = more aggressive }); } } @@ -1827,24 +1833,71 @@

${lobby.name} 0.6) { + // High aggression: chase nearest snake (player or other enemies) + let nearestSnake = null; + let minDist = Infinity; + + // Check player snake + if (snake.length > 0) { + const playerHead = snake[0]; + const dist = + Math.abs(head.x - playerHead.x) + + Math.abs(head.y - playerHead.y); + if (dist < minDist) { + minDist = dist; + nearestSnake = playerHead; + } + } + + // Check other enemy snakes + enemySnakes.forEach((otherEnemy) => { + if ( + otherEnemy !== enemy && + otherEnemy.alive && + otherEnemy.snake.length > 0 + ) { + const otherHead = otherEnemy.snake[0]; + const dist = + Math.abs(head.x - otherHead.x) + + Math.abs(head.y - otherHead.y); + if (dist < minDist) { + minDist = dist; + nearestSnake = otherHead; + } + } + }); + + if (nearestSnake && minDist < 10) { + target = nearestSnake; + } + } + + // If no target or low aggression, chase food + if (!target) { + let nearestFood = foods[0]; + let minDist = + Math.abs(head.x - nearestFood.x) + + Math.abs(head.y - nearestFood.y); + + for (let i = 1; i < foods.length; i++) { + const dist = + Math.abs(head.x - foods[i].x) + Math.abs(head.y - foods[i].y); + if (dist < minDist) { + minDist = dist; + nearestFood = foods[i]; + } } + target = nearestFood; } - // Decide direction - const dx = nearestFood.x - head.x; - const dy = nearestFood.y - head.y; + // Decide direction towards target + const dx = target.x - head.x; + const dy = target.y - head.y; if (Math.abs(dx) > Math.abs(dy)) { enemy.dx = dx > 0 ? 1 : -1; @@ -1865,6 +1918,49 @@

${lobby.name} { + if (otherEnemy !== enemy && otherEnemy.alive) { + for (let i = 1; i < otherEnemy.snake.length; i++) { + if ( + newHead.x === otherEnemy.snake[i].x && + newHead.y === otherEnemy.snake[i].y + ) { + // Enemy cut another enemy's tail - kill it + otherEnemy.alive = false; + setTimeout(() => { + const spawnPositions = [ + { x: 2, y: 2 }, + { x: tileCount - 3, y: 2 }, + { x: 2, y: tileCount - 3 }, + { x: tileCount - 3, y: tileCount - 3 }, + { x: Math.floor(tileCount / 2), y: 2 }, + ]; + const spawnPos = + spawnPositions[ + Math.floor(Math.random() * spawnPositions.length) + ]; + otherEnemy.snake = [{ x: spawnPos.x, y: spawnPos.y }]; + otherEnemy.dx = 1; + otherEnemy.dy = 0; + otherEnemy.moveCounter = 0; + otherEnemy.alive = true; + }, 2000); + break; + } + } + } + }); + // Check if enemy ate food let ateFood = false; for (let i = 0; i < foods.length; i++) { From e138a7f6fac426b367cd411c75d3f00b1258e7c5 Mon Sep 17 00:00:00 2001 From: MentatBot <160964065+MentatBot@users.noreply.github.com> Date: Thu, 9 Oct 2025 21:30:12 +0000 Subject: [PATCH 75/82] Add error handling to prevent game crashes and blank screen **Issue:** Game turns blank after short time due to unhandled errors **Fix:** Added comprehensive error handling: ### 1. Try-Catch in drawGame() - Wraps entire game rendering logic - Logs errors to console for debugging - Attempts automatic recovery by restarting game - Prevents blank screen from crashes ### 2. Try-Catch in singlePlayerGameLoop() - Protects requestAnimationFrame loop - Stops loop gracefully on error - Prevents infinite error loops - Logs errors for debugging ### 3. Error Recovery - Automatic restart after 1 second - Cleans up game state properly - User sees error in console but game recovers **Benefits:** - Game won't go blank from errors - Easier to debug issues - Better user experience - Graceful error handling **Next:** User can send console errors to help identify root cause of crashes Mentat precommit script failed. Log: https://mentat.ai/gh/AbanteAI/party/log/daec939c-31b2-492b-bcf1-9e71d2b05328 Co-authored-by: granawkins <50287275+granawkins@users.noreply.github.com> --- snake/index.html | 55 +++++++++++++++++++++++++++++++++--------------- 1 file changed, 38 insertions(+), 17 deletions(-) diff --git a/snake/index.html b/snake/index.html index b31e4ad..2af0005 100644 --- a/snake/index.html +++ b/snake/index.html @@ -2064,12 +2064,13 @@

${lobby.name}${lobby.name} { + console.log('Attempting to recover...'); + restartGame(); + }, 1000); + } + } } function placeFoodAt(index) { @@ -2346,17 +2358,26 @@

${lobby.name}= gameSpeed) { - lastFrameTime = currentTime - (deltaTime % gameSpeed); - drawGame(); - } + // Update game logic based on gameSpeed + if (deltaTime >= gameSpeed) { + lastFrameTime = currentTime - (deltaTime % gameSpeed); + drawGame(); + } - gameLoopId = requestAnimationFrame(singlePlayerGameLoop); + gameLoopId = requestAnimationFrame(singlePlayerGameLoop); + } catch (error) { + console.error('Error in game loop:', error); + gameRunning = false; + if (gameLoopId) { + cancelAnimationFrame(gameLoopId); + gameLoopId = null; + } + } } // Keyboard controls From 54560dc4c9704fff5b43cc246bc13847241b5e14 Mon Sep 17 00:00:00 2001 From: MentatBot <160964065+MentatBot@users.noreply.github.com> Date: Thu, 9 Oct 2025 22:01:03 +0000 Subject: [PATCH 76/82] Add OpenWebUI integration with Docker Compose MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **New Features:** ### OpenWebUI Integration 💬 - AI chat interface using OpenWebUI - Powered by Pollinations API (free, no API key needed) - Runs on port 3000 - Separate from main app ### Docker Setup 🐳 **docker-compose.yml:** - OpenWebUI service configuration - Port forwarding (3000:8080) - Volume for persistent data - Network configuration - Auto-restart enabled **Configuration:** - Pre-configured for Pollinations API - Authentication disabled by default - Signup enabled - Custom branding ("Mentat Party Chat") ### Documentation 📚 **Updated README.md:** - Project overview and features - Quick start guide - OpenWebUI setup instructions - Docker commands - Project structure - Development workflow **Added .env.example:** - Environment variable template - Pollinations API configuration - OpenWebUI settings - Comments for customization **Added .dockerignore:** - Optimized Docker builds - Excludes node_modules, logs, etc. ### Architecture ``` Main App (5173) ← existing Snake Game ← existing OpenWebUI (3000) ← NEW ``` **Usage:** ```bash docker-compose up -d # Start OpenWebUI docker-compose down # Stop services docker-compose logs -f # View logs ``` Users can now chat with AI via Pollinations while playing games! 🎮🤖 Mentat precommit script failed. Log: https://mentat.ai/gh/AbanteAI/party/log/9e2da11d-de8a-4cff-932b-7366f436ac86 Co-authored-by: granawkins <50287275+granawkins@users.noreply.github.com> --- .dockerignore | 43 ++++++++++++++++++++++ .env.example | 14 ++++++++ README.md | 90 ++++++++++++++++++++++++++++++++++++++++++++-- docker-compose.yml | 28 +++++++++++++++ 4 files changed, 172 insertions(+), 3 deletions(-) create mode 100644 .dockerignore create mode 100644 .env.example create mode 100644 docker-compose.yml diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..f35e7bc --- /dev/null +++ b/.dockerignore @@ -0,0 +1,43 @@ +# Dependencies +node_modules +**/node_modules +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Build outputs +dist +**/dist +build +.next + +# Development +.git +.gitignore +.env +.env.local +.env.*.local + +# IDE +.vscode +.idea +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db + +# Logs +logs +*.log +.mentat/logs + +# Testing +coverage +.nyc_output + +# Misc +*.md +!README.md diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..b9b4bd1 --- /dev/null +++ b/.env.example @@ -0,0 +1,14 @@ +# OpenWebUI Configuration +OLLAMA_BASE_URL=https://text.pollinations.ai +WEBUI_AUTH=false +WEBUI_NAME=Mentat Party Chat +WEBUI_URL=http://localhost:3000 +DEFAULT_MODELS=openai +ENABLE_SIGNUP=true +ENABLE_LOGIN=false + +# Pollinations API Configuration +# Note: Pollinations is free and doesn't require an API key +# But you can configure other providers here if needed +# OPENAI_API_KEY=your_key_here +# ANTHROPIC_API_KEY=your_key_here diff --git a/README.md b/README.md index ca105bc..8f0f6b1 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,33 @@ -# Mentat Template JS +# Mentat Party -A full-stack JavaScript template project with React frontend and Express backend, both using TypeScript. +An experimental massively-multiplayer Mentat project featuring interactive games and AI chat. + +## Features + +- 🐍 **Snake Game** - Single-player and multiplayer snake with AI enemies +- 💬 **AI Chat** - OpenWebUI integration with Pollinations API +- 🎮 **Interactive Games** - More games coming soon! +- 🤖 **Mentat Integration** - AI-powered development and features + +## Quick Start + +### Running the Main App + +```bash +npm install +npm run dev +``` + +### Running OpenWebUI (AI Chat) + +```bash +docker-compose up -d +``` + +Then visit: +- Main App: http://localhost:5173 +- Snake Game: http://localhost:5173/snake +- AI Chat: http://localhost:3000 ## Getting Started @@ -21,4 +48,61 @@ You're all set! You can begin using Mentat by - Creating a new issue and tagging '@MentatBot' - Pushing PRs to your repository and having Mentat review them -- Chatting wtih Mentat about your project from the [Mentat website](https://mentat.ai) +- Chatting with Mentat about your project from the [Mentat website](https://mentat.ai) + +## OpenWebUI Setup + +OpenWebUI provides a chat interface powered by Pollinations AI. + +### Configuration + +1. Copy `.env.example` to `.env` (optional, defaults work out of the box) +2. Run `docker-compose up -d` +3. Access at http://localhost:3000 + +### Features + +- Free AI chat via Pollinations API +- No API key required +- Multiple model support +- Clean, modern interface + +### Customization + +Edit `docker-compose.yml` to customize: +- Port (default: 3000) +- UI name and branding +- Authentication settings +- Model providers + +## Project Structure + +``` +├── client/ # React frontend +├── server/ # Express backend +├── snake/ # Snake game +├── .mentat/ # Mentat configuration +└── docker-compose.yml # OpenWebUI setup +``` + +## Development + +- `npm run dev` - Start development servers +- `npm run build` - Build for production +- `npm test` - Run tests +- `npm run lint` - Lint code +- `npm run format` - Format code + +## Docker Services + +- **open-webui** - AI chat interface (port 3000) + +To stop services: +```bash +docker-compose down +``` + +To view logs: +```bash +docker-compose logs -f +``` diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..91e15a6 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,28 @@ +version: '3.8' + +services: + open-webui: + image: ghcr.io/open-webui/open-webui:main + container_name: open-webui + ports: + - "3000:8080" + environment: + - OLLAMA_BASE_URL=https://text.pollinations.ai + - WEBUI_AUTH=false + - WEBUI_NAME=Mentat Party Chat + - WEBUI_URL=http://localhost:3000 + - DEFAULT_MODELS=openai + - ENABLE_SIGNUP=true + - ENABLE_LOGIN=false + volumes: + - open-webui-data:/app/backend/data + restart: unless-stopped + networks: + - mentat-network + +networks: + mentat-network: + driver: bridge + +volumes: + open-webui-data: From bdbd79934b22b0b336b509975adb1f2883d2a8e8 Mon Sep 17 00:00:00 2001 From: MentatBot <160964065+MentatBot@users.noreply.github.com> Date: Thu, 9 Oct 2025 22:39:43 +0000 Subject: [PATCH 77/82] Create AI chat interface with Pollinations API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **New Feature: AI Chat** 💬🤖 Created `client/src/Chat.tsx` - a custom OpenWebUI-inspired chat interface ### Features: - **Streaming responses** - Real-time AI responses - **Model selector** - Choose from openai, mistral, claude, llama - **Clean UI** - Modern gradient design matching app theme - **Message history** - Full conversation context - **Keyboard shortcuts** - Enter to send, Shift+Enter for new line - **Loading states** - Visual feedback during responses - **Error handling** - Graceful error messages ### Technical Details: - Uses Pollinations API (`text.pollinations.ai/openai`) - OpenAI-compatible format - Streaming via ReadableStream - React hooks for state management - Responsive design - Auto-scroll to latest message ### UI Design: - Purple gradient background - Glass-morphism effects - User messages: white bubbles (right) - AI messages: dark bubbles (left) - Welcome screen for empty state **Next:** Add routing and navigation to integrate into main app Mentat precommit script failed. Log: https://mentat.ai/gh/AbanteAI/party/log/72953867-2e25-4aeb-a72f-c34413e271a4 Co-authored-by: granawkins <50287275+granawkins@users.noreply.github.com> --- client/src/Chat.tsx | 285 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 285 insertions(+) create mode 100644 client/src/Chat.tsx diff --git a/client/src/Chat.tsx b/client/src/Chat.tsx new file mode 100644 index 0000000..ab31360 --- /dev/null +++ b/client/src/Chat.tsx @@ -0,0 +1,285 @@ +import { useState, useRef, useEffect } from 'react'; + +interface Message { + role: 'user' | 'assistant' | 'system'; + content: string; +} + +const AVAILABLE_MODELS = [ + 'openai', + 'mistral', + 'claude', + 'llama', +]; + +export default function Chat() { + const [messages, setMessages] = useState([]); + const [input, setInput] = useState(''); + const [isLoading, setIsLoading] = useState(false); + const [selectedModel, setSelectedModel] = useState('openai'); + const messagesEndRef = useRef(null); + + const scrollToBottom = () => { + messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); + }; + + useEffect(() => { + scrollToBottom(); + }, [messages]); + + const sendMessage = async () => { + if (!input.trim() || isLoading) return; + + const userMessage: Message = { role: 'user', content: input }; + setMessages((prev) => [...prev, userMessage]); + setInput(''); + setIsLoading(true); + + try { + const response = await fetch( + 'https://text.pollinations.ai/openai/chat/completions', + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + model: selectedModel, + messages: [...messages, userMessage], + stream: true, + }), + } + ); + + if (!response.ok) { + throw new Error('Failed to get response'); + } + + const reader = response.body?.getReader(); + const decoder = new TextDecoder(); + let assistantMessage = ''; + + if (reader) { + setMessages((prev) => [ + ...prev, + { role: 'assistant', content: '' }, + ]); + + while (true) { + const { done, value } = await reader.read(); + if (done) break; + + const chunk = decoder.decode(value); + const lines = chunk.split('\n'); + + for (const line of lines) { + if (line.startsWith('data: ')) { + const data = line.slice(6); + if (data === '[DONE]') continue; + + try { + const parsed = JSON.parse(data); + const content = parsed.choices?.[0]?.delta?.content; + if (content) { + assistantMessage += content; + setMessages((prev) => { + const newMessages = [...prev]; + newMessages[newMessages.length - 1] = { + role: 'assistant', + content: assistantMessage, + }; + return newMessages; + }); + } + } catch (e) { + // Skip invalid JSON + } + } + } + } + } + } catch (error) { + console.error('Error:', error); + setMessages((prev) => [ + ...prev, + { + role: 'assistant', + content: 'Sorry, I encountered an error. Please try again.', + }, + ]); + } finally { + setIsLoading(false); + } + }; + + const handleKeyPress = (e: React.KeyboardEvent) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + sendMessage(); + } + }; + + return ( +
+ {/* Header */} +
+

+ 🤖 AI Chat +

+
+ +
+
+ + {/* Messages */} +
+ {messages.length === 0 && ( +
+

+ 👋 Welcome! +

+

+ Start a conversation with AI powered by Pollinations +

+
+ )} + + {messages.map((message, index) => ( +
+
+ {message.content} +
+
+ ))} + +
+
+ + {/* Input */} +
+
+