# Project export: Pawn Up

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: Cal Hacks 11.0
- Tagline: Pawn Up: Your Personal AI Chess Coach—Analyze, Train, and Master Chess on Your Own Time.
- Devpost: https://devpost.com/software/gambit-y0j7mu
- GitHub: https://github.com/satwikug25/calhacks
- Result: winner (Chroma: Build your project with Chroma in an AI application)
- Team: 3 GitHub contributor(s) — Sachuman (18 commits), Soham Daga (13 commits), Sai Shiva Satwik Mallajosyula (13 commits)

## Devpost submission (written by the team)

### Overview

About the Project We are a bunch of amateur players who love playing chess, but over time we noticed that our improvement has become stagnant. Like many college students, we neither have the time nor the financial means to invest in professional coaching to take our game to the next level. This frustration sparked the idea behind Pawn Up—a project built to help players like us break through the plateau and improve their chess skills in their own time, without expensive coaches or overwhelming resources. What Inspired Us As passionate chess players, we struggled with finding affordable and effective ways to improve. Chess can be an expensive hobby if you want to seek professional help or guidance. The available tools often lacked the depth we needed or came with hefty price tags. We wanted something that would provide personalized feedback, targeted training, and insights that anyone could access—regardless of their financial situation. How We Built It We started by integrating Lichess authentication to fetch a user's game history, allowing them to directly analyze their own performance. With Groq and Llama3.1, we leveraged AI to categorize mistakes, generate feedback, and suggest relevant puzzles to help users train and improve. We also levergae ChromaDB for vector search features and Gemini pro and Gemini embedding Our project features four key components: Analyze: Fetches the user's last 10 games, provides analysis on each move, and visualizes a heatmap showing the performance of legal moves for each piece. Users can also interact with the game for deeper analysis. Train: Using AI, the system analyzes the user's past games and suggests categorized puzzles that target areas of improvement. Search: We created a vector database storing thousands of grandmaster games. Users can search for specific games and replay them with detailed analysis, just like with their own games. Upload: Users can upload their own chess games and perform the same analyses and training as with the Search feature. What We Learned Throughout the development of Pawn Up, we gained a deeper understanding of AI-powered analysis and how to work with complex game datasets. We learned how to integrate chess engines, handle large amounts of data, and create user-friendly interfaces. Additionally, we explored how LLMs (large language models) can provide meaningful feedback and how vector databases can be used to store and retrieve massive datasets efficiently. Challenges We Faced One of the main challenges we encountered was making the AI feedback meaningful for players across various skill levels. It was crucial that the system didn’t just provide generic advice but rather tailored suggestions that were both practical and actionable. Handling large amounts of chess data efficiently, without compromising on speed and usability, also posed a challenge. Building the vector database to store and search through grandmaster games was a particularly challenging but rewarding experience. Despite these hurdles, we’re proud of what we’ve built. Pawn Up is the solution we wish we had when we first started hitting that plateau in our chess journeys, and we hope it can help others as well.

## README (from the GitHub repository)

# This project was made for CalHacks 2024

## Overview
 It is a chess AI tutor that evaluates your lichess games and helps you improve through customized puzzles, using AI to evaluate and understand every move possible.


## Installation Guide

1. Clone the repository
2. Install the dependencies (npm install, pip install)
3. Run the frontend (npm run dev)
4. Run the backend (flask --app app run)
5. Run search backend ( npm start )
6. Start the Chroma DB with chroma run --path ./getting-started

   Read the docs for more clarification ( https://docs.trychroma.com/getting-started ) 


## APIs KEYS 
- Google Gemini API
- RAPID CHESS API
- Llama API
- Groq API

## How it works
- Get customized puzzles based on your lichess games
- Evaluate your games and give you personalized feedback with AI
- Upload your games to the database and view them in the website or search for popular games with semantic search
* Example: "Carlsen, Nakamura, Queens openings"


## Results
![alt text](https://github.com/satwikug25/CalHacks/blob/main/frontend/public/images/arch.png?raw=true)
![alt text](https://github.com/satwikug25/CalHacks/blob/main/frontend/public/images/img1.png?raw=true)
![alt text](https://github.com/satwikug25/CalHacks/blob/main/frontend/public/images/img2.png?raw=true)
![alt text](https://github.com/satwikug25/CalHacks/blob/main/frontend/public/images/img3.png?raw=true)
![alt text](https://github.com/satwikug25/CalHacks/blob/main/frontend/public/images/img4.png?raw=true) 
![alt text](https://github.com/satwikug25/CalHacks/blob/main/frontend/public/images/img5.png?raw=true)
![alt text](https://github.com/satwikug25/CalHacks/blob/main/frontend/public/images/img6.png?raw=true)
![alt text](https://github.com/satwikug25/CalHacks/blob/main/frontend/public/images/img7.png?raw=true)







## Detected evidence (automated analysis)

Indexed codebase: 25 recognized source files, 80 KB.
- CSS (language) — detected in the code
- Express (technology) — detected in the code
- Google Gemini (technology) — detected in the code
- HTML (language) — detected in the code
- JavaScript (language) — detected in the code
- Python (language) — detected in the code
- React (technology) — detected in the code
- Tailwind CSS (technology) — detected in the code
- Docker (technology) — claimed on Devpost, not found in the code
- Flask (technology) — claimed on Devpost, not found in the code
- Node.js (technology) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (37 of 37)

```
.DS_Store
backend/.gitignore
backend/app.py
backend/LICENSE
backend/methods.py
backend/Pipfile
backend/Pipfile.lock
backend/README.md
backend/vercel.json
frontend/.gitignore
frontend/eslint.config.js
frontend/index.html
frontend/package.json
frontend/postcss.config.js
frontend/README.md
frontend/src/App.css
frontend/src/App.jsx
frontend/src/app/Analyze.jsx
frontend/src/app/Choose.jsx
frontend/src/app/components/ChessGame.jsx
frontend/src/app/components/ChessGameChat.jsx
frontend/src/app/Login.jsx
frontend/src/app/Navbar.jsx
frontend/src/app/Search.jsx
frontend/src/app/Train.jsx
frontend/src/app/UploadSearch.jsx
frontend/src/index.css
frontend/src/main.jsx
frontend/tailwind.config.js
frontend/vite.config.js
readme.md
search-engine-nodejs-server/.gitignore
search-engine-nodejs-server/chroma.log
search-engine-nodejs-server/docker-compose.yml
search-engine-nodejs-server/getting-started/chroma.sqlite3
search-engine-nodejs-server/package.json
search-engine-nodejs-server/server.js
```

### Dependencies

- frontend/package.json: @bity/oauth2-auth-code-pkce@^2.13.0, @chakra-ui/react@^2.10.3, @emotion/react@^11.13.3, @emotion/styled@^11.13.0, @eslint/js@^9.11.1, @types/react@^18.3.10, @types/react-dom@^18.3.0, @vitejs/plugin-react@^4.3.2, autoprefixer@^10.4.20, axios@^1.7.7, chess.js@^1.0.0-beta.8, eslint@^9.11.1, eslint-plugin-react@^7.37.0, eslint-plugin-react-hooks@^5.1.0-rc.0, eslint-plugin-react-refresh@^0.4.12, framer-motion@^11.11.9, globals@^15.9.0, postcss@^8.4.47, react@^18.3.1, react-chessboard@^4.7.2, react-dom@^18.3.1, react-icons@^5.3.0, react-router-dom@^6.27.0, shadcn-ui@^0.9.2, tailwindcss@^3.4.14, vite@^5.4.8
- search-engine-nodejs-server/package.json: @google/generative-ai@^0.1.3, @singlestore/client@^0.0.41, axios@^1.7.7, body-parser@^1.20.3, chromadb@^1.9.2, chromadb-default-embed@^2.13.2, cors@^2.8.5, dotenv@^16.4.5, express@^4.21.1, llama-node@^0.1.6, mysql2@^3.11.3, nodemon@^3.1.7, punycode@^2.3.1

### Recent commits (newest first)

- Update readme.md
- readme
- readme
- done
- all UI done
- SVG
- Made changes
- puzzling
- Merge branch 'main' of github.com:satwikug25/CalHacks
- puzzling
- Changed UI
- train done
- merged
- Added animations
- changes
- Made UI changes
- Made UI changes
- Merge branch 'main' of https://github.com/satwikug25/CalHacks
- Fixed winner UI
- Final

## Key source files (fetched from GitHub, selected and truncated for size)

### search-engine-nodejs-server/docker-compose.yml

```yaml
version: "3.9"

services:
  chroma:
    image: chromadb/chroma:latest
    ports:
      - "8000:8000"
    volumes:
      - ./chroma_data:/chromadb/data
```

### search-engine-nodejs-server/package.json

```
{
  "type": "module",
  "scripts": {
    "start": "nodemon server.js"
  },
  "dependencies": {
    "@google/generative-ai": "^0.1.3",
    "@singlestore/client": "^0.0.41",
    "axios": "^1.7.7",
    "body-parser": "^1.20.3",
    "chromadb": "^1.9.2",
    "chromadb-default-embed": "^2.13.2",
    "cors": "^2.8.5",
    "dotenv": "^16.4.5",
    "express": "^4.21.1",
    "llama-node": "^0.1.6",
    "mysql2": "^3.11.3",
    "nodemon": "^3.1.7",
    "punycode": "^2.3.1"
  }
}

```

### frontend/package.json

```
{
  "name": "frontend",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "lint": "eslint .",
    "preview": "vite preview"
  },
  "dependencies": {
    "@bity/oauth2-auth-code-pkce": "^2.13.0",
    "@chakra-ui/react": "^2.10.3",
    "@emotion/react": "^11.13.3",
    "@emotion/styled": "^11.13.0",
    "axios": "^1.7.7",
    "chess.js": "^1.0.0-beta.8",
    "framer-motion": "^11.11.9",
    "react": "^18.3.1",
    "react-chessboard": "^4.7.2",
    "react-dom": "^18.3.1",
    "react-icons": "^5.3.0",
    "react-router-dom": "^6.27.0",
    "shadcn-ui": "^0.9.2"
  },
  "devDependencies": {
    "@eslint/js": "^9.11.1",
    "@types/react": "^18.3.10",
    "@types/react-dom": "^18.3.0",
    "@vitejs/plugin-react": "^4.3.2",
    "autoprefixer": "^10.4.20",
    "eslint": "^9.11.1",
    "eslint-plugin-react": "^7.37.0",
    "eslint-plugin-react-hooks": "^5.1.0-rc.0",
    "eslint-plugin-react-refresh": "^0.4.12",
    "globals": "^15.9.0",
    "postcss": "^8.4.47",
    "tailwindcss": "^3.4.14",
    "vite": "^5.4.8"
  }
}

```

### search-engine-nodejs-server/server.js

```javascript
import express from 'express';
import cors from 'cors';
import bodyParser from 'body-parser';
import { ChromaClient, OpenAIEmbeddingFunction } from 'chromadb';
import { GoogleGenerativeAI } from '@google/generative-ai';
import dotenv from 'dotenv';

dotenv.config();

const app = express();
const port = process.env.PORT || 8081;

app.use(cors());
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));

const chroma = new ChromaClient();
const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY);

let collection;
(async () => {
  collection = await chroma.getOrCreateCollection({
    name: "chess_games",
    embeddingFunction: new OpenAIEmbeddingFunction({ openai_api_key: process.env.GEMINI_API_KEY })
  });
})();

async function embedText(text) {
  const model = genAI.getGenerativeModel({ model: "text-embedding-004" });
  const result = await model.embedContent(text);
  if (Array.isArray(result.embedding)) {
    return result.embedding;
  } else if (typeof result.embedding === 'object' && result.embedding.values) {
    return result.embedding.values;
  } else {
    throw new Error('Unexpected embedding format');
  }
}

async function generateDescription(game) {
  const model = genAI.getGenerativeModel({ model: "gemini-1.5-flash" });
  const prompt = `Analyze this chess game and provide a description in the following format:
  "Opening is [opening name], endgame is [endgame description], players are [white player] and [black player], Elo is [white Elo] and [black Elo]."
  Game details:
  ${JSON.stringify(game, null, 2)}`;

  const result = await model.generateContent(prompt);
  console.log(result.response.text());
  return result.response.text();
}

app.post('/upload', async (req, res) => {
  try {
    const game = req.body;
    const description = await generateDescription(game);
    const fullGame = { ...game, description };
    console.log(description);
    const embedding = await embedText(JSON.stringify(fullGame));

    await collection.add({
      ids: [game.id || Date.now().toString()],
      embeddings: [embedding],
      metadatas: [fullGame],
      documents: [JSON.stringify(fullGame)]
    });

    res.json({ message: 'Game uploaded successfully', description });
  } catch (error) {
    console.error('Error uploading game:', error);
    res.status(500).json({ error: 'An error occurred while uploading the game' });
  }
});

app.post('/search', async (req, res) => {
  try {
    const { query } = req.body;
    const queryEmbedding = await embedText(query);

    // Extract nResults from the query
    const nResultsMatch = query.match(/\b(\d+)\s+results?\b/i);
    const nResults = nResultsMatch ? parseInt(nResultsMatch[1]) : 9;

    const results = await collection.query({
      queryEmbeddings: [queryEmbedding],
      nResults: nResults
    });

    const formattedResults = results.documents[0].map(game => {
      game = JSON.parse(game);
      console.log(game.moves);
      return ({
        white: game.white,
        black: game.black,
        whiteElo: game.whiteElo,
        blackElo: game.blackElo,
        result: game.result,
        moves: game.moves || '',
        description: game.description
      });
    });

    res.json(formattedResults);
  } catch (error) {
    console.error('Error searching games:', error);
    res.status(500).json({ error: 'An error occurred while searching games' });
  }
});

app.listen(port, () => {
  console.log(`Server running on port ${port}`);
});

```

### backend/app.py

```python
from flask import Flask, jsonify, redirect, request, session, url_for
import requests
import os
import hashlib
import base64
import secrets
from methods import getGames
from groq import Groq
import json
from flask_cors import CORS
from dotenv import load_dotenv
import urllib.parse
load_dotenv()

app = Flask(__name__)
app.secret_key = os.urandom(24)
CORS(app, resources={r"/*": {"origins": "http://localhost:5173"}})  
client_id = 'chessicle.com'
port = 5000

username = ""

groq_client = Groq(api_key=os.environ["GROQ_API_KEY"])

# Function to base64url encode
def base64url_encode(data):
    return base64.urlsafe_b64encode(data).rstrip(b'=').decode('utf-8')

# Create the code verifier
def create_verifier():
    return base64url_encode(secrets.token_bytes(32))

# Create the code challenge from verifier
def create_challenge(verifier):
    digest = hashlib.sha256(verifier.encode('utf-8')).digest()
    return base64url_encode(digest)

@app.route('/login')
def login():
    verifier = create_verifier()
    challenge = create_challenge(verifier)

    session['code_verifier'] = verifier

    params = {
        'response_type': 'code',
        'client_id': client_id,
        'redirect_uri': url_for('callback', _external=True),
        'scope': 'preference:read',
        'code_challenge_method': 'S256',
        'code_challenge': challenge
    }
    auth_url = 'https://lichess.org/oauth?' + '&'.join([f'{key}={value}' for key, value in params.items()])
    return redirect(auth_url)

@app.route('/callback')
def callback():
    code = request.args.get('code')
    verifier = session.get('code_verifier')
    redirect_uri = url_for('callback', _external=True)

    # Exchange authorization code for access token
    token_url = 'https://lichess.org/api/token'
    data = {
        'grant_type': 'authorization_code',
        'client_id': client_id,
        'code': code,
        'code_verifier': verifier,
        'redirect_uri': redirect_uri,
    }
    response = requests.post(token_url, json=data, headers={'Content-Type': 'application/json'})
    token_response = response.json()

    if 'access_token' not in token_response:
        return 'Failed to get access token'

    access_token = token_response['access_token']

    # Use the access token to get the user's Lichess account information
    user_info_url = 'https://lichess.org/api/account'
    headers = {'Authorization': f'Bearer {access_token}'}
    user_info_response = requests.get(user_info_url, headers=headers)
    user_info = user_info_response.json()
    

    # Redirect back to the React frontend with the username
    return redirect(f'http://localhost:5173/choose?username={user_info["username"]}')

@app.get('/analyze_and_get_puzzles')
def analyze_and_get_puzzles():
    username = request.args.get('username')
    if not username:
        return jsonify({"error": "Username is required"}), 400

    try:
        # First, get the analysis from the /train endpoint
        games = getGames(username)
        feedback = get_llama_feedback(games,username)
        
        # Extract recommended themes from the analysis
        recommended_themes = json.loads(feedback).get('recommended_themes', [])
        
        # Use these themes to fetch puzzles
        puzzles = get_puzzles(recommended_themes)
        
        print({
            "analysis": feedback,
            "puzzles": puzzles
        })
        return jsonify({
            "analysis": feedback,
            "puzzles": puzzles
        })
    except Exception as e:
        print(f"Error in analyze_and_get_puzzles: {str(e)}")
        return jsonify({"error": "An error occurred while processing your request"}), 500


# @app.get('/train')
# def train():
#     username = request.args.get('username')
#     if not username:
#         return jsonify({"error": "User not logged in"}), 401

#     games = getGames(username)
#     feedback = get_llama_feedback(games,username)
#     return jsonify(feedback)

groq_api_key = os.environ.get('GROQ_API_KEY')
if not groq_api_key:
    raise ValueError("GROQ_API_KEY is not set in environment variables")

groq_client = Groq(api_key=groq_api_key)

    # Use a model that exists and you have access to
model = "llama-3.1-70b-versatile"

@app.post('/ask_question')
def ask_question():
    data = request.json
    question = data.get('question')
    pgn = data.get('pgn')
    currentMove = data.get('currentMove')

    prompt = f'''You will be given the following details about a chess game: 
                \nQuestion: {question}

                If the question is not related to chess, no matter the below details, respond with "I'm sorry, I can only provide feedback on chess games."
                The user might also ask general questions about moves that are not in the PGN but hypothetical, in which case you should answer them based on the PGN and current board position.

                \nPGN: {pgn}
                \nCurrent Move: {currentMove}

                Limit your answers to 50 words and be brutal.'''
    
    try:
        response = groq_client.chat.completions.create(
            model=model,
            messages=[
                {
                    "role": "system",
                    "content": "You are an expert chess analyst providing personalized feedback to players."
                },
                {
                    "role": "user",
                    "content": prompt
                }
            ],
            temperature=0.7,
            max_tokens=1000,
            top_p=1,
            stream=False,
            stop=None
        )
        return response.choices[0].message.content
    except Exception as e:
        print(f"Error in get_llama_feedback: {str(e)}")
        return f"An error occurred while generating feedback: {str(e)}"

@app.post('/get_evaluation')
def get_evaluation():
    data = request.json
    fen = data.get('fen')
    square = data.get('square')
    moves = data.get('moves')
    
    prompt = f'''You are given the FEN of a chess ga
[truncated — 6635 more characters]
```

### frontend/src/main.jsx

```javascript
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import App from './App.jsx'
import './index.css'


createRoot(document.getElementById('root')).render(
  <StrictMode>
      <App />
  </StrictMode>,
)

```

### frontend/src/App.jsx

```javascript
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import { useState } from 'react';
import './App.css';
import Login from "./app/Login";
import Choose from "./app/Choose";
import Train from "./app/Train";
import UploadSearch from "./app/UploadSearch";
import Search from "./app/Search";

import Analyze from "./app/Analyze";


function App() {
  const [openedChessGameChat, setOpenedChessGameChat] = useState(null);
  return (
    <Router>
      <Routes>
        <Route path="/" element={<Login />} />
        <Route path="/choose" element={<Choose />} />
        <Route path="/train" element={<Train />} />
        <Route path="/upload" element={<UploadSearch />} />
        <Route path="/search" element={<Search openedChessGameChat={openedChessGameChat} setOpenedChessGameChat={setOpenedChessGameChat} />} />
        <Route path="/analyze" element={<Analyze openedChessGameChat={openedChessGameChat} setOpenedChessGameChat={setOpenedChessGameChat} />} />

      </Routes>
    </Router>
  );
}

export default App;
```

### frontend/postcss.config.js

```javascript
export default {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  },
}

```

### frontend/vite.config.js

```javascript
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react()],
})

```

### frontend/index.html

```html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>PawnUp</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.jsx"></script>
  </body>
</html>

```

[13 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]