Tarot with Local AI Agent Suggestion
Project Overview
A full-stack interactive tarot reading web application that combines an immersive card selection experience with AI-powered interpretations. Users input their question, browse and select cards from a rotating ring of all 78 tarot cards, choose upright or reversed orientations, and receive personalized readings generated by a locally-hosted LLM — no cloud APIs, no data leaving your machine. The core idea: bring the ritual feel of a real tarot reading into the browser, then let a local AI model act as the reader, weaving together card meanings, spread positions, and the user's question into a coherent, streaming narrative.
Interactive Demo
Demo mode — the AI reading is simulated with pre-composed text (English / 中文 switchable, top-right); card physics and animations are the real app. Full Ollama-backed version on GitHub.
Tech Stack & Links
Frontend
- React + TypeScript
- Vite
Backend
- Python + FastAPI
AI Engine
- Ollama running Gemma 4 12B locally on GPU
Streaming
- Server-Sent Events (SSE) — real-time, token-by-token delivery
Architecture
- Fully local — frontend, API, and LLM all run on one machine with zero external network dependencies at runtime
Key Features
Interactive card ring
All 78 tarot cards are arranged in a rotatable circular formation. Users drag to spin the ring, tap to select cards, and watch them animate into their spread positions — the ring interaction is the UI centerpiece, designed to feel tactile and ceremonial rather than transactional.
Manual upright / reversed selection
After selecting each card, users choose its orientation through a visual flip control. This preserves the traditional tarot practice where the querent's intention matters, rather than relying on random assignment.
Four-card spread with positional meaning
Each card maps to a fixed spread position — Present Situation, Obstacle, Advice, Outcome — giving the AI structured context to build a layered interpretation.
Streaming AI interpretation
The LLM response arrives token by token via SSE, rendering in real time with a typewriter effect, so the reading unfolds gradually — mimicking the pacing of a human reader rather than dumping a wall of text.
Fully offline / privacy-first
The entire stack runs locally. No user questions or card selections ever leave the machine; the LLM (Gemma 4 12B via Ollama) runs on local GPU, making this a zero-trust-required setup.
Algorithm Flow
A reading flows front-to-back through a single request: the user picks four oriented cards from the 78-card ring, the FastAPI backend validates the selection, looks up each card's meaning, assembles a structured prompt, and streams the local model's narration back to the browser token by token over SSE.
- 1
Ask & Load Deck
The user enters a question and the 78-card ring loads via GET /api/cards.
- 2
Select Cards
The user spins the ring, picks 4 cards, and sets each upright or reversed; the frontend POSTs { question, cards: [{ card_id, orientation } ×4] } to /api/reading.
- 3
Validate
The backend rejects bad input with 422 — wrong card count, duplicate cards, or an empty question — and only passes clean requests through.
- 4
Assemble Prompt
The backend looks up each card's upright_meaning or reversed_meaning from tarot_cards.json, loads the system.txt / user.txt templates, and fills the variables: user question, the four card names, their orientations (正位 / 逆位), and their meanings.
- 5
Stream from the Local LLM
The assembled messages go to Ollama's /api/chat (model gemma4:12b, stream: true). An SSE stream returns a metadata event (card meanings for the flip-reveal UI), token events (text, character by character), and a done event.
- 6
Render
The frontend plays the card-flip animations, types out the streaming reading, and shows a 'Read Again' button.
Challenges & Solutions
Keeping prompt quality high with a local 12B model
Smaller models struggle with vague instructions. The solution was a highly structured prompt template that pre-injects each card's traditional meaning, its spread-position label, and the user's exact question — so the model doesn't need to 'know' tarot from its training data. It just needs to synthesize and narrate, which a 12B model handles well.
SSE over POST requests
The browser's native EventSource API only supports GET, but the reading request carries a JSON body (question + 4 cards). The frontend instead uses the fetch API with a ReadableStream to parse SSE events line by line, handling edge cases like multi-byte UTF-8 characters split across chunks.
Making 78 cards feel browsable, not overwhelming
Laying out 78 items in a flat grid would be visually noisy and unengaging. The circular ring arrangement shows a natural subset at any time while letting users spin through the full deck — turning card selection into an exploratory, almost meditative interaction.
Enforcing architecture discipline across a multi-phase build
The project was built by an autonomous coding agent (Fable 5) following a modular build manual. To prevent the agent from drifting — embedding prompt text in frontend code, hardcoding model names, skipping error handling — the manual defines 7 'iron rules' with machine-verifiable grep/test commands that gate each phase's completion. No phase passes validation until every rule checks clean.