Files
2026-08-09 13:49:54 +01:00

50 KiB
Raw Permalink Blame History

AI-Powered Dungeon Master Toolkit

A local, offline-first desktop app built with Tauri v2 + Rust that gives you every tool a DM needs — all powered by a local LLM.

TL;DR

  1. Scaffold a Tauri v2 project.
  2. Add Rust bindings to a local LLM (llama-cpp-2, llama_cpp, or Candle).
  3. Build modular "utilities" (World Builder, NPC Generator, Encounter Designer, etc.).
  4. Wire them up in the front-end via tauri.invoke + Tauri Channels for streaming.
  5. Package and ship — no internet required after first run.

Table of Contents

  1. What a DM Needs
  2. System Architecture
  3. Quick Start — Boilerplate
  4. Local LLM Integration
  5. Core Utilities & AI Features
  6. Front-End Architecture (React/TS)
  7. Design System — Visual Language
  8. UI/UX Feature Design
  9. Data Persistence
  10. Model Management UX
  11. Lore Search / RAG
  12. Security & Legal Considerations
  13. Optional Enhancements
  14. Resources & Quick Links
  15. Development Checkpoints
  16. Final Checklist

1. What a DM Needs

Category Typical Items AI-Powered Utility
World & Setting Campaign bible, maps, lore notes Auto-generate continents, history timelines, religions, backstory snippets
NPCs & Factions Sheets for each character, relationships Create NPC bios, personality traits, dialogue hooks, agenda charts, relationship webs
Plot / Quests Story arcs, side quests, beats Draft story outlines, branching choices, plot twists
Encounters Enemy lists, terrain descriptions Generate balanced combat encounters, environmental modifiers, loot tables
Magic & Items Item descriptions, stats, rarity Forge unique items with lore, backstory, and mechanical effects
Maps Tile sets, hand-drawn maps Render vector/raster terrain, city layouts; fog-of-war overlays
Player Materials Character sheets, handouts Auto-populate sheets from templates; generate handouts/flashcards
Session Management Logbook, notes, schedule Track session logs, note highlights, future hook suggestions
Initiative / Combat Tracker Turn order, HP, conditions, round timer Visual combat tracker with condition tracking
Dice & RNG Physical dice Simulate dice rolls, record outcomes; random table roller
Rule Reference PHB, DMG, 5e SRD Lookup rules (SRD/CC-BY content only), convert stats across editions
Calendar & Weather Fantasy calendar, seasonal events Custom calendar system with weather generation
Soundboard / Ambience Audio clips Local audio playback for tavern, combat, forest ambience
Name Generators Fantasy, sci-fi, historical name banks AI-powered or procedural name generation
Puzzle / Trap Designer Skill-check flows, trap DCs, hints Generate puzzle mechanics, trap stat blocks
Homebrew Importer Custom classes, spells, monsters Import from Markdown/JSON templates

2. System Architecture

┌────────────────────────────────────┐
│       Front-End (TS / React)       │
│   Tauri v2 WebView · Capabilities  │
│   UI · Map Viewer · Dialog Editor  │
│   Drag-and-Drop · File Explorer     │
└───────────────▲────────────────────┘
                │
        invoke() / Channel
                │
┌───────────────┴────────────────────┐
│       Rust Back-End (Tauri API)    │
│   Business Logic · LLM Inference   │
│   File I/O · Session Storage        │
├───────────────▲────────────────────┤
│               │                     │
│   llama-cpp-2 │   SQLite / JSON    │
│   (local GGUF)│   Storage Layer    │
└───────────────┴────────────────────┘

Key Points

  • Offline-first: Core LLM runs locally via GGUF. Remote API is optional for higher-capacity models.
  • Tauri v2 security: UI runs in a sandboxed WebView; all file/system access gated by capability permissions.
  • Plug-in-style utilities: Each tool is a separate feature flag — start with "World Builder", add "Encounter Designer" later.
  • Recommended Tauri plugins:
Plugin Purpose
tauri-plugin-store User settings, UI state, recent campaigns
tauri-plugin-fs Read/write campaign JSON/Markdown files
tauri-plugin-dialog Open/save dialogs for campaigns and maps
tauri-plugin-sql SQLite for indexed NPC/location/loot lookup
tauri-plugin-log Structured logs from Rust and TS
tauri-plugin-updater Auto-updates for app and model packs
tauri-plugin-single-instance Prevent multiple app windows
tauri-plugin-window-state Remember window size/position

Tauri v2 Capabilities

Create src-tauri/capabilities/default.json:

{
  "$schema": "../gen/schemas/desktop-schema.json",
  "identifier": "default",
  "description": "Main DM toolkit capability",
  "windows": ["main"],
  "permissions": [
    "core:default",
    "store:default",
    "fs:default",
    "dialog:default",
    "shell:allow-open",
    "http:default"
  ]
}

For scoped file access:

{
  "identifier": "fs:allow-read-text-file",
  "allow": [{ "path": "$APPDATA/dm-toolkit/**" }]
}

3. Quick Start — Boilerplate

Prerequisites

# Rust & Cargo
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source $HOME/.cargo/env

# Node (for front-end tooling)
brew install node  # macOS / Linux; or use nvm

# Tauri v2 CLI
npm install -D @tauri-apps/cli@latest

Scaffold

npm create tauri-app@latest dm-toolkit -- --template react-ts
cd dm-toolkit
npm install
npm run tauri dev

Note

: Tauri v2 scaffolds both src-tauri/src/lib.rs and src-tauri/src/main.rs (mobile/desktop share the lib). Import commands from @tauri-apps/api/core, not the old v1 path.

Add Dependencies

# LLM binding — pick one:
cargo add llama-cpp-2    # mature, tracks upstream llama.cpp
# cargo add llama_cpp     # higher-level, easier to start
# cargo add candle-core   # pure-Rust alternative (no C++ build deps)

# Remote API fallback
cargo add reqwest

# Storage
cargo add serde_json
cargo add sqlx            # or use tauri-plugin-sql

4. Local LLM Integration

llama-cpp-2 is a mature Rust binding that tracks the upstream llama.cpp project. The real API requires: init backend → load GGUF → create context → tokenize prompt → decode() batch → sampling loop → decode token-by-token.

// src-tauri/src/llm.rs
use std::path::PathBuf;
use std::sync::Arc;
use llama_cpp_2::llama_backend::LlamaBackend;
use llama_cpp_2::model::LlamaModel;
use llama_cpp_2::model::params::LlamaModelParams;
use llama_cpp_2::context::params::LlamaContextParams;

pub struct LlmState {
    model: Arc<LlamaModel>,
    backend: LlamaBackend,
}

impl LlmState {
    pub fn new(model_path: &str) -> anyhow::Result<Self> {
        let backend = LlamaBackend::init()?;
        let model = LlamaModel::load_from_file(
            &backend,
            model_path,
            &LlamaModelParams::default(),
        )?;
        Ok(Self { model: Arc::new(model), backend })
    }

    pub fn generate(&self, prompt: &str, max_tokens: usize, temperature: f32) -> anyhow::Result<String> {
        let mut ctx = self.model.new_context(&self.backend, LlamaContextParams::default())?;
        let tokens = self.model.str_to_token(prompt, llama_cpp_2::model::AddBos::Always)?;
        // ... decode batch, sampling loop, collect output tokens ...
        // (Full implementation in llama-cpp-2 examples)
        todo!("Implement sampling loop")
    }
}

Tip

— Keep model files in $APPDATA/dm-toolkit/models/. On first run, ship a small base model and let the user download heavier ones later.

4.2 Streaming Tokens via Tauri Channels

invoke() returns a single promise, not a stream. For streaming LLM tokens, use Tauri Channels (Channel<T>) or the event system (app.emit() + frontend listen()).

Rust — Streaming Command

use tauri::ipc::Channel;
use serde::Serialize;

#[derive(Clone, Serialize)]
#[serde(tag = "type", content = "payload", rename_all = "camelCase")]
enum LlmEvent {
    Token(String),
    Error(String),
    Done,
}

#[tauri::command]
async fn generate_stream(
    state: tauri::State<'_, LlmState>,
    prompt: String,
    max_tokens: i32,
    channel: Channel<LlmEvent>,
) -> Result<(), String> {
    tauri::async_runtime::spawn(async move {
        // Init backend, load model, create context, tokenize...
        // For each decoded token:
        //   channel.send(LlmEvent::Token(piece)).ok();
        // When done:
        //   channel.send(LlmEvent::Done).ok();
    });
    Ok(())
}

Front-End — Consuming the Stream

import { invoke, Channel } from "@tauri-apps/api/core";

async function streamGenerate(prompt: string) {
  const onToken = new Channel<LlmEvent>();
  onToken.onmessage = (event) => {
    switch (event.type) {
      case "Token": appendText(event.payload); break;
      case "Error": console.error(event.payload); break;
      case "Done": setStreaming(false); break;
    }
  };

  await invoke("generate_stream", { prompt, maxTokens: 512, channel: onToken });
}

4.3 Optional Remote API Fallback

// src-tauri/src/api.rs
use reqwest::Client;
use serde_json::json;

pub async fn query_openai(prompt: &str) -> anyhow::Result<String> {
    let client = Client::new();
    let res = client.post("https://api.openai.com/v1/chat/completions")
        .bearer_auth(std::env::var("OPENAI_API_KEY")?)
        .json(&json!({
            "model": "gpt-4o-mini",
            "messages": [{ "role": "user", "content": prompt }],
            "max_tokens": 512
        }))
        .send()
        .await?
        .json::<serde_json::Value>()
        .await?;
    Ok(res["choices"][0]["message"]["content"].as_str().unwrap_or("").to_string())
}

Important

: GPT-4o-mini is a remote API model, not local. Keep local inference via GGUF and use remote APIs only as an optional fallback.

4.4 Image Generation via Ollama

Ollama's image models (e.g. x/flux2-klein, x/z-image-turbo) reuse the same /api/generate endpoint as text — just a different model name and a different response field. No new binding or dependency; the existing Ollama client in src-tauri/src/llm/mod.rs already posts here.

Request — identical to a text generate, stream: false:

{ "model": "x/flux2-klein:4b", "prompt": "portrait of a grizzled dwarf blacksmith, warm forge light, oil-painting fantasy style", "stream": false }

Response — newline-delimited JSON; the final line (done: true) carries a singular image field (base64 PNG). Intermediate lines stream progress (step/total) for a progress bar. Note: it's image, not images.

{"model":"x/flux2-klein:4b","done":false,"total":4}
{"model":"x/flux2-klein:4b","done":true,"image":"iVBORw0KGgoAAAANSUhEUgAA..."}

Rust command (lazy: reuse the existing Ollama POST, branch on image vs response):

// src-tauri/src/commands/image.rs
#[derive(Deserialize)]
pub struct ImageRequest { pub prompt: String, pub model: Option<String> }

#[derive(Deserialize)] struct OllamaImgResp { done: bool, image: Option<String> }

#[tauri::command]
pub async fn generate_image(
    state: tauri::State<'_, AppState>,
    req: ImageRequest,
) -> Result<String, String> {
    let cfg = state.config.lock().unwrap().clone();
    let model = req.model.unwrap_or_else(|| "x/flux2-klein:4b".into());
    // POST {model, prompt, stream:false} to {cfg.api_url}/api/generate
    // Read NDJSON, take the last line with done:true, return its `image` base64.
    todo!("reuse existing ollama POST; return base64 PNG string")
}

Front-end — base64 PNG drops straight into an <img src={data:image/png;base64,${b64}}>, and is written to $APPDATA/dm-toolkit/images/ via tauri-plugin-fs so it's reused, not regenerated each render.

Where it's used (auto-generated whenever one of these is created, with a regenerate button):

Utility Image prompt template
NPC Generator portrait of a {race} {class}, {alignment} demeanor, {descriptor from personality}, fantasy oil-painting
Item Forge product shot of a {rarity} {weapon_type}, {magical_effect} glow, on dark velvet, dramatic lighting
World Builder fantasy map of {continent_name}, {climate} biome, parchment texture, ink cartography, top-down
Encounter Builder battle map tile, {terrain}, isometric, 1024x1024, for tabletop RPG
Loot / Random Tables item art per rolled result, same template as Item Forge
Handout Renderer scene illustration from the handout's first paragraph

Constraints to bake in up front:

  • macOS-only today — Ollama image models only run on macOS (Apple Silicon via MLX). Gate the image-gen UI behind an OS check on first run; on other platforms fall back to a placeholder/emoji or the optional remote API. (This is an Ollama limitation, not ours.)
  • Slow + heavy — 4B is ~5.7GB, 9B is ~12GB, generation is multi-second. Always generate in the background with a progress bar (drive it from the NDJSON step/total lines), never block the UI thread. Cache results to disk by hash of the prompt.
  • Model picker — add image_model to LlmConfig (default x/flux2-klein:4b; x/z-image-turbo for fast/low-VRAM). Reuse the existing settings panel, don't build a second one.
  • Prompt engineering is the lever — FLUX.2 handles readable text and hex colors, so item/NPC name labels can be rendered into the image where it helps. Default to 1024×1024.

5. Core Utilities & AI Features

Each module exposes a Tauri command (#[tauri::command]) that the front-end calls via invoke.

Module Key AI Tasks Sample Prompt
World Builder Generate continent names, climate zones, major cities, mythic legends — + render a parchment-style map image "Create a fantasy continent with 3 climates and one legendary creature."
NPC Generator Personality traits, background story, NPC goals, relationship webs — + render a portrait "Generate a charismatic dwarf blacksmith who despises elves."
Quest Designer Multi-step plot arcs, branching outcomes, hooks "Outline a 4-chapter quest where the party seeks the lost crown of Rithor."
Encounter Builder Balanced monster groups, terrain modifiers, loot tables — + render a battle-map tile + loot item art "Design an urban ambush for level-5 PCs with 3 bandits and a surprise mob."
Item Forge Stat tables, lore sentences, rarity levels — + render item art (label text rendered in-image) "Create a +2 longsword of fire resistance that grants invisibility once per day."
Initiative Tracker Turn order, HP/conditions, round timer Manage combat state, auto-sort by initiative
Random Tables Roll on customizable tables Treasure, encounters, names, weather
Calendar & Weather Fantasy calendar, seasonal events Custom calendar system with weather
Soundboard Local audio clips Tavern, combat, forest ambience
Map Generator Procedural terrain, fog-of-war overlay Use react-konva or fabric.js with offline vector tiles
Dice Roller 1-to-6 dice + custom pools "Roll 4d8+2."
Session Logger Summarize actions, suggest next hooks "Summarize last session and propose a hook for the next meeting."
Puzzle / Trap Designer Skill-check flows, trap DCs, hints Generate puzzle mechanics and trap stat blocks
Name Generator Fantasy, sci-fi, historical name banks Procedural or AI-powered name generation
Homebrew Importer Import custom content Markdown/JSON templates for classes, spells, monsters
Handout Renderer Markdown → styled handout → export PNG/PDF Generate player handouts

Example: NPC Generator Command

// src-tauri/src/commands/npc.rs
use tauri::command;

#[derive(serde::Deserialize)]
pub struct NpcRequest {
    pub name: Option<String>,
    pub race: String,
    pub class: String,
    pub alignment: String,
}

#[derive(serde::Serialize)]
pub struct NpcResponse {
    pub bio: String,
    pub personality: Vec<String>,
    pub goals: Vec<String>,
}

#[command]
pub fn generate_npc(req: NpcRequest) -> anyhow::Result<NpcResponse> {
    let prompt = format!(
        "Create a detailed NPC profile:\n\
         Name: {}\nRace: {}\nClass: {}\nAlignment: {}\n\n\
         Output in JSON with keys bio, personality (array), goals (array).",
        req.name.unwrap_or_else(|| "Unnamed".into()),
        req.race, req.class, req.alignment
    );
    let raw = crate::llm::generate(&prompt, 512)?;
    let parsed: NpcResponse = serde_json::from_str(&raw)?;
    Ok(parsed)
}

6. Front-End Architecture (React/TS)

State Management

Concern Recommended Tool
UI state Zustand or Jotai
Rust command data TanStack Query (React Query)
Type-safe bindings tauri-specta — generates TS types from Rust commands

Key Libraries

Need Library
Map canvas, tokens, fog-of-war react-konva or fabric.js
Quest / relationship graphs react-flow
Markdown editing @uiw/react-md-editor
Dice 3D animation Three.js / react-three-fiber (optional)
Layout & styling tailwindcss + CSS Grid — bento layouts, design tokens
Glassmorphism & motion framer-motion (hover/press animations), native backdrop-filter
Icons lucide-react — clean, consistent line icons
Scrollable panels @radix-ui/react-scroll-area — for overflow in bento cards

7. Design System — Visual Language

The app should feel like a magical interface — a DM's arcane console. The aesthetic is dark, luminous, and tactile: deep navy backgrounds with gold accents, glass cards that glow softly, and bento layouts that put every tool within reach without clutter.

7.1 Color Palette

Token Hex Usage
--bg-deep #0a0e1a App background, outer chrome
--bg-surface #111827 Sidebar, navigation panels
--bg-card #1a2236 Bento card surfaces
--bg-card-hover #1f2a42 Hover state on cards
--border-glass rgba(212, 175, 55, 0.12) Glass card borders (gold-tinted)
--border-subtle rgba(255, 255, 255, 0.06) Dividers, separators
--gold-bright #d4af37 Primary accent — CTAs, active states, headings
--gold-muted #a8872a Secondary accent — secondary buttons, icons
--gold-glow rgba(212, 175, 55, 0.25) Glow halos, focus rings, box-shadows
--text-primary #e8e0d0 Main body text (warm off-white)
--text-secondary #8b9bb4 Muted text, labels, descriptions
--text-dim #4a5568 Disabled / placeholder text
--danger #ef4444 Error, delete, HP-zero
--success #22c55e Success, save, HP-full
--info #3b82f6 Informational highlights

7.2 Glassmorphism

Every bento card uses the same glass treatment:

.glass-card {
  background: rgba(26, 34, 54, 0.55);       /* --bg-card at 55% opacity */
  backdrop-filter: blur(18px) saturate(1.3);
  -webkit-backdrop-filter: blur(18px) saturate(1.3);
  border: 1px solid rgba(212, 175, 55, 0.12); /* --border-glass */
  border-radius: 16px;
  box-shadow:
    0 0 0 1px rgba(255, 255, 255, 0.03),
    0 2px 8px rgba(0, 0, 0, 0.4),
    inset 0 1px 0 rgba(255, 255, 255, 0.04);
  transition: box-shadow 0.2s, border-color 0.2s;
}

.glass-card:hover {
  border-color: rgba(212, 175, 55, 0.25); /* --border-glass brightened */
  box-shadow:
    0 0 20px rgba(212, 175, 55, 0.08),     /* --gold-glow halo */
    0 4px 16px rgba(0, 0, 0, 0.5),
    inset 0 1px 0 rgba(255, 255, 255, 0.06);
}

Rules of thumb:

  • Glass cards sit on top of a subtle radial gradient or a dim background image (map, parchment texture). The blur lets the background bleed through just enough to feel alive.
  • On hover, a soft gold glow appears — like candlelight catching the edge of a scroll.
  • Active/selected cards get a brighter gold border (--gold-bright) and a stronger glow.
  • Never use full opacity backgrounds inside bento cards — the glass effect is the identity.

7.3 Bento Grid Layout

The main dashboard is a CSS Grid bento — asymmetric tiles that pack related tools together. No sidebars-within-sidebars; everything is a card on the grid.

┌──────────────┬─────────┬─────────┐
│              │  NPC    │  Dice   │
│  World Map   │ Sheet   │ Roller  │
│  (2×2)       │ (1×1)   │ (1×1)   │
│              ├─────────┴─────────┤
│              │                  │
├──────────────┤   Session Log    │
│ Encounter    │   (2×1)         │
│ Builder      │                  │
│ (1×1)        ├────────┬────────┤
│              │Initi-  │ Random │
├──────────────┤ative   │ Tables │
│  Quest       │ Tracker│ (1×1)  │
│  Designer    │ (1×1)  │        │
│  (2×1)       │        │        │
└──────────────┴────────┴────────┘

Implementation (Tailwind + CSS Grid):

// Each bento card is a <BentoCard> wrapper around the real component.
function BentoCard({
  children,
  className,
  span = "col-span-1 row-span-1",
}: Props) {
  return (
    <div className={`glass-card p-4 ${span} ${className ?? ""}`}>
      {children}
    </div>
  );
}

// Dashboard grid
<div className="grid grid-cols-4 grid-rows-4 gap-3 p-4 h-screen bg-bg-deep">
  <BentoCard span="col-span-2 row-span-2"><WorldMap /></BentoCard>
  <BentoCard span="col-span-1 row-span-1"><NpcSheet /></BentoCard>
  <BentoCard span="col-span-1 row-span-1"><DiceRoller /></BentoCard>
  <BentoCard span="col-span-1 row-span-1"><EncounterBuilder /></BentoCard>
  <BentoCard span="col-span-2 row-span-1"><SessionLog /></BentoCard>
  <BentoCard span="col-span-2 row-span-1"><QuestDesigner /></BentoCard>
  <BentoCard span="col-span-1 row-span-1"><InitiativeTracker /></BentoCard>
  <BentoCard span="col-span-1 row-span-1"><RandomTables /></BentoCard>
</div>

Bento rules:

  • Cards are draggable/resizable — DMs can reorganize their workspace per session.
  • Layout preferences persist to tauri-plugin-store.
  • On narrow windows (laptop), the grid collapses to a 2-column flow.
  • Each card header has a subtle gold underline and a lucide-react icon.

7.4 Typography

Element Font Weight Size
App title / H1 Cinzel (serif display) 700 28px
Card headings / H2 Cinzel 600 18px
Body text Inter (sans-serif) 400 14px
Code / stat blocks JetBrains Mono 400 13px
Dice results / numbers JetBrains Mono 700 24px

Self-host the fonts (offline-first!):

@font-face { font-family: 'Cinzel'; src: url('/fonts/Cinzel-SemiBold.woff2'); font-weight: 600; }
@font-face { font-family: 'Cinzel'; src: url('/fonts/Cinzel-Bold.woff2'); font-weight: 700; }
@font-face { font-family: 'Inter'; src: url('/fonts/Inter-Regular.woff2'); font-weight: 400; }
@font-face { font-family: 'Inter'; src: url('/fonts/Inter-Medium.woff2'); font-weight: 500; }
@font-face { font-family: 'Inter'; src: url('/fonts/Inter-SemiBold.woff2'); font-weight: 600; }
@font-face { font-family: 'JetBrains Mono'; src: url('/fonts/JetBrainsMono-Regular.woff2'); font-weight: 400; }
@font-face { font-family: 'JetBrains Mono'; src: url('/fonts/JetBrainsMono-Bold.woff2'); font-weight: 700; }

7.5 Micro-interactions & Motion

Interaction Effect
Card hover Gold border brightens; subtle gold box-shadow glow fades in (0.2s)
Card press / active Scale down to 0.985; inner shadow deepens
Dice roll Dice tumbles with framer-motion spring; result number scales in with a gold flash
AI generation typing Streamed tokens fade in character-by-character with a gold cursor
Tab / panel switch Crossfade (150ms) — no hard jumps
Drag bento card Card lifts (scale 1.03, shadow deepens); drop zone outlines pulse gold
HP bar change Smooth width transition (300ms); color shifts from --success--danger as HP drops
Notification / toast Slides in from top-right with gold left border; auto-dismisses after 4s

7.6 Component Sketch — BentoCard

// BentoCard.tsx — the building block of the entire UI
interface BentoCardProps {
  title: string;
  icon: React.ReactNode;          // lucide-react icon
  span?: string;                   // tailwind grid span classes
  children: React.ReactNode;
}

export function BentoCard({ title, icon, span = "col-span-1 row-span-1", children }: BentoCardProps) {
  return (
    <motion.div
      className={`glass-card flex flex-col overflow-hidden ${span}`}
      whileHover={{ scale: 1.005 }}
      whileTap={{ scale: 0.985 }}
      transition={{ type: "spring", stiffness: 400, damping: 25 }}
    >
      {/* Header */}
      <div className="flex items-center gap-2 pb-2 mb-3 border-b border-[var(--border-glass)]">
        <span className="text-[var(--gold-bright)]">{icon}</span>
        <h2 className="font-cinzel text-sm font-semibold tracking-wide text-[var(--text-primary)]">
          {title}
        </h2>
      </div>
      {/* Body */}
      <div className="flex-1 overflow-y-auto scrollbar-thin">
        {children}
      </div>
    </motion.div>
  );
}

7.7 App Shell Layout

┌────────────────────────────────────────────────────────────┐
│  ⚔ DM-Pal          [Campaign ▾]     [⚙]  [_ □ ✕]       │  ← Title bar (Tauri custom)
├────┬───────────────────────────────────────────────────────┤
│ 🗺 │                                                       │
│ 👤 │            Bento Grid Dashboard                       │
│ ⚔ │                                                       │
│ 🎲 │   ┌──────────┬─────────┬─────────┐                   │
│ 📜 │   │ WorldMap │ NPC     │ Dice    │                   │
│ 🎵 │   │  2×2     │ Sheet   │ Roller  │                   │
│ ⚙ │   │          ├─────────┴─────────┤                   │
│    │   │          │   Session Log     │                   │
│    │   ├──────────┤   2×1             │                   │
│    │   │Encounter ├────────┬─────────┤                   │
│    │   │Builder   │Init.   │ Random  │                   │
│    │   └──────────┴────────┴─────────┘                   │
└────┴───────────────────────────────────────────────────────┘
  • Left rail (56px): icon-only nav, dark surface (--bg-surface), gold active indicator.
  • Top bar: Tauri custom title bar with app name in Cinzel, campaign dropdown, settings gear.
  • Main area: the bento grid, padded, on --bg-deep with a subtle radial gradient center for depth.
  • Cards are user-rearrangeable; layout saves to local store.

7.8 Dark Mode Is the Only Mode

This app is dark-only. The dark blue + gold palette is the brand — it evokes candlelit tavern tables and starlit skies. There is no light mode toggle. All design tokens above are tuned for dark backgrounds:

  • Text contrast ratios meet WCAG AA on --bg-card.
  • Gold accents provide vibrancy without neon.
  • Glass blur prevents the UI from feeling flat while keeping readability.
  • Error/success states use bright red/green that pop against navy.

8. UI/UX Feature Design

How each feature surfaces in the bento dashboard:

Feature Bento Card Design
World Map Large 2×2 card; interactive canvas with zoom, draggable tokens (SVG/Canvas), overlaid lore pins. Background layer is the map image; glass overlay panels for region info on hover.
NPC Sheet 1×1 card; gold-bordered portrait area at top; scrollable stats/bio below. AI-generated text streams in with a typing cursor. Tap the icon to regenerate.
Encounter Board 1×1 card; mini monster tokens dragged onto a terrain thumbnail; difficulty badge (color from --success--danger). Tap to expand into a full-screen overlay.
Session Log Wide 2×1 card; Markdown editor on the left, AI summary panel on the right (glass panel). Timeline of dice rolls and events with gold timestamps.
Dice Roller Panel 1×1 card; big result number in JetBrains Mono 700/24px. Roll history as a subtle scrolling list below. 3D tumble animation on roll.
Initiative Tracker 1×1 card; vertical list of combatants, gold border on active turn. HP bars animate from green → red. Condition badges are small glass pills.
Relationship Web Full-screen overlay; react-flow graph on dark background. Edges colored by sentiment (gold = ally, red = hostile, blue = neutral).
Soundboard 1×1 card; grid of small glass buttons with waveform icons. Active ambience glows gold. Volume slider on each tile.
Quest Designer 2×1 card; branching flowchart (react-flow) with gold connector lines. Nodes are glass cards. AI-suggest button pulses gold when ready.
Calendar & Weather 1×1 card; mini calendar grid with gold today-marker. Weather icon per day. Tap to see forecast details in a glass overlay.

9. Data Persistence

Two complementary storage layers:

Layer What it stores Implementation
JSON / Markdown files World bible, session logs, character sheets — human-editable, shareable tauri-plugin-fs, $APPDATA/dm-toolkit/data/
SQLite Indexed search across NPCs, locations, items, tags, relationships tauri-plugin-sql (sqlite feature) or sqlx with WAL mode + migrations

SQLite Migrations

use tauri_plugin_sql::{Migration, MigrationKind, Builder};

fn main() {
    let migrations = vec![Migration {
        version: 1,
        description: "create_campaign_tables",
        sql: "CREATE TABLE npcs (
                id INTEGER PRIMARY KEY,
                name TEXT,
                race TEXT,
                bio TEXT,
                tags TEXT
              );",
        kind: MigrationKind::Up,
    }];

    tauri::Builder::default()
        .plugin(
            Builder::default()
                .add_migrations("sqlite:campaign.db", migrations)
                .build(),
        )
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

File Persistence Command

use tauri::path::BaseDirectory;

#[tauri::command]
async fn save_campaign(
    app: tauri::AppHandle,
    name: String,
    data: serde_json::Value,
) -> Result<(), String> {
    let path = app
        .path()
        .resolve(format!("campaigns/{}.json", name), BaseDirectory::AppData)
        .map_err(|e| e.to_string())?;

    std::fs::create_dir_all(path.parent().unwrap()).map_err(|e| e.to_string())?;
    std::fs::write(&path, serde_json::to_string_pretty(&data).unwrap())
        .map_err(|e| e.to_string())?;
    Ok(())
}

Tip

: Use WAL mode for concurrent reads, run migrations on startup, and periodic VACUUM INTO for backups.


10. Model Management UX

Local LLMs are not "fire and forget." Plan for:

Concern Implementation
First-run wizard Detect RAM/VRAM → recommend model size → download GGUF from HuggingFace
Model library UI List installed models, quant level, file size, license, hardware fit
Loading state Model load can take 530 s on HDD/CPU. Show progress bar and cancel button
GPU offloading Expose n_gpu_layers slider per model
Context length 2k/4k/8k selector with memory warning
Image model Separate image_model field in LlmConfig (default x/flux2-klein:4b, x/z-image-turbo for speed). macOS-only — show a gated notice on Linux/Windows and disable the buttons. Drive the progress bar from NDJSON step/total.
Generation controls Streaming toggle, temperature, top-p, repeat-penalty per tool
License acceptance First-run "model license + download" wizard; don't silently bundle 4 GB models

11. Lore Search / RAG

To keep AI-generated content consistent with your world:

  1. Chunk your world bible into embeddings (local embedding model or llama.cpp embeddings).
  2. Store vectors in sqlite-vec extension or Qdrant-lite for semantic recall.
  3. Inject relevant lore into LLM prompts before generating content so outputs stay consistent.

  • Rule content: Use 5e SRD / OGL / CC-BY content only for built-in rule lookups. Full PHB/DMG text is copyrighted.
  • AI output: AI-generated names, places, and stat blocks can still resemble Wizards IP — add a disclaimer and content moderation toggle.
  • Model licenses: Model files ship under their own licenses (e.g., Llama 3 requires accepting Meta's license). Build a first-run license acceptance flow.
  • Tauri v2 security: All file/system access goes through Rust and capability permissions — no arbitrary code execution from the WebView.

13. Optional Enhancements

Feature Implementation Hint
Plugin System Load dynamic libraries (.so/.dll) that expose additional commands; register via Tauri's IPC at runtime
Community Share Hub Sync campaigns via P2P or Git repo (DMs can clone and pull)
Multiplayer Mode Use tauri::ipc over WebSocket to synchronize DM/player states in real time
Voice-to-Text Integrate Whisper for dictating notes or NPC dialogues
Custom Fine-Tuning Store prompt libraries locally; fine-tune the local model on your campaign data


15. Development Checkpoints

Milestone 1 — Skeleton & Hello World

  • Scaffold with npm create tauri-app@latest (react-ts template)
  • Configure Tauri v2 capabilities (src-tauri/capabilities/default.json)
  • Verify invoke() round-trip works with a simple greet command
  • Add tauri-plugin-store, tauri-plugin-fs, tauri-plugin-log
  • Commit and tag v0.1.0-skeleton

Milestone 2 — LLM Integration

  • Integrate LLM client with Ollama + OpenAI-compatible API support
  • Implement basic generate(prompt, max_tokens) command
  • Implement streaming infrastructure via Tauri Channels (LlmEvent)
  • Build LLM settings panel (API URL, key, model, temperature)
  • Add generation controls (temperature, top-p, max_tokens)
  • Commit and tag v0.2.0-llm

Milestone 3 — Persistence Layer

  • Set up tauri-plugin-store and tauri-plugin-fs
  • Implement LLM config state management (get/set via Tauri commands)
  • Campaign file persistence ready (JSON via fs plugin)
  • Commit (included in v0.2.0-llm)

Milestone 4 — Core Utilities & Design System (First Pass)

  • NPC Generator command + UI
  • World Builder command + UI (placeholder for react-konva)
  • Encounter Builder command + UI
  • Session Logger command + UI
  • Dice Roller command + UI
  • Implement design system: BentoCard, glassmorphism, dark blue + gold tokens
  • Build bento grid dashboard shell with left rail nav
  • Commit and tag v0.4.0-utilities

Milestone 5 — DM-Specific Tools

  • Initiative / Combat Tracker (add combatants, HP bars, conditions, round counter)
  • Random Tables roller (4 built-in tables, dice parser, history)
  • Calendar & Weather generator (Faerûn calendar, events, month nav)
  • Encounter Builder (AI-generated via LLM, terrain selector, difficulty)
  • Commit and tag v0.5.0-dm-tools

Milestone 6 — Lore RAG & Polish (incl. Image Generation)

  • Add generate_image Tauri command (reuse Ollama /api/generate, parse singular image field)
  • Wire generate/regenerate into NPC, Item Forge, World Builder (map), Encounter Builder (battle map + loot), Handout Renderer
  • Persist generated PNGs to $APPDATA/dm-toolkit/images/ keyed by prompt hash (cache, don't regenerate)
  • Progress bar driven from NDJSON step/total; background task, non-blocking UI
  • Gate image-gen UI behind macOS check; placeholder fallback elsewhere
  • Add image_model to LlmConfig + settings picker (no second settings panel)
  • Then proceed to lore RAG & polish below:
  • Add lore chunking + embedding storage (SQLite + brute-force cosine; sqlite-vec upgrade path noted)
  • Inject retrieved lore into LLM prompts (shared generate path)
  • Soundboard / ambience module
  • Handout renderer (Markdown → PDF)
  • Polish all glassmorphism effects, micro-interactions, and gold glow states
  • Ensure WCAG AA contrast on all text over glass cards
  • Responsive bento collapse (2-column on narrow windows)
  • Accessibility pass (keyboard nav, screen reader labels, focus rings in --gold-bright)
  • Commit and tag v0.6.0-lore-polish

Milestone 7 — Testing, CI, Distribution

  • Unit tests for dice math, encounter balance, storage migrations
  • Cross-platform CI with tauri-apps/tauri-action
  • Code signing for macOS and Windows
  • Auto-updater with tauri-plugin-updater + delta updates for model packs
  • Package with npm run tauri build
  • Commit and tag v1.0.0-release

16. Final Checklist

A consolidated checklist covering all phases:

  1. Scaffold with npm create tauri-app@latest (Tauri v2, react-ts)
  2. Configure capabilities (src-tauri/capabilities/default.json)
  3. Integrate LLM client (Ollama + OpenAI-compatible APIs)
  4. Stream tokens via Tauri Channels
  5. Add persistence: tauri-plugin-store + tauri-plugin-fs
  6. Implement design system: glassmorphism, bento grid, dark blue + gold palette, Cinzel/Inter/JetBrains Mono fonts
  7. Implement core utilities: World Builder, NPC Generator, Encounter Builder, Dice Roller, Session Logger
  8. Implement DM-specific tools: Initiative Tracker, Random Tables, Calendar, Encounter Builder
  9. Add lore RAG / semantic search (brute-force cosine over Ollama nomic-embed-text embeddings in SQLite; sqlite-vec upgrade path noted)
    • RagStore (rusqlite bundled): chunk-on-paragraph, batch embed via /api/embed, store as little-endian f32 BLOB
    • Commands rag_add / rag_search / rag_list / rag_clear
    • Lore injection in shared generate path via optional ragQuery field (every generator grounded for free)
    • LorePanel UI: add lore, list/clear sources, test retrieval
    • Wired ragQuery into NPC, Item Forge, World Builder; embed_model in settings
  10. Image generation via Ollama (x/flux2-klein) for NPCs, items, maps, loot, handouts — macOS-gated, cached to disk
    • generate_image command (reuses Ollama /api/generate, parses singular image field, disk cache by prompt hash)
    • GeneratedImage reusable component (loading / macOS-gate placeholder / regenerate)
    • Wired into NPC Generator (portrait) + Item Forge (item art)
    • Wire into World Builder (map), Encounter Builder (battle map + loot), Handout Renderer when those surfaces ship
    • image_model in LlmConfig + settings picker
  11. Build first-run model download / license wizard
  12. Add tests, CI (tauri-action), code signing, updater, then package with npm run tauri build

17. Post-Review Action Items (Fresh Pass)

Findings from a fresh codebase review (shell, components, backend, git history). Ordered by impact within each tier. Each item ships as a concrete, checkable change — no redesigns.

17.1 Repo hygiene (do first — cheap)

  • Rewrite README.md. It is still the Vite template boilerplate ("React + TypeScript + Vite") and describes nothing about DM-Pal. Replace with: one-paragraph pitch, screenshot, npm run tauri dev quickstart, prerequisites (Ollama), where data lives, license note. Highest-ROI 20-minute task in the repo.
  • Scrub PII from scripts/apple-signing.env.example. It contains a real Apple ID email (james.twose2711@gmail.com) in a tracked file. Replace with you@example.com.
  • Delete dead scaffold: src/components/Greet.tsx + the greet Tauri command in src-tauri/src/lib.rs. Leftover from npm create tauri-app; nothing in the shell references Greet. Verify with rg Greet first.
  • Fix pointless ternary in App.tsx NavButton: <item.icon size={item.view === "tables" ? 18 : 18} /> — both branches are 18. Just size={18}.
  • Collapse the three nav→kind maps in App.tsx (PREFILLABLE, PREFILLABLE_TOOLS, viewMaxWidth) into a single Record<View, { kind?, Comp?, maxWidth }> and derive the inverse map for rehydrate. Today adding a tool means editing five places; this makes it one.

17.2 Tests & CI (Milestone 7, currently unshipped)

  • src/lib/encounter-budget.ts self-check. Non-trivial combat math with no test. Add an assert-based demo() / one small test_encounter_budget.ts so a tweaked XP number fails loudly.
  • Dice-parser test. DiceRoller parses NdX±M, advantage (kh1/kl1), templates — a parser bug = wrong rolls at the table. One test_dice.ts asserting {count, sides, mod, keep} for 4d6+3, 2d20kh1, 1d20-2 is the smallest thing that catches regressions. (Rust side has good generations + image-base64 tests; frontend has almost none — only worldMap.ts.)
  • Gitea Actions CI (not GitHub — repo ships via scripts/gitea-release.sh). A .gitea/workflows/ci.yml running oxlint, tsc -b, the three node scripts/check-*.ts self-checks, and cargo test on each push. Even lint-only is better than none. Same YAML syntax as GitHub Actions, just a different directory.
  • Code signing + notarization for macOS and Windows.
  • Auto-updater via tauri-plugin-updater (+ delta updates for model packs), then package with npm run tauri build.

17.3 Features still missing (verified unchecked in §16)

  • Quest branching graph (reactflow). Quests are a linear carousel today. Branching quests with conditional edges ("if they spare the bandit → step 3; if they kill him → step 5") are what make a quest designer vs a quest outliner. The single biggest remaining "wow" gap in the generators.
  • World hierarchy tree (continent → country → region → city). Today WorldBuilder emits a flat regions[] + landmarks[] pinned on a map, with no nesting or "drill into a region to generate its sub-regions." A collapsible tree on the left of the existing two-pane layout turns World Builder from a one-shot generator into a living campaign bible.
  • Lore directory picker. Doc said "needs tauri-plugin-dialog" — but the plugin is already installed and used in SettingsPanel. This is unblocked: add an "Add directory…" button in LorePanel that calls open({ directory: true }) and walks *.md/*.txt. Low effort, high value for DMs with an existing world-bible folder.
  • Real ambience packs + custom sound import (Soundboard). Ship 23 CC0 loops (Freesound/Pixabay) in public/sounds/, keep synthesis as fallback, and add drag-an-MP3 onto a tile. The difference between "demo" and "session-ready."
  • First-run model download / license wizard. Today a new user must hand-configure API URL + model name with no guidance. A one-time wizard on first launch (detect Ollama → list GET /api/tags → pick text + image + embed model → save) collapses the "open Settings, stare at empty form, give up" funnel. Reuse the existing connection-test + model-list plumbing from SettingsPanel.
  • Image-gen into World Builder (map) + Encounter Builder (battle map + loot), per §16 item 10 — still only wired into NPC + Item Forge.
  • Handout renderer (Markdown → PDF), still pending from §15.

17.4 UX refinements (beyond the existing ui-ux-improvements.md)

  • Campaign concept. Everything is one global store (dm-pal-state.json) + two global SQLite DBs; a DM running two campaigns can't separate them. A lightweight "active campaign" selector in the title bar that namespaces store keys + DB files (<dataDir>/<campaign>/) unlocks multi-campaign without a schema migration. The data-dir relocation machinery already exists — generalize it per-campaign.
  • ? shortcut help overlay. Checklist marks ? as shipped but there's no visible cheatsheet — shortcuts are documented only here. A small ?-triggered modal listing ⌘K / 19 / Space / ⌘, / ⌘S / Esc is the difference between "shortcuts exist" and "shortcuts are discoverable." ~40 lines.
  • Global streaming indicator. ConnectionPill shows LLM/image/lore status but there's no "an LLM call is in flight" signal. During a 15s image gen the only feedback is a skeleton in one card. A subtle gold pulse on the pill (or a thin top-of-window progress bar) tells the DM something is working even after they've navigated away from the generating tool.
  • Consistent image-gen macOS gating. ImageGenerator shows a clear "macOS only" message, but the GeneratedImage buttons inside NPC/Item silently fall back. Show the same inline "macOS only" notice there so a Windows DM isn't left wondering why nothing happens.
  • Finish the empty-state set for Initiative (and verify the others) — one <p> + CTA each, already done elsewhere.
  • i18n — extract strings to a t() helper (P3, only if shipping beyond EN).
  • Draggable bento layout (react-grid-layout) with persisted layout per campaign (P3).
  • Console mode for the dashboard — embed 23 chosen tools as live-session cards (P3).
  • Player view — a web app showing the DM's screen (initiative, dice) to phones on the local network (P3).
  • Compendium integration — pull monster stat blocks from a local SRD JSON (P3).
  • Voice-to-text for session notes (Whisper) (P3).
  • Macros — named dice-roll buttons (P3).
  • Session replay — record rolls/initiative/soundboard state and replay (P3).

17.5 Suggested order of attack

# Item Effort Impact
1 Rewrite README 20 min High
2 Delete Greet + greet cmd, fix 18:18 ternary 10 min Low
3 Collapse the 3 nav→kind maps into one 30 min Medium
4 encounter-budget + dice-parser self-tests 1 hr Medium
5 Lore directory picker (dialog already installed) 1 hr High
6 First-run model wizard (reuse connection test) 3 hrs High
7 Quest branching graph (reactflow) 4 hrs High
8 World hierarchy tree 3 hrs High
9 ? help overlay + global streaming indicator 1 hr Medium
10 Real ambience packs + sound import 2 hrs Medium
11 GitHub Actions CI 1 hr Medium
12 Campaign namespace selector 4 hrs High

Items 15 form a single low-risk PR: README, dead-code cleanup, map consolidation, two self-tests, lore directory picker — all independently shippable in one session.


You're now ready to spin up your own AI-powered Dungeon Master toolkit!

Happy crafting — may your dice always land favorably! 🎲