Back to Projects

Tarot with Local AI Agent Suggestion

ReactTypeScriptFastAPIOllamaSSELocal LLM

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

/demos/tarot/DemoOpen

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. 1

    Ask & Load Deck

    The user enters a question and the 78-card ring loads via GET /api/cards.

  2. 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. 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. 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. 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. 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.