🔴 Introduction
Remember the original Pokedex? It was a handheld encyclopedia that gave you information about every Pokemon you encountered. Fast forward to 2025, and I thought: "What if the Pokedex could actually talk back?"
In this article, I'll take you on a deep technical dive into how I built a "Minimal Pokedex" web app. This isn't just a "hello world" tutorial; I'll show you how I handled real-world challenges like API caching, complex UI states, recursive data structures, and finally, integrating a Large Language Model (LLM) for real-time chat.
Best of all? It's built entirely in Python, hosted for free, and the AI costs $0 to run!
🔗 Live Demo: https://pokedex-ai.streamlit.app/
👨💻 GitHub Repo: cam-hm/pokedex-ai
🛠️ The Tech Stack
I chose this stack for speed and simplicity:
- Frontend: Streamlit - Turns Python scripts into shareable web apps in minutes.
- Data: PokeAPI - The gold standard for Pokemon data.
- AI: Groq API - Running Llama-3.3-70b, which is incredibly fast and free for developers.
- Hosting: Streamlit Cloud - One-click deployment from GitHub.
✨ What We're Building (Feature Overview)
Before we dive into the code, let's look at what this app actually does. It's not just a list of names; it's a full-featured companion.
1. 🏠 The Animated Grid
Instead of static images, the home page features a responsive grid of Animated Sprites (using Showdown GIFs). It feels alive and nostalgic.
2. 📊 Deep Detail View
Clicking a Pokemon reveals:
- Official Artwork (High-res)
- Base Stats visualized with colored progress bars.
- Evolution Chain navigation.
- Type Effectiveness calculator.
3. 🤖 The AI Expert
The killer feature. You can chat with the Pokedex!
- "How do I evolve Eevee into Umbreon?"
- "Is this Pokemon good against Water types?" The AI answers using real game data, not just generic knowledge.
📱 Phase 1: The "Monolith" (Building the MVP)
I started exactly how you should start a prototype: One single file (app.py).
My goal was to get something on the screen fast. I didn't worry about folder structures or architecture. I just wanted to fetch Pokemon and show them.
1. Fetching Data (The Naive Way)
I wrote a simple function right inside app.py to hit the PokeAPI. To prevent the app from being slow, I used Streamlit's caching.
# app.py import streamlit as st import requests @st.cache_data def get_pokemon_list(limit=100): url = f"https://pokeapi.co/api/v2/pokemon?limit={limit}" return requests.get(url).json()['results'] pokemon_list = get_pokemon_list() 2. The Grid Layout
I iterated through the list and displayed them. I wanted animated sprites, so I added some logic to check for the "Showdown" GIF first.
# app.py (continued) cols = st.columns(4) for i, pokemon in enumerate(pokemon_list): with cols[i % 4]: # Logic to find the best sprite URL sprite_url = get_sprite_url(pokemon['id']) st.image(sprite_url) 3. Adding Complexity (The Detail View)
Then I added the detail view. This is where things got tricky.
The Tricky Part: Evolution Chains (Recursion!) 🤯
Displaying "Charmander → Charmeleon → Charizard" sounds easy, but the API returns a deeply nested JSON tree (chain -> evolves_to -> evolves_to...).
I had to write a recursive function inside app.py to flatten this tree:
# app.py (still the same file!) def parse_evolution_chain(chain_link, evo_list=None): if evo_list is None: evo_list = [] species_name = chain_link['species']['name'] evo_list.append(species_name) # Recursion magic happens here for evolution in chain_link['evolves_to']: parse_evolution_chain(evolution, evo_list) return evo_list Visualizing Stats
I also wanted those cool colored progress bars for stats (Green for high, Red for low). Streamlit's default bar wasn't enough, so I injected custom HTML/CSS directly into the Python script.
# app.py st.markdown(f""" <div style="width: {percentage}%; background-color: {color};"></div> """, unsafe_allow_html=True) All of this logic—API calls, recursion, HTML generation—was stuffed into app.py.
By the time I finished the core features, app.py had grown to 500+ lines. It was a mix of everything.
It worked, but it was becoming unmanageable.
🏗️ Phase 2: Refactoring for Scale (The "Spaghetti" Problem)
As I added more features—Shiny Toggle, Type Effectiveness Modal, Audio Cries—my app.py started to look like a disaster zone. It was a 500-line monolithic script where UI code was mixed with API logic and state management.
The Problem:
- Readability: I couldn't find anything.
- State Management: Variables were being overwritten.
- Scalability: Adding AI to this mess would be a nightmare.
The Solution: Modular Architecture
I stopped coding features and spent an hour refactoring. I split the app into 4 distinct layers:
pokedex_app/ ├── app.py # Entry point (Main router - only 20 lines!) ├── src/ │ ├── config/ # Constants (Type colors, Gen limits) │ ├── api/ # API Client (Network layer, Caching) │ ├── services/ # Business Logic (Data processing, AI) │ └── ui/ # Presentation Layer │ ├── components/ # Reusable widgets (Modals, Cards) │ ├── home.py # Home Page Logic │ └── detail.py # Detail Page Logic Why this matters:
By moving the Pokemon logic to src/services/pokemon_service.py and the UI to src/ui/, I created a clean slot for the AI. When I was ready to add the chatbot, I didn't have to touch the existing UI code—I just created a new src/services/ai_service.py.
🤖 Phase 3: The AI Brain (Groq + Llama 3)
Now, the killer feature: Conversational AI.
I didn't want a generic chatbot that says "I am an AI model." I wanted a Pokemon Expert that knows exactly which Pokemon you are looking at.
1. The Challenge: Hallucinations
If you ask ChatGPT "How do I beat Charizard?", it gives a generic answer. But what if you're playing a specific ROM hack where Charizard is a Water type? The AI wouldn't know.
2. The Solution: Context Injection (RAG-lite)
I used a technique similar to RAG (Retrieval Augmented Generation). Instead of searching a vector database, I inject the Ground Truth (the exact stats of the current Pokemon) directly into the system prompt.
Here is the exact prompt engineering strategy:
# src/services/ai_service.py class PokemonChatbot: def chat(self, pokemon_name, pokemon_data, user_message): # 1. Extract relevant data stats = pokemon_data['stats'] types = [t['type']['name'] for t in pokemon_data['types']] abilities = [a['ability']['name'] for a in pokemon_data['abilities']] # 2. Construct the Context String # This is what the AI "sees" before it answers context = f""" You are analyzing {pokemon_name}. Type: {', '.join(types)} Abilities: {', '.join(abilities)} Base Stats: {stats} Height: {pokemon_data['height']} | Weight: {pokemon_data['weight']} """ # 3. System Prompt system_prompt = f""" You are a Pokemon Expert. Use the data below to answer the user's question. DATA CONTEXT: {context} GUIDELINES: - Be concise, strategic, and friendly. - Use emojis (🔥, ⚡, 🛡️). - If asked about battles, refer to the specific Stats provided. - Do NOT make up information not in the data. """ # 4. Call Groq API return self.client.chat.completions.create( model="llama-3.3-70b-versatile", messages=[{"role": "system", "content": system_prompt}, ...] ) 3. Why Groq? (Speed is a Feature)
For a chat interface, latency is everything.
- OpenAI GPT-4: Great quality, but can be slow (2-5s) and expensive.
- Groq Llama 3: Instant (<0.5s) and Free for developers.
Groq's LPU (Language Processing Unit) inference engine delivers responses so fast it feels like the AI is pre-generating them. For a real-time companion app, this speed difference is the difference between "cool" and "usable."
🚀 Deployment
directly with GitHub.
The Secret Sauce:
To keep my API keys safe, I used Streamlit's Secrets management.
- Local:
.streamlit/secrets.toml(Git ignored) - Production: Streamlit Dashboard -> App Settings -> Secrets
# .streamlit/secrets.toml GROQ_API_KEY = "gsk_..." 💡 Key Takeaways
- UX Matters: Small details like animated sprites, colored stat bars, and loading states make a "toy app" feel professional.
- Architecture Saves Time: Refactoring early allowed me to add the AI feature in just 30 minutes.
- AI Needs Context: The chatbot is only smart because I programmatically feed it the correct data. Without context, it's just ChatGPT; with context, it's a Pokedex Expert.
👋 Try it out!
Go ahead, ask the AI for a competitive moveset for Magikarp (spoiler: it's Flail).
👉 App: pokedex-ai.streamlit.app
👉 Code: github.com/cam-hm/pokedex-ai
If you enjoyed this write-up, drop a star on the repo! ⭐

Top comments (8)
This is fun
Thank you so much!
This is SO good and detailed. Love the inclusion of EVs and enjoyed talking to it about my boy Heracross.
Thank you so much! Happy you liked it — I’ll keep optimizing for better response.
Spectacular work and an even better explanation!
I would not mind something like that for my Pokemon Pocket collection, very nice work.
Thank you for your kind words!
Some comments may only be visible to logged-in visitors. Sign in to view all comments.