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/.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/health-check.sh b/.mentat/health-check.sh new file mode 100755 index 0000000..57e6815 --- /dev/null +++ b/.mentat/health-check.sh @@ -0,0 +1,57 @@ +#!/bin/bash + +echo "🏥 Running Mentat Party Health Check..." +echo "" + +# Check branch +BRANCH=$(git branch --show-current) +echo "📍 Current branch: $BRANCH" +if [ "$BRANCH" != "mentat-1" ]; then + echo "⚠️ WARNING: You're not on the mentat-1 branch!" + echo " Run: git checkout mentat-1" +fi +echo "" + +# Check if client is built +if [ -d "client/dist" ]; then + echo "✅ Client is built (client/dist exists)" +else + echo "❌ Client is NOT built!" + echo " Run: cd client && npm run build" +fi +echo "" + +# Check if node_modules exist +if [ -d "node_modules" ] && [ -d "client/node_modules" ] && [ -d "server/node_modules" ]; then + echo "✅ Dependencies are installed" +else + echo "❌ Dependencies are NOT fully installed!" + echo " Run: npm install" +fi +echo "" + +# Check if servers are running +if lsof -i :5000 > /dev/null 2>&1; then + echo "✅ Server is running on port 5000" +else + echo "⚠️ Server is NOT running on port 5000" + echo " Run: cd server && npm run dev" +fi + +if lsof -i :5173 > /dev/null 2>&1; then + echo "✅ Client dev server is running on port 5173" +else + echo "⚠️ Client dev server is NOT running on port 5173" + echo " Run: cd client && npm run dev" +fi +echo "" + +# Test API endpoint +if curl -s http://localhost:5000/api/messages > /dev/null 2>&1; then + echo "✅ API is responding" +else + echo "❌ API is NOT responding" +fi +echo "" + +echo "🏥 Health check complete!" 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/.mentat/run/auto/0-server.sh b/.mentat/run/auto/0-server.sh new file mode 100755 index 0000000..a6a2b97 --- /dev/null +++ b/.mentat/run/auto/0-server.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +# Start the Express server in development mode +cd server +npm run dev diff --git a/.mentat/run/auto/1-client.sh b/.mentat/run/auto/1-client.sh new file mode 100755 index 0000000..35fdf81 --- /dev/null +++ b/.mentat/run/auto/1-client.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +# Start the React client in development mode +cd client +npm run dev diff --git a/.mentat/run/auto/2-snake.sh b/.mentat/run/auto/2-snake.sh new file mode 100755 index 0000000..1c4f263 --- /dev/null +++ b/.mentat/run/auto/2-snake.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +# Start the snake game server +cd snake +npm install --silent +npm start diff --git a/.mentat/setup.sh b/.mentat/setup.sh index dbe0ba3..e5d531c 100755 --- a/.mentat/setup.sh +++ b/.mentat/setup.sh @@ -3,3 +3,11 @@ # Install dependencies for both client and server # The root package.json has an install script that handles this npm install + +# Build the client so port 5000 works immediately +echo "Building client for production..." +cd client +npm run build +cd .. + +echo "✅ Setup complete! Client is built and ready." diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..0821bd0 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,212 @@ +# Agent Guide for Mentat Party + +This document provides essential information for AI agents working on this repository. + +## Repository Overview + +This is the **Mentat Party** repository - a public experiment where anyone can chat with AI agents and collaboratively build features. The repo contains: + +- **Client**: React 19 + TypeScript + Vite (chat interface, snake game, polls, stock ticker) +- **Server**: Express + TypeScript (REST API for messages, polls, leaderboards) +- **Snake Game**: Standalone game with 100+ features (particles, trails, achievements) + +## Critical Setup Steps + +### 1. Branch Awareness + +⚠️ **IMPORTANT**: The `main` branch contains only the template. All features are on the `mentat-1` branch! + +```bash +# Always check which branch you're on +git branch + +# If on main, switch to mentat-1 +git checkout mentat-1 +``` + +### 2. Install Dependencies + +```bash +# Install all dependencies (runs for both client and server) +npm install +``` + +### 3. Build the Client + +⚠️ **CRITICAL**: The client MUST be built for port 5000 to work! + +```bash +cd client +npm run build +``` + +This creates the `client/dist` directory that the Express server serves. + +### 4. Start Development Servers + +```bash +# Option 1: Start both servers (from root) +npm run dev + +# Option 2: Start individually +cd server && npm run dev # Port 5000 +cd client && npm run dev # Port 5173 +``` + +## Port Configuration + +- **Port 5173**: Vite dev server (hot reload, development) +- **Port 5000**: Express server (serves built client + API) +- **Port 5174**: Snake game standalone server + +**For users to see the app**: They need to access port 5000 (which serves the built client). + +## Common Issues + +### Issue: "Mentat Template JS" on Port 5000 + +**Cause**: Client hasn't been built +**Solution**: Run `cd client && npm run build` + +### Issue: Blank Page on Port 5173 + +**Cause**: Port forwarding issues or missing dependencies +**Solution**: + +1. Check dependencies: `cd client && npm install` +2. Use port 5000 instead (more reliable) + +### Issue: "Module not found" errors + +**Cause**: Dependencies not installed or wrong branch +**Solution**: + +1. Verify you're on `mentat-1` branch +2. Run `npm install` in root, client, and server directories + +## Development Workflow + +### Making Changes + +1. **Always work on `mentat-1` branch** +2. Make your changes +3. Test locally +4. **Rebuild client if you changed client code**: `cd client && npm run build` +5. Commit and push + +### Testing Changes + +```bash +# Run tests +npm test + +# Run linter +npm run lint + +# Format code +npm run format +``` + +## Key Files and Directories + +``` +. +├── client/ # React frontend +│ ├── src/ +│ │ ├── App.tsx # Main chat interface +│ │ ├── Router.tsx # Routing (/, /snake) +│ │ └── Snake.tsx # Snake game component +│ └── dist/ # Built client (MUST exist for port 5000!) +├── server/ # Express backend +│ └── src/ +│ ├── app.ts # Express app setup +│ └── server.ts # Server entry point +├── snake/ # Standalone snake game +│ └── index.html # Snake game with 100+ features +└── .mentat/ + ├── setup.sh # Run on agent start + ├── format.sh # Run before commits + └── preview/ # Auto-start scripts +``` + +## Features Overview + +### Chat Interface (Main App) + +- Real-time messaging +- Emoji reactions +- Polls with voting +- Stock ticker (AAPL, TSLA, etc.) +- User profiles with localStorage + +### Snake Game + +- 100+ feature variables +- Particle effects on food collection +- Snake trails with fade +- Dynamic background (changes with score) +- 10 achievements +- Custom snake skins (rainbow, fire, ocean, forest) +- Animated food +- Game statistics tracking +- Leaderboard integration + +## API Endpoints + +- `GET /api/messages` - Get all messages +- `POST /api/messages` - Send a message +- `POST /api/messages/:id/react` - Add reaction +- `GET /api/polls` - Get all polls +- `POST /api/polls` - Create a poll +- `POST /api/polls/:id/vote` - Vote on a poll +- `GET /api/stock/:symbol` - Get stock data +- `GET /api/snake/scores` - Get snake leaderboard +- `POST /api/snake/scores` - Submit snake score + +## Mentat Scripts + +### .mentat/setup.sh + +Runs automatically when agents start. Installs dependencies for both client and server. + +### .mentat/format.sh + +Runs automatically before commits. Formats code with Prettier and fixes linting issues. + +### .mentat/preview/ scripts + +Auto-start scripts for development servers: + +- `0-server.sh` - Starts Express server (port 5000) +- `1-client.sh` - Starts Vite dev server (port 5173) +- `2-snake.sh` - Starts snake game server (port 5174) + +## Tips for Agents + +1. **Always verify the branch** before making changes +2. **Build the client** after making frontend changes +3. **Test on port 5000** - it's more reliable for users +4. **Check logs** if something isn't working: `../logs/shell/` +5. **Use diagnostic tools**: TypeScript, ESLint, tests +6. **Document your changes** in commit messages +7. **Preserve existing features** when adding new ones + +## Getting Help + +If you encounter issues: + +1. Check this document first +2. Run diagnostics: `npm test`, `npm run lint` +3. Check if dependencies are installed: `npm install` +4. Verify you're on the right branch: `git branch` +5. Check if client is built: `ls client/dist` + +## Credits + +- Snake game features by @mcgdj (GitHub) +- Chat interface and infrastructure by various contributors +- Mentat system by AbanteAI + +--- + +**Remember**: This is a public experiment. Be helpful, be creative, and have fun! 🎉 diff --git a/README.md b/README.md index ca105bc..3c4c414 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,34 @@ -# 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 +49,64 @@ 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/client/package.json b/client/package.json index 48bb867..4bc6195 100644 --- a/client/package.json +++ b/client/package.json @@ -11,8 +11,11 @@ "preview": "vite preview" }, "dependencies": { + "chart.js": "^4.5.0", "react": "^19.0.0", - "react-dom": "^19.0.0" + "react-chartjs-2": "^5.3.0", + "react-dom": "^19.0.0", + "react-router-dom": "^7.9.4" }, "devDependencies": { "@testing-library/jest-dom": "^6.6.3", diff --git a/client/src/App.tsx b/client/src/App.tsx index 43b4b68..5e82e83 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -1,38 +1,312 @@ -import { useState, useEffect } from 'react'; -import mentatLogo from '/mentat.png'; +import { useState, useEffect, useRef, useCallback } from 'react'; +import { + Chart as ChartJS, + CategoryScale, + LinearScale, + PointElement, + LineElement, + Title, + Tooltip, + Legend, + Filler, +} from 'chart.js'; +import { Line } from 'react-chartjs-2'; + +ChartJS.register( + CategoryScale, + LinearScale, + PointElement, + LineElement, + Title, + Tooltip, + Legend, + Filler +); + +interface Message { + id: number; + username: string; + message: string; + timestamp: string; + reactions: { [emoji: string]: string[] }; + snakeScore: number; +} + +interface Poll { + id: number; + question: string; + options: string[]; + votes: { [option: string]: string[] }; + createdBy: string; + createdAt: string; +} function App() { - const [message, setMessage] = useState(null); + const [messages, setMessages] = useState([]); + const [polls, setPolls] = useState([]); + const [username, setUsername] = useState(''); + const [messageText, setMessageText] = useState(''); const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); + const [showPollForm, setShowPollForm] = useState(false); + const [pollQuestion, setPollQuestion] = useState(''); + const [pollOptions, setPollOptions] = useState(['', '']); + const [stockData, setStockData] = useState<{ + labels: string[]; + prices: number[]; + currentPrice: number; + change: number; + changePercent: number; + } | null>(null); + const messagesEndRef = useRef(null); + // Load username from localStorage useEffect(() => { - const fetchBackendMessage = async () => { - setLoading(true); - setError(null); + const savedUsername = localStorage.getItem('chatUsername'); + if (savedUsername) { + setUsername(savedUsername); + } + }, []); - try { - const response = await fetch('/api'); + // Save username to localStorage when it changes + useEffect(() => { + if (username) { + localStorage.setItem('chatUsername', username); + } + }, [username]); - if (!response.ok) { - throw new Error(`HTTP error ${response.status}`); - } + const scrollToBottom = () => { + // Only auto-scroll if user is near the bottom (within 100px) + const messagesDiv = messagesEndRef.current?.parentElement; + if (messagesDiv) { + const isNearBottom = + messagesDiv.scrollHeight - + messagesDiv.scrollTop - + messagesDiv.clientHeight < + 100; + if (isNearBottom) { + messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); + } + } + }; + const fetchMessages = useCallback(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); + } + }, []); + + useEffect(() => { + scrollToBottom(); + }, [messages]); + + useEffect(() => { + fetchMessages(); + const interval = setInterval(fetchMessages, 2000); + return () => clearInterval(interval); + }, [fetchMessages]); + + 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); + } + }; - fetchBackendMessage(); + const addReaction = async (messageId: number, emoji: string) => { + const trimmedUsername = username.trim(); + if (!trimmedUsername) { + alert('Please enter a username first!'); + return; + } + + try { + const response = await fetch(`/api/messages/${messageId}/react`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + emoji, + username: trimmedUsername, + }), + }); + + if (response.ok) { + await fetchMessages(); + } + } catch (err) { + console.error('Error adding reaction:', err); + } + }; + + const fetchPolls = useCallback(async () => { + try { + const response = await fetch('/api/polls'); + if (response.ok) { + const data = await response.json(); + setPolls(data); + } + } catch (err) { + console.error('Error fetching polls:', err); + } }, []); + useEffect(() => { + fetchPolls(); + const interval = setInterval(fetchPolls, 3000); + return () => clearInterval(interval); + }, [fetchPolls]); + + const createPoll = async (e: React.FormEvent) => { + e.preventDefault(); + + const trimmedUsername = username.trim(); + const trimmedQuestion = pollQuestion.trim(); + const trimmedOptions = pollOptions + .map((opt) => opt.trim()) + .filter((opt) => opt.length > 0); + + if (!trimmedUsername) { + alert('Please enter a username first!'); + return; + } + + if (!trimmedQuestion || trimmedOptions.length < 2) { + alert('Please enter a question and at least 2 options!'); + return; + } + + try { + const response = await fetch('/api/polls', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + question: trimmedQuestion, + options: trimmedOptions, + username: trimmedUsername, + }), + }); + + if (response.ok) { + setPollQuestion(''); + setPollOptions(['', '']); + setShowPollForm(false); + await fetchPolls(); + } + } catch (err) { + console.error('Error creating poll:', err); + } + }; + + const vote = async (pollId: number, option: string) => { + const trimmedUsername = username.trim(); + if (!trimmedUsername) { + alert('Please enter a username first!'); + return; + } + + try { + const response = await fetch(`/api/polls/${pollId}/vote`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + option, + username: trimmedUsername, + }), + }); + + if (response.ok) { + await fetchPolls(); + } + } catch (err) { + console.error('Error voting:', err); + } + }; + + const fetchStockData = useCallback(async () => { + try { + const response = await fetch('/api/stock/TSLA'); + if (response.ok) { + const data = await response.json(); + const result = data.chart.result[0]; + const timestamps = result.timestamp; + const prices = result.indicators.quote[0].close; + + // Format dates and filter out null prices + const labels: string[] = []; + const validPrices: number[] = []; + + timestamps.forEach((ts: number, i: number) => { + if (prices[i] !== null) { + const date = new Date(ts * 1000); + labels.push( + date.toLocaleDateString('en-US', { + month: 'short', + day: 'numeric', + }) + ); + validPrices.push(prices[i]); + } + }); + + const currentPrice = validPrices[validPrices.length - 1]; + const previousPrice = validPrices[validPrices.length - 2]; + const change = currentPrice - previousPrice; + const changePercent = (change / previousPrice) * 100; + + setStockData({ + labels, + prices: validPrices, + currentPrice, + change, + changePercent, + }); + } + } catch (err) { + console.error('Error fetching stock data:', err); + } + }, []); + + useEffect(() => { + fetchStockData(); + const interval = setInterval(fetchStockData, 60000); // Update every minute + return () => clearInterval(interval); + }, [fetchStockData]); + return (
- {/* Logo */} -
- - Mentat Logo - -
- - {/* Main content */}
-

Mentat Template JS

+

mentat party 🥳

- {/* Tech stack */} -
+
+
+
+ TSLA +
+
+ ${stockData.currentPrice.toFixed(2)} +
+
+
+
= 0 ? '#10b981' : '#ef4444', + }} + > + {stockData.change >= 0 ? '+' : ''}$ + {stockData.change.toFixed(2)} ( + {stockData.changePercent.toFixed(2)}%) +
+
+ Last 30 days +
+
+
+
+ +
+
+ )} + + {/* Poll creation button */} +
+ 📊 {showPollForm ? 'Cancel Poll' : 'Create Poll'} + - {/* Server message */} -
+ {/* Poll creation form */} + {showPollForm && (
- Message from server: -
-
- {loading ? ( - 'Loading message from server...' - ) : error ? ( - Error: {error} - ) : message ? ( - message - ) : ( - - No message from server - - )} +
+ setPollQuestion(e.target.value)} + style={{ + width: '100%', + padding: '8px', + marginBottom: '10px', + borderRadius: '6px', + border: '1px solid #d1d5db', + fontSize: '14px', + }} + /> + {pollOptions.map((option, index) => ( +
+ { + const newOptions = [...pollOptions]; + newOptions[index] = e.target.value; + setPollOptions(newOptions); + }} + style={{ + flex: 1, + padding: '8px', + borderRadius: '6px', + border: '1px solid #d1d5db', + fontSize: '14px', + }} + /> + {pollOptions.length > 2 && ( + + )} +
+ ))} +
+ + +
+
-
+ )} - {/* Call to action */} + {/* Messages area */}
- Create a new GitHub issue and tag{' '} - ({ + type: 'poll' as const, + data: poll, + timestamp: new Date(poll.createdAt).getTime(), + })), + ...messages.map((msg) => ({ + type: 'message' as const, + data: msg, + timestamp: new Date(msg.timestamp).getTime(), + })), + ] + .sort((a, b) => a.timestamp - b.timestamp) + .map((item) => { + if (item.type === 'poll') { + const poll = item.data; + // Safety checks for incomplete poll data + if (!poll.options || !poll.votes) { + return null; + } + + const totalVotes = Object.values(poll.votes).reduce( + (sum, voters) => sum + voters.length, + 0 + ); + const userVote = Object.entries(poll.votes).find(([, voters]) => + voters.includes(username.trim()) + )?.[0]; + + return ( +
+
+ 📊 {poll.question} +
+
+ by {poll.createdBy} • {totalVotes} vote + {totalVotes !== 1 ? 's' : ''} +
+
+ {poll.options.map((option) => { + const votes = poll.votes[option]?.length || 0; + const percentage = + totalVotes > 0 + ? Math.round((votes / totalVotes) * 100) + : 0; + const isUserVote = userVote === option; + + return ( + + ); + })} +
+
+ ); + } else { + // Message + const msg = item.data; + return ( +
+
+ {msg.username} + {msg.snakeScore > 0 && ( + + 🐍 {msg.snakeScore} + + )} +
+
+ {msg.message} +
+
+ {/* Existing reactions */} + {msg.reactions && + Object.entries(msg.reactions).map(([emoji, users]) => ( + + ))} + {/* Quick reaction buttons */} + {['👍', '❤️', '😂', '🎉', '🔥', '🚀'] + .filter( + (emoji) => + !msg.reactions || + !msg.reactions[emoji] || + msg.reactions[emoji].length === 0 + ) + .map((emoji) => ( + + ))} +
+
+ {new Date(msg.timestamp).toLocaleTimeString()} +
+
+ ); + } + })} +
+
+ + {/* Input form */} +
+ setUsername(e.target.value)} + style={{ + padding: '10px', + borderRadius: '6px', + border: '1px solid #d1d5db', + fontSize: '14px', + width: '150px', + }} + /> + setMessageText(e.target.value)} style={{ - backgroundColor: '#f8fafc', - padding: '2px 6px', - borderRadius: '4px', - fontSize: '13px', - color: '#1f2937', + flex: 1, + padding: '10px', + borderRadius: '6px', + border: '1px solid #d1d5db', + fontSize: '14px', + }} + /> +
+ Send + +
); diff --git a/client/src/Chat.tsx b/client/src/Chat.tsx new file mode 100644 index 0000000..75f34de --- /dev/null +++ b/client/src/Chat.tsx @@ -0,0 +1,277 @@ +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 { + // 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 */} +
+
+