Add a Moves Database and AI Chatbot

Fetch live move data from PokeAPI and add a context-aware Gemini chatbot.

Introduction

30 Second Summary

Every Pokemon trainer knows that picking the right moves is what separates a casual team from a competitive one. But scrolling through wiki pages to compare 40+ moves per Pokemon is tedious when you just want to find the best options fast.

In this project, you will add a live moves database and an AI-powered chatbot to your existing Pokedex app. Your Pokedex will fetch real move data from PokeAPI, display it in organized tabs, and let you ask a Google Gemini chatbot strategy questions about the Pokemon you are viewing.

What You'll Build

Click any Pokemon card and watch it expand to reveal a full moves database pulled live from PokeAPI. Then open the floating chat panel, ask "What's a good competitive moveset?" and get specific advice that references the actual moves this Pokemon can learn.

By the end of this project, you'll have:

  • A tabbed moves table where you click any Pokemon, see its full moveset load in real-time from PokeAPI, and switch between Level Up, TM/HM, Egg, and Tutor tabs to browse moves with type-colored badges, damage class labels, and complete battle stats.
  • A floating AI chat panel where you type a question like "What's a good competitive moveset?" and the chatbot responds with specific move recommendations pulled from the Pokemon's actual learnable moves.
  • A working example of LLM context injection where you experience the chatbot failing without context, then fix it by passing the Pokemon's data into the prompt so the AI gives grounded, specific answers instead of generic ones.
  • Secret Mission: Add type filtering and column sorting to the moves table so you can quickly find the strongest Fire-type physical moves.

Are there any prerequisites?

This project builds directly on the "Build a Futuristic Pokedex Web UI" project. You need the pokedex-search folder from that project with server.py, engine.py, fetcher.py, pokedex.json, and the static/ folder containing index.html, style.css, and app.js.

You also need a Google account to get a free Gemini API key.

Before We Start

Before We Start...

Before we dive in, let's lock in what you're about to build and why it matters for your existing Pokedex.

Install Packages and Get Your API Key

Your existing Pokedex shows stats, types, and abilities for each Pokemon. But it has no move data and no way to ask AI-powered questions about strategy.

To unlock both features, you need two new Python packages and a free API key from Google. In this step, you will install everything and confirm it works.

In this step, get ready to:
  • Install the httpx and google-genai packages using uv.
  • Get a free Gemini API key from Google AI Studio.
  • Verify the packages load correctly.
Install httpx and google-genai

You need two packages that do not come with your existing project. httpx enables async HTTP requests, which you will use to fetch move data from PokeAPI in parallel. The google-genai package is the official Google Generative AI SDK for Python, which powers the chatbot.

  • Open a terminal in VS Code by pressing Ctrl+` (macOS and Windows).
  • Install both packages by running this command:
uv add httpx google-genai

You should see output showing both packages being downloaded and installed, ending with a success message.

Seeing a 'command not found' error?

Make sure your terminal is open inside the pokedex-search folder. The uv tool must be available on your PATH from the previous project setup.

If your terminal opened in a different folder, run cd ~/Desktop/pokedex-search first.

Help me fix my uv installation.

Get a free Gemini API key

The chatbot you will build uses Google Gemini to generate responses. You need a free API key to authenticate your requests.

  • Open your browser and visit:
  • https://aistudio.google.com/apikey
  • Sign in with your Google account.
  • Click Create API key.
  • Select a project when prompted (the default project is fine).
  • Copy the generated key. You will only see it once.

Is this free?

Yes. The Gemini API free tier has no charge for input tokens, output tokens, or context caching. Your personal Pokemon project will stay well within these limits.

Now store the key in a file your server can read. Your project needs a .env file so the key stays out of your code.

  • In VS Code, click File in the top menu bar.
  • Click New File.
  • Name the file .env and save it in your pokedex-search folder.
  • Paste the following into the file, replacing your-api-key-here with the key you just copied:
GEMINI_API_KEY=your-api-key-here

Why use a .env file?

A .env file keeps secrets separate from your code. Your server will load this value at startup so the API key never appears in server.py or gets accidentally committed to version control.

  • Save the .env file.

Having trouble creating the API key?

If you see a permission error, try opening the page in an incognito window or a different browser. Some browser extensions interfere with Google AI Studio.

The Gemini API free tier is not available in all countries. If you see a region-related error, check the Gemini API troubleshooting docs for supported regions.

Help me troubleshoot my Gemini API key setup.

Verify the setup

Both packages are installed and your key is stored. Before you move on, confirm that Python can import the Gemini SDK without errors.

What do you expect to see when you try to import the google-genai package? Will it succeed, or might something go wrong?

  • Run this verification command in your terminal:
uv run python -c "from google import genai; print('Gemini SDK ready')"

You should see Gemini SDK ready printed with no errors.

Seeing an ImportError?

If you see ModuleNotFoundError: No module named 'google', the uv add command may not have completed successfully. Run uv add google-genai again and check for error messages in the output.

Make sure you are running the command from inside your pokedex-search folder where your pyproject.toml lives.

Help me fix my google-genai import error.

Your environment is ready with both packages installed and your API key stored. Next up, you will build a new endpoint that fetches live move data from PokeAPI.

Build the Moves API Endpoint

Your environment is ready with both packages installed and your Gemini API key stored safely. Now it's time to build the core feature: a live moves database that fetches data directly from PokeAPI.

Your current Pokedex only shows data stored in pokedex.json (stats, types, abilities). Move data is far too large to store locally. Charizard alone has 80+ moves across all game versions. You need to fetch moves live from PokeAPI and filter them to the latest game version.

In this step, get ready to:
  • Add new imports and setup code to server.py for async fetching and AI integration.
  • Build a /api/pokemon/{name}/moves endpoint that fetches live move data from PokeAPI.
  • Add parallel fetching and caching so moves load fast on repeated views.
Update server.py imports and add setup code

Before you can fetch moves or chat with AI, server.py needs new imports and configuration. You will add httpx for async HTTP requests, asyncio for parallel execution, and the google-genai SDK for the chatbot you will build in a later step.

  • In VS Code, open server.py from the file explorer sidebar.
  • Select everything from the first line down to (but not including) the @app.get("/api/search") decorator, and replace it with this code:
import os
import asyncio
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse
from pydantic import BaseModel
import httpx
from google import genai

from engine import PokedexSearchEngine
from fetcher import load_pokedex

app = FastAPI()

# Load the Pokedex data and build the search index once at startup
pokedex = load_pokedex()
engine = PokedexSearchEngine(pokedex)

What changed in the imports?

  • os reads environment variables (your Gemini API key).
  • asyncio enables parallel HTTP requests with gather.
  • httpx is an async-capable HTTP client that works with FastAPI's async model.
  • from google import genai loads the Gemini AI SDK (used in a later step).
  • BaseModel from Pydantic defines typed request schemas.
  • Directly below the engine = PokedexSearchEngine(pokedex) line, add this setup code:
# Cache for move data to avoid repeated PokeAPI fetches
move_cache = {}
pokemon_moves_cache = {}

# Load Gemini API key from .env file
from dotenv import load_dotenv
load_dotenv()
GEMINI_API_KEY = os.getenv("GEMINI_API_KEY", "")

# Initialize Gemini client
gemini_client = None
if GEMINI_API_KEY:
    gemini_client = genai.Client(api_key=GEMINI_API_KEY)


class ChatRequest(BaseModel):
    message: str
    context: str = ""

What does this setup code do?

  • move_cache stores individual move details so you don't re-fetch the same move from PokeAPI twice.
  • pokemon_moves_cache stores the fully-assembled moves response for each Pokemon.
  • load_dotenv() reads your .env file so the Gemini API key is available.
  • ChatRequest is a Pydantic model you will use in the chatbot step later.
  • Save server.py.
  • Check your terminal where the dev server is running. It should auto-reload without errors.

Seeing an ImportError?

  • If you see an error mentioning httpx or genai, confirm you ran uv add httpx google-genai in the previous step.
  • If you see an error about dotenv, run uv add python-dotenv to install it.
  • help me debug an ImportError in my server.py
Create the moves endpoint and fetch from PokeAPI

Now you will create the endpoint that fetches a Pokemon's move data live from PokeAPI and filters it to only the scarlet-violet version group. This gives you a clean, current moveset instead of 80+ entries spanning every game version.

  • In server.py, scroll down past the existing get_pokemon endpoint. Add this new endpoint directly below it (above the app.mount line):
@app.get("/api/pokemon/{name}/moves")
async def get_pokemon_moves(name: str):
    if name.lower() in pokemon_moves_cache:
        return pokemon_moves_cache[name.lower()]

    async with httpx.AsyncClient(timeout=30.0) as client:
        resp = await client.get(f"https://pokeapi.co/api/v2/pokemon/{name.lower()}")
        if resp.status_code != 200:
            return {"error": "Pokemon not found on PokeAPI"}
        data = resp.json()

    grouped_moves = {}
    move_names_to_fetch = set()
    move_learn_info = {}

    for move_entry in data["moves"]:
        move_name = move_entry["move"]["name"]
        for detail in move_entry["version_group_details"]:
            if detail["version_group"]["name"] == "scarlet-violet":
                method = detail["move_learn_method"]["name"]
                level = detail["level_learned_at"]
                if method not in grouped_moves:
                    grouped_moves[method] = []
                move_names_to_fetch.add(move_name)
                if move_name not in move_learn_info:
                    move_learn_info[move_name] = []
                move_learn_info[move_name].append({"method": method, "level": level})

What does this code do?

  • The cache check at the top returns instantly if this Pokemon's moves have been fetched before.
  • httpx.AsyncClient fetches the Pokemon's full data from https://pokeapi.co/api/v2/pokemon/{name}.
  • The loop filters the moves array to only entries where the version_group is scarlet-violet (the latest game).
  • move_learn_info tracks which method (level-up, machine, egg, tutor) each move is learned by, along with the level.
  • Save server.py.
  • Open your browser and visit http://127.0.0.1:8000/api/pokemon/charizard/moves.

You should see null in the browser. That is expected. The endpoint successfully fetches from PokeAPI and filters the moves, but it does not build a return value yet.

Seeing a 500 error instead of null?

  • Check your indentation. Every line inside get_pokemon_moves must be indented with 4 spaces.
  • Make sure the endpoint is placed above the app.mount("/static", ...) line. FastAPI matches routes in order, and the static mount would catch /api/ paths if it comes first.
  • help me debug a 500 error on my /api/pokemon/{name}/moves endpoint
Fetch individual move details in parallel

Each move in the filtered list is just a name. To get its type, power, accuracy, PP, and damage class, you need a separate API call to PokeAPI's move endpoint. Fetching 30+ moves one at a time would take 10-15 seconds. Using asyncio.gather sends all requests simultaneously, reducing the wait to about 1-2 seconds.

  • In server.py, add this code directly below the filtering loop you just wrote (still inside the get_pokemon_moves function):
    uncached_moves = [m for m in move_names_to_fetch if m not in move_cache]

    if uncached_moves:
        async with httpx.AsyncClient(timeout=30.0) as client:
            tasks = [
                client.get(f"https://pokeapi.co/api/v2/move/{move_name}/")
                for move_name in uncached_moves
            ]
            responses = await asyncio.gather(*tasks, return_exceptions=True)
            for move_name, resp in zip(uncached_moves, responses):
                if isinstance(resp, Exception) or resp.status_code != 200:
                    continue
                move_data = resp.json()
                move_cache[move_name] = {
                    "name": move_data["name"],
                    "type": move_data["type"]["name"],
                    "damage_class": move_data["damage_class"]["name"],
                    "power": move_data["power"],
                    "accuracy": move_data["accuracy"],
                    "pp": move_data["pp"],
                }

What does this code do?

  • uncached_moves filters out moves already in the cache, so you never fetch the same move twice.
  • asyncio.gather fires all HTTP requests at once and waits for all of them to complete. This is dramatically faster than sequential fetching.
  • return_exceptions=True ensures one failed request does not crash the entire batch.
  • Each successful response is stored in move_cache with its type, damage class (physical/special/status), power, accuracy, and PP.
  • Still inside the get_pokemon_moves function, add this final section below the parallel fetching code to build the grouped result and return it:
    result = {}
    for move_name, learn_entries in move_learn_info.items():
        if move_name not in move_cache:
            continue
        move_details = move_cache[move_name]
        for entry in learn_entries:
            method = entry["method"]
            if method not in result:
                result[method] = []
            result[method].append({
                **move_details,
                "level": entry["level"],
            })

    if "level-up" in result:
        result["level-up"].sort(key=lambda m: m["level"])

    pokemon_moves_cache[name.lower()] = result
    return result

What does this final section do?

  • It assembles the result dictionary by grouping each move under its learn method (level-up, machine, egg, tutor).
  • Level-up moves are sorted by level so they display in a natural order.
  • The final result is cached in pokemon_moves_cache so the next request for the same Pokemon returns instantly.

Before you test, what do you think the response will look like? Consider: the endpoint groups moves by learn method, and each move has 6 fields.

  • Save server.py.
  • Visit http://127.0.0.1:8000/api/pokemon/charizard/moves in your browser.

You should see a JSON response with keys like level-up, machine, egg, and tutor. Each key contains an array of moves with name, type, damage_class, power, accuracy, and pp fields. The first request may take 1-2 seconds as it fetches from PokeAPI. Refresh the page and it returns instantly from cache.

Getting an empty response or error?

  • If you see null, make sure you added the result-building code (the last block) below the parallel fetching code. The function needs an explicit return result statement.
  • If you see {"error": "Pokemon not found on PokeAPI"}, check that the URL uses a lowercase Pokemon name (charizard not Charizard).
  • If you see a timeout error, your network may be blocking PokeAPI. Try again or check your internet connection.
  • help me debug my moves endpoint returning empty or erroring

✔️ Awesome, I've got everything!

Great. Double check you have saved server.py and the endpoint returns grouped moves with all 6 fields per move.

ⓧ I'd like to double check the full code

Here is the complete server.py as it should look at the end of this step. Scroll through and confirm each section is present in order.

Your file has these sections from top to bottom:

  • Imports (os, asyncio, FastAPI, StaticFiles, FileResponse, BaseModel, httpx, genai, engine, fetcher).
  • App creation, pokedex loading, and engine initialization.
  • Cache dictionaries (move_cache and pokemon_moves_cache).
  • Dotenv loading and Gemini client initialization.
  • ChatRequest Pydantic model.
  • The /api/search endpoint (unchanged from the previous project).
  • The /api/pokemon/{name} endpoint (unchanged from the previous project).
  • The new /api/pokemon/{name}/moves endpoint with cache check, PokeAPI fetch, filtering, parallel move detail fetching, result building, sorting, and cache write.
  • Static file mount and root route (unchanged from the previous project).

Your moves endpoint is live. Next up, you will build the tabbed UI that displays these moves in the browser when you click a Pokemon card.

Display the Moves Table with Tabs

Your moves endpoint returns raw JSON, but nobody wants to read nested objects in a browser tab. The real payoff is seeing moves organized in the browser, right below the stat bars you already built.

A tabbed interface lets you switch between Level Up, TM/HM, Egg, and Tutor moves without scrolling through 40+ rows at once. In this step you will wire the frontend to your new endpoint and build the UI that displays moves in an interactive table.

In this step, get ready to:
  • Add JavaScript functions that fetch move data and display a loading spinner while waiting.
  • Build a tabbed table renderer that groups moves by learn method and shows type-colored badges.
  • Style the moves section to match the existing sci-fi theme with glowing tabs and hover effects.
Add the moves section HTML and fetch logic

When a card expands, it needs to show a loading placeholder for moves, then fetch the data from your new endpoint. You will add two new global variables to track the currently expanded Pokemon, then two functions that handle the loading UI and the fetch request.

  • In VS Code, open static/app.js.
  • Find the line let debounceTimer = null; near the top of the file.
  • Add the following two variable declarations and two new functions directly below it (before the typeWriter function):
let currentPokemon = null;
let currentMoves = null;
  • Now add these two functions after the existing animateStatBars function:
function buildMovesSection(pokemonName) {
    return `<div class="moves-section" id="moves-${pokemonName}"><div class="moves-loading">Loading moves<span class="loading-dots"></span></div></div>`;
}

async function loadMoves(pokemonName, container) {
    try {
        const resp = await fetch(`/api/pokemon/${pokemonName}/moves`);
        const data = await resp.json();
        if (data.error) {
            container.innerHTML = '<div class="moves-error">Could not load moves.</div>';
            return;
        }
        currentMoves = data;
        renderMoveTabs(data, container);
    } catch (e) {
        container.innerHTML = '<div class="moves-error">Could not load moves.</div>';
    }
}

What does this code do?

  • currentPokemon and currentMoves track which Pokemon is currently expanded and its fetched move data.
  • buildMovesSection returns an HTML string with a pulsing "Loading moves..." message that appears inside the card while the API call is in progress.
  • loadMoves fetches the /api/pokemon/{name}/moves endpoint you built in Step 2, stores the result in currentMoves, and hands the data off to renderMoveTabs (which you will write next).

Next, you need to update the createCard function so that the card HTML includes the moves placeholder and the click handler triggers the fetch.

  • Inside createCard, find this line in the card's HTML template:
${buildStatBars(pokemon.stats)}
  • Add this line directly below it, so the moves section appears after the stat bars:
${buildMovesSection(pokemon.name)}
  • Now find the existing click handler at the bottom of createCard. Replace the entire card.addEventListener('click', ...) block with this updated version:
card.addEventListener('click', () => {
    const details = card.querySelector('.card-details');
    const isExpanding = !details.classList.contains('expanded');
    details.classList.toggle('expanded');
    if (isExpanding) {
        setTimeout(() => animateStatBars(details), 50);
        // Find the moves container and fetch moves if not already loaded
        const movesContainer = card.querySelector('.moves-section');
        if (movesContainer && movesContainer.querySelector('.moves-loading')) {
            loadMoves(pokemon.name, movesContainer);
        }
        currentPokemon = pokemon;
    } else {
        currentPokemon = null;
        currentMoves = null;
    }
});

What changed in the click handler?

  • isExpanding checks whether the card is opening (not already expanded).
  • When expanding, it finds the .moves-section element and calls loadMoves only if the loading placeholder is still present (avoids re-fetching on second click).
  • When collapsing, currentPokemon and currentMoves are cleared so the app knows nothing is selected.
  • Save app.js.
  • Refresh your browser, search for fire, and click Charizard.
  • You should see "Loading moves..." text appear below the stat bars for a moment, then change to "Could not load moves." after the fetch completes.

Why does it say 'Could not load moves'?

loadMoves tries to call renderMoveTabs, which does not exist yet. The resulting error is caught by the catch block, which shows the error message. You will fix this in the next substep by adding the rendering functions.

Not seeing the loading text at all?

  • Check that ${buildMovesSection(pokemon.name)} is inside the card.innerHTML template, after the stat bars line.
  • Open your browser's DevTools console (F12) and check for syntax errors that prevent the file from loading.
  • Make sure your server is still running in the terminal.

Help me debug why the moves loading text isn't appearing in my Pokedex card.

Render the moves table for each tab

Now you need the two functions that turn the raw move data into a tabbed, readable table. renderMoveTabs builds the tab buttons for each learn method, and renderMoveTable fills in the table rows for the active tab.

  • In app.js, add the following function directly after loadMoves:
function renderMoveTabs(movesData, container) {
    const methodLabels = { 'level-up': 'Level Up', 'machine': 'TM/HM', 'egg': 'Egg', 'tutor': 'Tutor' };
    const methods = Object.keys(movesData).filter(m => movesData[m].length > 0);

    if (methods.length === 0) {
        container.innerHTML = '<div class="moves-error">No moves found for latest game version.</div>';
        return;
    }

    const tabs = methods.map((m, i) =>
        `<button class="move-tab${i === 0 ? ' active' : ''}" data-method="${m}">${methodLabels[m] || m}</button>`
    ).join('');

    container.innerHTML = `<div class="move-tabs">${tabs}</div><div class="move-table-container"></div>`;

    const tabButtons = container.querySelectorAll('.move-tab');
    const tableContainer = container.querySelector('.move-table-container');

    tabButtons.forEach(btn => {
        btn.addEventListener('click', (e) => {
            e.stopPropagation();
            tabButtons.forEach(b => b.classList.remove('active'));
            btn.classList.add('active');
            renderMoveTable(movesData[btn.dataset.method], tableContainer, btn.dataset.method);
        });
    });

    renderMoveTable(movesData[methods[0]], tableContainer, methods[0]);
}

What does renderMoveTabs do?

  • methodLabels maps the API's keys (level-up, machine, egg, tutor) to human-friendly labels.
  • It creates a button for each learn method that has moves, marks the first one as active, and renders the corresponding table immediately.
  • e.stopPropagation() prevents tab clicks from toggling the card's expanded state (since the card itself also listens for clicks).
  • Add the renderMoveTable function directly below renderMoveTabs:
function renderMoveTable(moves, container, method) {
    const showLevel = method === 'level-up';
    const header = `<tr>${showLevel ? '<th>Lvl</th>' : ''}<th>Move</th><th>Type</th><th>Class</th><th>Pwr</th><th>Acc</th><th>PP</th></tr>`;
    const rows = moves.map(m => {
        const typeColor = TYPE_COLORS[m.type] || '#555';
        const classLabel = m.damage_class === 'physical' ? 'PHY' : m.damage_class === 'special' ? 'SPC' : 'STS';
        return `<tr>
            ${showLevel ? `<td>${m.level || '-'}</td>` : ''}
            <td class="move-name">${m.name.replace(/-/g, ' ')}</td>
            <td><span class="move-type-badge" style="background:${typeColor}">${m.type}</span></td>
            <td class="move-class move-class-${m.damage_class}">${classLabel}</td>
            <td>${m.power || '-'}</td>
            <td>${m.accuracy || '-'}</td>
            <td>${m.pp || '-'}</td>
        </tr>`;
    }).join('');
    container.innerHTML = `<table class="moves-table">${header}${rows}</table>`;
}

What does renderMoveTable do?

  • It builds a table with columns for Move Name, Type, Class, Power, Accuracy, and PP.
  • For level-up moves, it adds a "Lvl" column showing the level the move is learned at.
  • Type badges are colored using the existing TYPE_COLORS map. Damage class is displayed as PHY (physical), SPC (special), or STS (status).
  • Null values for power and accuracy display as a dash - since status moves have no power stat.

Before you test, what do you think you will see when you click Charizard now that all three functions exist?

  • Save app.js and refresh your browser.
  • Search for fire and click Charizard.

You should see tab buttons (Level Up, TM/HM, etc.) appear below the stat bars, with a table of moves underneath. The table works and you can switch between tabs, but it looks unstyled and hard to read.

Still seeing 'Could not load moves'?

  • Make sure renderMoveTabs and renderMoveTable are defined as top-level functions (not nested inside another function).
  • Check the DevTools console for any syntax errors. A missing backtick or brace will prevent the functions from loading.
  • Confirm your server is still running and that visiting http://127.0.0.1:8000/api/pokemon/charizard/moves in a new tab returns JSON data.

Help me debug why my moves table isn't loading after I added renderMoveTabs and renderMoveTable.

Style the moves table to match the sci-fi theme

The moves table works but blends into the page without any visual distinction. You will add CSS for the tab buttons (with a glowing active state), the table rows (with a glass background), and the type badges.

  • Open static/style.css and scroll to the bottom of the file.
  • Add the following styles for the loading state, error message, and tab buttons:
/* Moves section */
.moves-section { margin-top: 1rem; }
.moves-loading { color: var(--text-muted); font-size: 0.8rem; text-align: center; padding: 1rem 0; }
.loading-dots::after { content: '...'; animation: dots 1.5s infinite; }
@keyframes dots { 0% { content: '.'; } 33% { content: '..'; } 66% { content: '...'; } }
.moves-error { color: var(--accent-magenta); font-size: 0.8rem; text-align: center; padding: 1rem 0; }

.move-tabs { display: flex; gap: 0.5rem; margin-bottom: 0.75rem; flex-wrap: wrap; }
.move-tab {
    font-family: 'Exo 2', sans-serif; font-size: 0.7rem; font-weight: 600;
    padding: 0.3rem 0.8rem; border-radius: 4px; border: 1px solid rgba(0,240,255,0.3);
    background: transparent; color: var(--text-muted); cursor: pointer;
    text-transform: uppercase; letter-spacing: 1px; transition: all 0.2s;
}
.move-tab:hover { border-color: var(--accent-cyan); color: var(--text-primary); }
.move-tab.active {
    background: rgba(0,240,255,0.1); border-color: var(--accent-cyan);
    color: var(--accent-cyan); box-shadow: 0 0 8px rgba(0,240,255,0.2);
}
  • Now add the table and badge styles directly below the tab styles:
.moves-table { width: 100%; border-collapse: collapse; font-size: 0.7rem; }
.moves-table th {
    text-align: left; padding: 0.4rem 0.5rem; color: var(--accent-cyan);
    border-bottom: 1px solid rgba(0,240,255,0.2); font-weight: 600;
    text-transform: uppercase; letter-spacing: 1px;
}
.moves-table td { padding: 0.35rem 0.5rem; border-bottom: 1px solid rgba(255,255,255,0.03); }
.moves-table tr:hover td { background: rgba(0,240,255,0.03); }
.move-name { text-transform: capitalize; color: var(--text-primary); }
.move-type-badge {
    font-size: 0.6rem; padding: 0.15rem 0.4rem; border-radius: 3px;
    color: #fff; text-transform: uppercase; letter-spacing: 0.5px;
}
.move-class { font-weight: 600; font-size: 0.65rem; }
.move-class-physical { color: #f08030; }
.move-class-special { color: #6890f0; }
.move-class-status { color: #a8a878; }

What do these styles do?

  • The active tab gets a cyan glow and a translucent background, matching the existing search glow effect.
  • Table rows use a subtle hover highlight so the learner can track across columns.
  • Each damage class has its own color: orange for physical, blue for special, and grey for status.
  • Type badges reuse the same inline color values from TYPE_COLORS in JavaScript, keeping the palette consistent.

What do you expect the moves section to look like now with both the tab glow and the colored badges applied?

  • Save style.css and refresh your browser.
  • Search for fire and click Charizard.

You should see a row of glowing tab buttons (Level Up, TM/HM, etc.) with the first one highlighted in cyan. The table below shows moves with colored type badges, a PHY/SPC/STS class label, and power/accuracy/PP numbers.

  • Click the TM/HM tab to see a different set of moves load into the table.

The active tab switches its glow, and the table re-renders with the TM/HM moves.

Tabs not glowing or badges not colored?

  • Make sure you saved style.css and did a hard refresh (Cmd+Shift+R on macOS, Ctrl+Shift+R on Windows) to clear cached styles.
  • Check that .move-tab.active has no typo. The class must be move-tab (not moves-tab).
  • Open DevTools, inspect a type badge, and confirm the inline background style is set. If it is blank, the JavaScript template literal in renderMoveTable may have a syntax error.

Help me figure out why my moves table styling isn't appearing correctly.

✔️ Awesome, I've got everything!

Great. Double check you have saved both app.js and style.css.

ⓧ I'd like to double check the full code

Your app.js should contain these functions in this order. Scroll through and confirm each one is present:

  • Top-level variables: searchInput, resultsGrid, resultsInfo, titleEl, TITLE_TEXT, SPRITE_BASE, TYPE_COLORS, debounceTimer, currentPokemon, currentMoves.
  • typeWriter() function.
  • spawnParticles() function.
  • buildStatBars() and animateStatBars() functions.
  • buildMovesSection() function.
  • loadMoves() async function.
  • renderMoveTabs() function.
  • renderMoveTable() function.
  • createCard() function (with buildMovesSection in its HTML and the updated click handler calling loadMoves).
  • performSearch() function and searchInput event listener.
  • typeWriter() call at the bottom.

For style.css, confirm you have all the existing styles from the previous project, plus the new moves section styles starting with .moves-section at the bottom.

Your Pokedex now shows a full moves database for every Pokemon. Next up, you will add an AI chatbot that can answer questions about Pokemon strategy.

Add a Pokemon Chatbot

Your Pokedex now shows a full moves database for every Pokemon. But what if you could ask questions about those moves? An AI chatbot could suggest competitive movesets, explain type matchups, or help you build a team.

In this step, you will add a Google Gemini-powered chatbot to your Pokedex. It will answer general Pokemon questions, but it will not yet know which Pokemon you are currently looking at.

In this step, get ready to:
  • Add a /api/chat endpoint to your server that talks to Gemini.
  • Build a floating chat panel UI that matches the sci-fi theme.
  • Wire the frontend to send messages and display AI responses.
Add the /api/chat endpoint

Your server needs a new endpoint that receives the user's message, sends it to the Gemini API with a Pokemon expert system prompt, and returns the AI's response.

  • In server.py, scroll to the bottom of the file and find the line that says:
  • app.mount("/static", StaticFiles(directory="static"), name="static")
  • Add the following endpoint ABOVE that line by copying this code:
@app.post("/api/chat")
async def chat(req: ChatRequest):
    if not gemini_client:
        return {"reply": "Chatbot unavailable. Please add your GEMINI_API_KEY to the .env file and restart the server."}

    system_prompt = "You are a Pokemon expert assistant integrated into a Pokedex app. Give concise, helpful answers about Pokemon strategy, moves, types, and matchups. Keep responses under 150 words."

    if req.context:
        system_prompt += f"\n\nThe user is currently viewing this Pokemon's data:\n{req.context}\n\nUse this data to give specific answers. Reference actual moves and stats from the data when relevant."

    user_message = req.message

    try:
        response = gemini_client.models.generate_content(
            model="gemini-2.5-flash",
            contents=[{"role": "user", "parts": [{"text": user_message}]}],
            config={
                "system_instruction": system_prompt,
                "max_output_tokens": 300,
            },
        )
        return {"reply": response.text}
    except Exception as e:
        return {"reply": f"Sorry, I encountered an error: {str(e)}"}

What does this code do?

The endpoint first checks if the Gemini client was initialized (it won't be if the API key is missing). If the client exists, it builds a system prompt that tells Gemini to act as a Pokemon expert.

The if req.context block is there for later. Right now, the frontend will send an empty context string, so this block won't activate. In the next step, you will pass actual Pokemon data here.

The config dictionary passes the system prompt as a system_instruction and caps the response length at 300 tokens to keep answers concise.

  • Save server.py.
  • Switch to your terminal and restart the server by pressing Ctrl+C to stop it, then running:
uv run fastapi dev server.py
  • Test the new endpoint by opening a second terminal and running:
curl -X POST http://127.0.0.1:8000/api/chat -H "Content-Type: application/json" -d '{"message": "What type is strong against dragon?", "context": ""}'

You should see a JSON response with a reply field containing the AI's answer about Dragon-type weaknesses (Ice, Dragon, and Fairy).

Seeing an error response?

  • If the reply says "Chatbot unavailable", your .env file is missing or the key is not set. Double check it exists in your project root with a valid key.
  • If you see a connection error, make sure the server is running in the first terminal.
  • If you get a 422 error, make sure your JSON body includes both message and context fields.

Help me debug my /api/chat endpoint

Build the chat panel UI

The chatbot endpoint works, but users need a way to interact with it. You will add a floating chat button in the bottom-right corner that opens a panel with a message history and input field.

  • In static/index.html, find the closing </div> tag for the .container div (it appears just before the <script> tag at the bottom).
  • Add the chat panel HTML between that closing </div> and the <script> tag:
    <!-- Chat Panel -->
    <button class="chat-toggle" id="chat-toggle">AI</button>
    <div class="chat-panel" id="chat-panel">
        <div class="chat-header">
            <span class="chat-title">Pokedex AI</span>
            <span class="chat-context" id="chat-context">Select a Pokemon to chat about it</span>
            <button class="chat-close" id="chat-close">X</button>
        </div>
        <div class="chat-messages" id="chat-messages"></div>
        <div class="chat-input-wrapper">
            <input type="text" id="chat-input" placeholder="Ask about this Pokemon..." autocomplete="off">
            <button id="chat-send">Send</button>
        </div>
    </div>

What does this HTML do?

The chat-toggle button is a small circular "AI" button that stays fixed in the bottom-right corner. Clicking it reveals the chat-panel div.

Inside the panel, chat-messages holds the conversation history and chat-context will eventually show which Pokemon you are chatting about (that comes in the next step).

  • Now open static/style.css and add the chat toggle button and panel container styles at the very bottom of the file:
/* Chat panel */
.chat-toggle {
    position: fixed; bottom: 1.5rem; right: 1.5rem; z-index: 900;
    width: 50px; height: 50px; border-radius: 50%;
    background: rgba(15,23,42,0.9); border: 2px solid var(--accent-cyan);
    color: var(--accent-cyan); font-family: 'Orbitron', sans-serif;
    font-size: 0.8rem; font-weight: 700; cursor: pointer;
    box-shadow: var(--glow-cyan); transition: transform 0.2s;
}
.chat-toggle:hover { transform: scale(1.1); }

.chat-panel {
    position: fixed; bottom: 5rem; right: 1.5rem; z-index: 901;
    width: 350px; height: 450px; border-radius: 12px;
    background: rgba(10,14,23,0.95); border: 1px solid rgba(0,240,255,0.3);
    backdrop-filter: blur(15px); display: flex; flex-direction: column;
    box-shadow: 0 0 20px rgba(0,240,255,0.1);
    transform: scale(0) translateY(20px); opacity: 0;
    transition: transform 0.3s, opacity 0.3s; transform-origin: bottom right;
}
.chat-panel.open { transform: scale(1) translateY(0); opacity: 1; }
  • Save both static/index.html and static/style.css.
  • Refresh your browser at http://127.0.0.1:8000. You should see a glowing circular "AI" button in the bottom-right corner.

Don't see the AI button?

  • Make sure you pasted the HTML outside the .container div, not inside it. The chat button needs to be fixed-positioned outside the main layout.
  • Check that the CSS is at the very bottom of style.css with no syntax errors above it (an unclosed brace would break everything below).

Help me find my chat button

  • Add the internal panel styles below the CSS you just added. These style the header, messages, and input area:
.chat-header {
    padding: 0.75rem 1rem; border-bottom: 1px solid rgba(0,240,255,0.2);
    display: flex; align-items: center; gap: 0.5rem;
}
.chat-title { font-family: 'Orbitron', sans-serif; font-size: 0.8rem; color: var(--accent-cyan); font-weight: 700; }
.chat-context { font-size: 0.65rem; color: var(--text-muted); flex: 1; text-align: right; }
.chat-close {
    background: none; border: none; color: var(--text-muted); cursor: pointer;
    font-size: 0.9rem; padding: 0.2rem 0.4rem;
}
.chat-close:hover { color: var(--accent-magenta); }

.chat-messages { flex: 1; overflow-y: auto; padding: 0.75rem; display: flex; flex-direction: column; gap: 0.5rem; }
.chat-msg {
    max-width: 85%; padding: 0.5rem 0.75rem; border-radius: 8px;
    font-size: 0.8rem; line-height: 1.4; word-wrap: break-word;
}
.chat-msg-user {
    align-self: flex-end; background: rgba(0,240,255,0.15);
    border: 1px solid rgba(0,240,255,0.3); color: var(--text-primary);
}
.chat-msg-bot {
    align-self: flex-start; background: rgba(255,255,255,0.05);
    border: 1px solid rgba(255,255,255,0.1); color: var(--text-primary);
}
.chat-thinking { opacity: 0.6; font-style: italic; }
  • Add the input field and send button styles directly below:
.chat-input-wrapper { display: flex; padding: 0.75rem; gap: 0.5rem; border-top: 1px solid rgba(0,240,255,0.2); }
#chat-input {
    flex: 1; padding: 0.5rem 0.75rem; font-size: 0.8rem; font-family: 'Exo 2', sans-serif;
    background: rgba(15,23,42,0.8); border: 1px solid rgba(0,240,255,0.2); border-radius: 6px;
    color: var(--text-primary); outline: none;
}
#chat-input:focus { border-color: var(--accent-cyan); }
#chat-input::placeholder { color: var(--text-muted); }
#chat-send {
    padding: 0.5rem 1rem; font-family: 'Exo 2', sans-serif; font-size: 0.75rem; font-weight: 600;
    background: rgba(0,240,255,0.15); border: 1px solid var(--accent-cyan); border-radius: 6px;
    color: var(--accent-cyan); cursor: pointer; text-transform: uppercase; letter-spacing: 1px;
    transition: background 0.2s;
}
#chat-send:hover { background: rgba(0,240,255,0.25); }
  • Save static/style.css and refresh your browser.
  • Click the "AI" button. The chat panel should slide open with a "Pokedex AI" header, empty message area, and a text input with a Send button.

Why does clicking the button do nothing yet?

The panel has CSS transitions ready (transform: scale(0) hidden, transform: scale(1) when the .open class is added). But nothing adds that class yet. The JavaScript you write next will toggle it.

Wire the frontend to the chat endpoint

The HTML and CSS are in place, but the panel does not open and messages cannot be sent. You need JavaScript to toggle the panel, capture user input, call the API, and display responses.

  • In static/app.js, scroll to the bottom of the file and find the typeWriter(); call (the last line).
  • Add the following code ABOVE typeWriter(); to set up the chat DOM references and toggle behavior:
// Chat functionality
const chatToggle = document.getElementById('chat-toggle');
const chatPanel = document.getElementById('chat-panel');
const chatClose = document.getElementById('chat-close');
const chatMessages = document.getElementById('chat-messages');
const chatInput = document.getElementById('chat-input');
const chatSend = document.getElementById('chat-send');
const chatContext = document.getElementById('chat-context');

chatToggle.addEventListener('click', () => {
    chatPanel.classList.toggle('open');
});

chatClose.addEventListener('click', () => {
    chatPanel.classList.remove('open');
});

function updateChatContext() {
    // Context awareness will be added in the next step
}

function addChatMessage(text, sender) {
    const msg = document.createElement('div');
    msg.classList.add('chat-msg', `chat-msg-${sender}`);
    msg.textContent = text;
    chatMessages.appendChild(msg);
    chatMessages.scrollTop = chatMessages.scrollHeight;
}

What does this code do?

  • The DOM references grab all the chat elements by their IDs so the rest of the code can interact with them.
  • Clicking the toggle button adds or removes the .open class, which triggers the CSS scale transition to show or hide the panel.
  • The addChatMessage helper creates a styled message div and scrolls the chat area to keep the latest message visible.
  • The updateChatContext function is an empty placeholder. You will fill it in the next step when you add Pokemon awareness.
  • Save static/app.js and refresh your browser.
  • Click the "AI" button. The panel should now animate open. Click the "X" to close it.
  • Now add the sendChatMessage function and event listeners directly below the code you just added (still above typeWriter();):
async function sendChatMessage() {
    const message = chatInput.value.trim();
    if (!message) return;
    chatInput.value = '';
    addChatMessage(message, 'user');

    // Show thinking indicator
    const thinking = document.createElement('div');
    thinking.classList.add('chat-msg', 'chat-msg-bot', 'chat-thinking');
    thinking.textContent = 'Thinking...';
    chatMessages.appendChild(thinking);
    chatMessages.scrollTop = chatMessages.scrollHeight;

    try {
        const resp = await fetch('/api/chat', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ message, context: '' }),
        });
        const data = await resp.json();
        thinking.remove();
        addChatMessage(data.reply, 'bot');
    } catch (e) {
        thinking.remove();
        addChatMessage('Error: Could not reach the chatbot.', 'bot');
    }
}

chatSend.addEventListener('click', sendChatMessage);
chatInput.addEventListener('keydown', (e) => {
    if (e.key === 'Enter') sendChatMessage();
});

What does this code do?

  • When the user sends a message, their text appears immediately as a right-aligned cyan bubble (chat-msg-user).
  • A "Thinking..." indicator appears while the API responds, then gets replaced by the actual reply.
  • Notice that context: '' sends an empty string. The chatbot can answer general Pokemon questions, but has no idea what you are currently viewing. You will fix this in the next step.
  • Save static/app.js and refresh your browser.

Before you try the chatbot, predict this: if you ask it "What are good fire-type moves for competitive play?" will it give you a useful answer?

  • Click the "AI" button to open the chat panel.
  • Type What are good fire-type moves for competitive play? and press Enter.

You should see a "Thinking..." indicator, then a helpful response listing popular Fire-type moves. The chatbot works for general knowledge questions.

Now try this: search for "fire", click Charizard to expand it, then ask the chatbot "What Pokemon am I viewing right now?"

The chatbot cannot answer. It says something generic like "I don't have information about what you're currently viewing." That is because the frontend sends an empty context string. The AI has no idea which Pokemon card you expanded.

Why can't the chatbot see my Pokemon?

Large language models only know what you send them in the prompt. Right now, the request body contains just the user's message and an empty context string. The model has no way to "see" your browser screen.

In the next step, you will inject the currently viewed Pokemon's name, types, stats, and full moves list into the context field. This transforms the chatbot from a generic assistant into one that gives advice specific to the Pokemon you are looking at.

✔️ Awesome, I've got everything!

Great. Double check you have saved all three files: server.py, static/index.html, static/style.css, and static/app.js.

ⓧ I'd like to double check the full code

Here is the full /api/chat endpoint that should be in your server.py (above the static mount):

@app.post("/api/chat")
async def chat(req: ChatRequest):
    if not gemini_client:
        return {"reply": "Chatbot unavailable. Please add your GEMINI_API_KEY to the .env file and restart the server."}

    system_prompt = "You are a Pokemon expert assistant integrated into a Pokedex app. Give concise, helpful answers about Pokemon strategy, moves, types, and matchups. Keep responses under 150 words."

    if req.context:
        system_prompt += f"\n\nThe user is currently viewing this Pokemon's data:\n{req.context}\n\nUse this data to give specific answers. Reference actual moves and stats from the data when relevant."

    user_message = req.message

    try:
        response = gemini_client.models.generate_content(
            model="gemini-2.5-flash",
            contents=[{"role": "user", "parts": [{"text": user_message}]}],
            config={
                "system_instruction": system_prompt,
                "max_output_tokens": 300,
            },
        )
        return {"reply": response.text}
    except Exception as e:
        return {"reply": f"Sorry, I encountered an error: {str(e)}"}

Here is the full chat panel HTML (between the </div> closing .container and the <script> tag):

    <!-- Chat Panel -->
    <button class="chat-toggle" id="chat-toggle">AI</button>
    <div class="chat-panel" id="chat-panel">
        <div class="chat-header">
            <span class="chat-title">Pokedex AI</span>
            <span class="chat-context" id="chat-context">Select a Pokemon to chat about it</span>
            <button class="chat-close" id="chat-close">X</button>
        </div>
        <div class="chat-messages" id="chat-messages"></div>
        <div class="chat-input-wrapper">
            <input type="text" id="chat-input" placeholder="Ask about this Pokemon..." autocomplete="off">
            <button id="chat-send">Send</button>
        </div>
    </div>

Here is the chat JavaScript that should be at the bottom of static/app.js (above typeWriter();):

// Chat functionality
const chatToggle = document.getElementById('chat-toggle');
const chatPanel = document.getElementById('chat-panel');
const chatClose = document.getElementById('chat-close');
const chatMessages = document.getElementById('chat-messages');
const chatInput = document.getElementById('chat-input');
const chatSend = document.getElementById('chat-send');
const chatContext = document.getElementById('chat-context');

chatToggle.addEventListener('click', () => {
    chatPanel.classList.toggle('open');
});

chatClose.addEventListener('click', () => {
    chatPanel.classList.remove('open');
});

function updateChatContext() {
    // Context awareness will be added in the next step
}

function addChatMessage(text, sender) {
    const msg = document.createElement('div');
    msg.classList.add('chat-msg', `chat-msg-${sender}`);
    msg.textContent = text;
    chatMessages.appendChild(msg);
    chatMessages.scrollTop = chatMessages.scrollHeight;
}
async function sendChatMessage() {
    const message = chatInput.value.trim();
    if (!message) return;
    chatInput.value = '';
    addChatMessage(message, 'user');

    // Show thinking indicator
    const thinking = document.createElement('div');
    thinking.classList.add('chat-msg', 'chat-msg-bot', 'chat-thinking');
    thinking.textContent = 'Thinking...';
    chatMessages.appendChild(thinking);
    chatMessages.scrollTop = chatMessages.scrollHeight;

    try {
        const resp = await fetch('/api/chat', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ message, context: '' }),
        });
        const data = await resp.json();
        thinking.remove();
        addChatMessage(data.reply, 'bot');
    } catch (e) {
        thinking.remove();
        addChatMessage('Error: Could not reach the chatbot.', 'bot');
    }
}

chatSend.addEventListener('click', sendChatMessage);
chatInput.addEventListener('keydown', (e) => {
    if (e.key === 'Enter') sendChatMessage();
});

Your Pokedex now has a working AI chatbot. Next up, you will give it the superpower of knowing exactly which Pokemon you are looking at.

Give the Chatbot Pokemon Context

Your chatbot can answer general Pokemon questions, but try asking "What Pokemon am I viewing right now?" and it draws a blank. The chatbot receives no information about your current page.

By passing the currently viewed Pokemon's data (name, types, stats, and moves) as context, the chatbot can give specific, grounded answers instead of generic ones. This is the core technique behind context-aware AI features: injecting structured data into the prompt so the model's answers stay relevant.

In this step, get ready to:
  • Track which Pokemon the learner is currently viewing in a global variable.
  • Build a context string from that Pokemon's data and send it to the chat endpoint.
  • Update the chat UI to reflect which Pokemon is active.
Track the currently viewed Pokemon in JavaScript

The variables currentPokemon and currentMoves already exist in your code. Right now nothing sets them when you click a card.

  • In app.js, scroll to the createCard function and find the card click event listener.
  • Find these lines inside the if (isExpanding) block:
if (isExpanding) {
    setTimeout(() => animateStatBars(details), 50);
    const movesContainer = card.querySelector('.moves-section');
    if (movesContainer && movesContainer.querySelector('.moves-loading')) {
        loadMoves(pokemon.name, movesContainer);
    }
}
  • Replace the entire if (isExpanding) block (including its closing brace) with this updated version that tracks the Pokemon and handles collapse:
if (isExpanding) {
    setTimeout(() => animateStatBars(details), 50);
    const movesContainer = card.querySelector('.moves-section');
    if (movesContainer && movesContainer.querySelector('.moves-loading')) {
        loadMoves(pokemon.name, movesContainer);
    }
    // Store the currently viewed Pokemon and update chat UI
    currentPokemon = pokemon;
    updateChatContext();
} else {
    // Clear context when card collapses
    currentPokemon = null;
    currentMoves = null;
    updateChatContext();
}

What does this code do?

When a card expands, currentPokemon stores that Pokemon's full data object (name, types, stats, abilities). The loadMoves function from Step 3 already sets currentMoves when the moves finish loading.

When the card collapses, both variables are cleared so the chatbot knows nothing is selected.

  • Save app.js and refresh your browser.
  • Search for "fire" and click the Charizard card to expand it.
  • Open DevTools with Cmd+Option+I (macOS) or Ctrl+Shift+I (Windows), go to the Console tab, type currentPokemon and press Enter.

You should see an object with Charizard's name, types, stats, and abilities printed in the console.

Seeing undefined in the console?

  • Make sure you saved app.js and did a hard refresh (Cmd+Shift+R or Ctrl+Shift+R).
  • Confirm you clicked a card to expand it before typing currentPokemon in the console.
  • Check that currentPokemon = pokemon; is inside the if (isExpanding) block, not outside it.

Help me debug why currentPokemon is undefined after clicking a card

Pass context to the chat endpoint

The /api/chat endpoint already knows how to use a context field. It injects the context into the system prompt sent to Gemini. Right now the frontend sends an empty string.

  • In app.js, find the sendChatMessage function and locate this line:
// Build context string from current Pokemon
let context = '';
  • Replace that empty context assignment with the following block that builds a structured context string from the current Pokemon's data:
// Build context string from current Pokemon
let context = '';
if (currentPokemon) {
    context = `Name: ${currentPokemon.name}\nTypes: ${currentPokemon.types.join(', ')}\nAbilities: ${currentPokemon.abilities.join(', ')}\nStats: ${JSON.stringify(currentPokemon.stats)}`;
    if (currentMoves) {
        const moveList = Object.entries(currentMoves).map(([method, moves]) =>
            `${method}: ${moves.map(m => `${m.name}(${m.type},${m.damage_class},pwr:${m.power || '-'})`).join(', ')}`
        ).join('\n');
        context += `\nMoves:\n${moveList}`;
    }
}

What does this code do?

  • It checks whether a Pokemon is currently expanded. If so, it builds a multi-line string with the Pokemon's name, types, abilities, and stats.
  • If the moves have loaded, it appends a formatted list of every learnable move grouped by method (level-up, machine, egg, tutor) with each move's type, damage class, and power.
  • This context string is what Gemini reads to understand which Pokemon you are viewing. The more specific the context, the more grounded the AI's answers.
  • Save app.js and refresh your browser.
  • Search for "fire" and click Charizard to expand it.
  • Wait a moment for the moves to load in the tabbed section.
  • Click the AI button in the bottom-right corner to open the chat panel.

Before you send a message, predict: will the chatbot now give you a Charizard-specific answer, or still a generic one?

  • Type "What's a good competitive moveset?" in the chat input and press Enter.

The chatbot should now respond with specific move recommendations that reference actual moves from Charizard's move list (like Flamethrower, Dragon Claw, or Earthquake).

Still getting generic answers?

  • Make sure you expanded Charizard's card and waited for moves to load before sending the message. The context includes moves only after they finish loading.
  • Check that body: JSON.stringify({ message, context }) in your fetch call uses the context variable you just built, not a hardcoded empty string.
  • Open the Network tab in DevTools, find the /api/chat request, and check the request body. The context field should contain Pokemon data.

Help me figure out why my chatbot is not using the Pokemon context I'm sending

Update the chat UI to show context awareness

The chatbot gives great answers now, but the UI still says "Select a Pokemon to chat about it" even when Charizard is expanded. The updateChatContext function is currently empty.

  • In app.js, find the updateChatContext function. It currently looks like this:
function updateChatContext() {
}
  • Replace it with this implementation that updates the chat header and placeholder text based on the current Pokemon:
function updateChatContext() {
    if (currentPokemon) {
        chatContext.textContent = `Chatting about: ${currentPokemon.name}`;
        chatInput.placeholder = `Ask about ${currentPokemon.name}...`;
    } else {
        chatContext.textContent = 'Select a Pokemon to chat about it';
        chatInput.placeholder = 'Ask about this Pokemon...';
    }
}

What does this code do?

When a Pokemon is expanded, the chat panel header shows its name ("Chatting about: charizard") and the input placeholder becomes "Ask about charizard...".

When no Pokemon is selected, it reverts to generic text. This gives the learner a clear signal that the chatbot knows which Pokemon they are viewing.

  • Save app.js and refresh your browser.
  • Search for "fire" and click Charizard.

Before you open the chat panel, predict: what text will the chat header show now?

  • Click the AI button to open the chat panel.

You should see "Chatting about: charizard" in the chat header and the input placeholder reads "Ask about charizard...".

  • Type "What's a good competitive moveset for this Pokemon?" and press Enter.

The chatbot responds with specific move recommendations that reference actual moves from Charizard's move list.

  • Click Charizard again to collapse it. The header reverts to "Select a Pokemon to chat about it".

Chat header not updating?

  • Confirm the chatContext variable at the top of your file references document.getElementById('chat-context'). A typo in the ID means the element is never found.
  • Check that updateChatContext() is called inside both the if (isExpanding) and else branches of the card click handler.

Help me debug why the chat context label is not updating when I click a Pokemon card

✔️ Awesome, I've got everything!

Great. Double check you have saved app.js with all three changes: the card click handler, the context-building logic in sendChatMessage, and the updateChatContext implementation.

ⓧ I'd like to double check the full code

Your app.js is now quite long. Scroll through and confirm these sections are present in order:

  • DOM references and constants at the top (searchInput, TYPE_COLORS, currentPokemon, currentMoves).
  • The typeWriter and spawnParticles functions.
  • The buildStatBars, animateStatBars, buildMovesSection, loadMoves, renderMoveTabs, and renderMoveTable functions.
  • The createCard function with the updated click handler (setting currentPokemon and calling updateChatContext()).
  • The performSearch function and search input listener.
  • Chat DOM references (chatToggle, chatPanel, chatClose, chatMessages, chatInput, chatSend, chatContext).
  • The updateChatContext function with the if/else logic.
  • The addChatMessage and sendChatMessage functions (sendChatMessage now builds the context string).
  • Event listeners for chatSend, chatInput keydown, and the typeWriter() call at the bottom.

Your Pokedex now has a context-aware AI assistant that gives specific answers grounded in real Pokemon data. Next up, you will take on the Secret Mission and add type filtering and column sorting to the moves table.

Secret mission

Add Move Filtering and Sorting

With 30+ moves visible per tab, finding the strongest Fire-type physical attack means scanning every single row. Add type filtering and column sorting to transform the moves table into a practical teambuilding tool.

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely locally with free APIs, so there are no ongoing costs.

Resources you used:

  • The FastAPI dev server (running process in your terminal).
  • The .env file containing your Gemini API key.

Keep everything running

No action needed. Choose this if you are still actively building on your Pokedex.

  • The Gemini free tier has no charges and PokeAPI is completely free with no rate limits for reasonable usage.
  • Your dev server stops automatically when you close the terminal window.

Pause - I'll come back to this later

Shut down the running server to free up your terminal. Your files stay on disk and you can restart any time.

  • Press Ctrl+C in the terminal where your server is running to stop it.
  • To restart later, open a terminal in your pokedex-search folder and run:
uv run fastapi dev server.py

Delete - I don't want to use this again

Remove the files that contain your credentials. The rest of the project files are unchanged from the previous Pokedex project.

  • Press Ctrl+C in the terminal to stop the dev server if it is still running.
  • Delete the .env file containing your Gemini API key by running:
rm .env
  • Confirm the file is gone by running:
ls .env

You should see a "No such file or directory" message confirming the file has been removed.

That's a Wrap!

That's a Wrap!

Nice work! You just extended your Pokedex with a live moves database and an AI-powered chatbot that gives strategy advice grounded in real Pokemon data.

You've learned how to:

  • Build a live moves database that fetches from PokeAPI in real-time, displaying name, type, damage class, power, accuracy, and PP in a tabbed interface organized by learn method.
  • Integrate Google Gemini as a context-aware chatbot that receives the currently viewed Pokemon's full data and gives specific strategy advice grounded in actual move data.
  • Fetch data from external APIs in parallel using httpx and asyncio.gather for performance, and inject structured context into LLM prompts to make AI responses relevant and specific.
  • Secret Mission: Added type filtering and column sorting to transform the moves table into a practical teambuilding research tool.

Ready to quiz yourself?