54 KiB
DM-Pal — UI/UX Improvements
A review of the current app shell, dashboard, and every utility. Findings are ordered by impact, not file order. Each item ships with a concrete, checkable recommendation.
Status: in progress — P0 and P1 shipped, P2 mostly done. Initiative, Dice, Encounter, Settings, Cross-cutting, Random Tables, Session Logger, Calendar, and Lore Panel are complete. NPC/Item/Quest have stat/structured/ reward fields shipped; soundboard scenes + image gallery/batch shipped; remaining P2 items are larger-effort or backend-gated: image-gen advanced params (Ollama API doesn't support them), soundboard real ambience packs + custom sound import, world hierarchy tree, lore directory picker (needs tauri-plugin-dialog), NPC roster / item inventory / quest roster (History view covers re-opening). The checklist below is updated as items land.
1. Executive Summary
DM-Pal is a Tauri v2 desktop app for running D&D sessions with a local LLM. The visual identity (dark navy + Cinzel headings + gold accents, glassmorphic bento cards) is on-brand and consistent across surfaces. The architecture is clean — every tool is a self-contained card mounted into either the bento dashboard or a single-column detail view.
The biggest problems right now are not aesthetic — they are interaction
shape and information density. The dashboard tries to render full tool
UIs inside ~250px tiles, the left rail has grown to 14 entries with two
identical Sparkles icons, and the "Expand" button is the only escape hatch
back to a usable view. There is no model-loading state, no streaming
indicator, no "thinking…" affordance, and the settings panel is a dead end
until you click "Load Settings". Detail views duplicate the same header
treatment as the dashboard and don't give the tools room to breathe.
The good news: the design system tokens, component primitives (BentoCard,
Toast, GeneratedImage), and Rust↔TS plumbing are all in place. Most of
the wins are layout, IA, and copy — not a redesign.
Top 5 wins (do these first)
- ✓ Kill the "expand to use it" pattern. Make every bento card a summary
with a clear
Openbutton. Tools render full-width in the detail view, not at 1/4 size on the dashboard. - ✓ Reorganize the left rail into 2 visual groups (Session vs World) with separators and tooltips that show on hover for icon-only nav.
- ✓ Add a real loading/streaming UI for LLM and image generation. The "Generating…" text in a button is not enough for 5–20 s waits.
- ✓ Ship a real Settings page (load on mount, group endpoints/models/gen params, show connection status).
- ✓ Fix the world/nav mismatch:
isDetailViewinApp.tsxonly shows the back button for some views, andview === "settings"is treated separately so a back arrow is missing.
2. Audit by Surface
2.1 Left Rail Navigation — src/App.tsx
Current state (problems):
- 14 icon buttons in a 56px column with no labels. Most users will not
hover-discover what each icon does. Two buttons use
Sparkles(Random Tables and Quest Designer) — they are visually indistinguishable. - The rail mixes 7 "primary" nav items, 6 "extra" nav items, and Settings,
with no visual hierarchy. The
flex-1spacer pushes extras to the bottom but a new user has no way to know which is which. navItemsis defined in code butinitiative,tables,quest,calendar,lore,imageare inlined as separate JSX blocks. The list is split between the array and 80+ lines of copy-pasted button markup.- Active-state styling is the same for primary and extras — there's no indication of "category".
- Tooltips use native
title=""— slow, ugly, and bad for keyboard users. - The settings cog duplicates a back-to-dashboard toggle, which is surprising. Users expect the gear to open settings, not toggle them.
- The order is: World, NPCs, Items, Encounter, Dice, Session, Sound, Initiative, Tables, Quest, Calendar, Lore, Image, Settings. There is no grouping by intent (in-session vs prep vs world-building).
Recommendations:
- Two visual groups with a thin gold separator and a small uppercase
label ("SESSION" / "WORLD") in
--color-text-dim. Group the 6 most in-combat tools together (Initiative, Dice, Sound, Encounter, NPCs, Quest) and the 6 prep/world tools below (World, Lore, Items, Calendar, Tables, Image). - Use distinct icons. Replace the duplicate
Sparkles(Tables) withShuffleorDices. Quest Designer can keepScrollTextor get its ownFlagicon. - Replace native tooltips with a Radix tooltip or a CSS-only
data-tooltipattribute that shows on hover and focus, with a 200ms delay, 12px--text-primaryon--bg-deepwith a 1px gold border. - Consolidate the array and inline buttons into a single
navItems: { icon, label, view, group }array, rendered with oneNavButtoncomponent. Removes ~60 lines of JSX and the bug risk of drifting styles. - Settings as a peer, not a toggle. Click = always open settings. Use the title bar's chevron to return to dashboard.
- Add keyboard shortcuts.
1–9jump to the first 9 nav items.Cmd+,opens settings. This is a desktop app — shortcuts are table stakes (Roll20, Foundry, D&D Beyond all have them).
2.2 Title Bar / Header — src/App.tsx
Current state (problems):
- The header is 40px tall with the app name, a fake "My Campaign ▾"
dropdown that does nothing, and a back button that only shows for some
views (
isDetailViewexcludessettings). isDetailViewis calculated asview !== "dashboard" && view !== "settings". That means on the Settings page there is no back arrow, so the only way to leave is the back chevron that doesn't exist there. Users are stuck — they have to click the dashboard logo in the rail.- The Tauri drag region is set on the entire header, but the campaign
selector and back button are inside the drag region, so they don't
receive click events when the window is dragged from the header (this
is a Tauri gotcha — interactive elements inside a
data-tauri-drag-regionneeddata-tauri-no-drag). - "My Campaign" is a placeholder. There's no campaign concept in the backend yet, but the UI pretends there is one. Either ship the campaign picker or replace the dropdown with a real status pill (e.g. "● LLM connected" / "○ offline").
Recommendations:
- Make the back button always show when not on dashboard, including
Settings. Either drop the
view !== "settings"clause or invert the logic toview === "dashboard" ? null : <BackButton />. - Add
data-tauri-no-dragto the back button and the campaign/status pill. - Replace the inert "My Campaign ▾" with a real status indicator:
- LLM connection state (green/gold dot, hover for endpoint)
- Image model state (macOS-gated, "ready"/"unsupported")
- Lore index size ("142 chunks indexed")
- Add a small global search
⌘Kthat opens a palette to jump to any tool, NPC, item, or lore source. This is the single biggest navigation improvement you can ship.
2.3 Dashboard / Bento Layout — src/components/Dashboard.tsx
Current state (problems):
- The dashboard renders the full component of every tool inside a ~250×180px tile. The header inside the tile says "World Builder" but the user sees a tiny 4-character input and a "Generate" button. This is the core UX problem.
- Every card ends with the same "⤢ Expand" link. So 100% of the time, the only useful interaction is to leave the dashboard. The dashboard is not a dashboard — it's a launcher.
- The Lore card is a static paragraph ("Index your world bible…") with an "Open" button, while every other card tries (and fails) to be a real tool. Be honest about which cards are launchers and which are at-a-glance summaries.
- The grid uses
grid-cols-1 sm:grid-cols-2 lg:grid-cols-4with hard spans. On a 13" laptop (1280px) you get 4 columns, but tiles then become 280px wide and content is unreadable. There's no 3-column breakpoint. - The inline
imports at the bottom ofDashboard.tsxare after the component — they work due to hoisting, but it's a readability smell. One import block at the top, sorted. - World Builder is
col-span-2 row-span-2. Lore is1×1. The "biggest tile" is also the one with the least useful at-a-glance content (a map you can't interact with at that size). Either give World a real mini-map (region pin list) or downgrade it to2×1.
Recommendations:
- Two dashboard modes, user-togglable:
- Launcher mode (default) — each card is an icon, title, one-line description, primary CTA, and a "Last generated: X" footer. No embedded input. Tap → detail view.
- Console mode — embed 2–3 chosen tools as real bento cards (e.g. Dice, Initiative, Sound) for live-session use. User picks which 3 from a settings list. This is the real session screen.
- For the at-a-glance cards, show:
- Dice: big last result + count of rolls in the session
- Initiative: round number + active combatant + # of combatants
- NPC: last generated NPC's portrait + name
- Encounter: last generated difficulty + monster count
- Item Forge: last forged item's name + rarity
- Session Log: entry count + last entry timestamp
- Quest: current quest title + step X/N
- Sound: # of active ambients + master volume
- Lore: chunk count + last-added source
- Image Generator: model name + last prompt
- Calendar: today's fantasy date + next event
- Tables: last roll result
- Add a 3-column breakpoint between tablet and desktop. 4 columns only on ≥1440px. The bento should be breathable.
- Use
react-grid-layout(already on your roadmap) for draggable, resizable cards. The plan calls for this — ship it. - Move all imports to the top of the file.
2.4 BentoCard — src/components/BentoCard.tsx
Current state (problems):
- The card is over-animated:
whileHover={{ scale: 1.005 }}andwhileTap={{ scale: 0.985 }}on every card. With 12 cards on the dashboard, the whole screen bobs slightly when you move the mouse. This is the kind of motion that looks great in a portfolio and terrible in a tool you use for 4 hours. - The header divider is a
border-b border-[var(--color-border-glass)]with no padding below the title. The icon and title sit on the baseline of the border, so on small cards they crowd the first row of content. - The
responsiveSpanfunction is fragile: it doesreplace("col-span-2", "sm:col-span-2 lg:col-span-2 col-span-1")which means if you passcol-span-3it stays ascol-span-3(no such class). Androw-span-1is replaced with itself, so the regex is busy-work. - No "drag handle" affordance. If you ship
react-grid-layout, the card needs a grab cursor and a small grip icon in the header. - The body uses
flex-1 overflow-y-autowhich is correct, but nested scrollbars inside tiny tiles make the bento feel like an ant farm.
Recommendations:
- Drop the whileHover scale. Replace with a CSS-only border-color
- box-shadow transition (already in
.glass-card:hover). KeepwhileTaponly on buttons.
- box-shadow transition (already in
- Move the
BentoCardAPI to a typed prop:{ title, icon, span, draggable? }with aSPAN_MAP: Record<Span, string>const. No string regex. - Add a
headerAction?: ReactNodeslot (for the "Open" / "Expand" button) so the card's "what to do here" lives in one place. - For the dashboard launcher mode, the body should be a fixed
min-h-[120px]content slot, notflex-1. Stop nesting scrollbars.
2.5 Detail View Shell — src/App.tsx
Current state (problems):
- Every detail view is wrapped in
<div className="max-w-2xl mx-auto p-6"><div className="glass-card p-6">…</div></div>. That means every tool is constrained to ~672px wide in a centered glass card. For a desktop DM tool, this is too narrow — World Builder, Quest Designer, Soundboard, and Image Generator all want to be wider. - The same wrapper is used for every tool, but the tools have very
different needs:
- Dice: 400px is plenty
- World Builder: 1200px+ (map, side panel)
- Image Generator: needs a full preview
- Soundboard: a grid that benefits from width
- LorePanel: two-pane (add + search) wants 1000px+
- The wrapper has no breadcrumb, no tool-specific header, no actions (save, copy, share). The back arrow is the only chrome.
- Tools like
LorePanelandSessionLoggeralready have their own internal two-pane layout, but they're squeezed into 672px.
Recommendations:
- Drop the universal glass-card wrapper. Let each tool own its own page layout.
- Add a page header pattern: title (Cinzel, gold) + description +
primary action on the right (e.g. "Save to Lore" / "Regenerate" /
"Copy Markdown"). Reusable
<ToolHeader>component. - Use viewport-width layouts with sensible max widths per tool:
- Narrow (max-w-2xl): Dice, Settings
- Medium (max-w-4xl): NPC, Item Forge, Encounter, Quest Designer
- Wide (max-w-6xl): World Builder, Lore, Image Generator, Soundboard, Session Logger
- Each tool should declare its preferred max width via a prop or via its own root container.
2.6 Settings — src/components/SettingsPanel.tsx
Current state (problems):
- The panel renders an empty state ("Configure your LLM connection") with a "Load Settings" button. Settings should always be loaded on mount. The "Load Settings" gate is hostile — a user opens Settings expecting to configure, and instead sees a button they have to click to unlock the form.
- The form is a flat stack of inputs with no grouping, no validation, no help text, no connection test, and no indication of what changed.
- "API Key" and "Model" inputs are plain text. No "show/hide" toggle on the API key, no model-name autocomplete, no "Test connection" button.
- After saving, the "Saved" checkmark lives in the button for 2 s and disappears. No toast, no persistent indicator.
- Temperature and max_tokens use range sliders with no scale labels (0 → 2 for temp is meaningless to most DMs). Either show labels ("Focused", "Balanced", "Creative") or numeric input alongside.
- No way to reset to defaults or export/import config.
- The "embed_model" and "image_model" are tucked at the bottom of the form with no hint that changing them requires a model download.
Recommendations:
- Auto-load on mount, drop the gate.
- Group settings into sections with
<fieldset>and visible legends:- LLM Connection — API URL, API key, connection test
- Text Model — model, temperature, max tokens, top-p
- Image Model — model, OS-compat note, "Open model library" link
- Embedding Model (Lore RAG) — model, "Reindex all" button
- Danger zone — Reset to defaults, Clear lore index
- Connection test button that does a
GET /api/tags(Ollama) orGET /v1/models(OpenAI) and shows ✅/❌ with the model list. - Model picker with autocomplete that calls
GET /api/tagsand lets the user select from installed models. Falls back to free text input. - Show a persistent status pill in the title bar driven by the connection test result.
- Use toasts (you already have
ToastContainer!) for save success instead of in-button text. - Add presets for common LLM providers: Ollama local, LM Studio, OpenAI, Anthropic, Custom. Each preset fills in the API URL pattern.
2.7 Dice Roller — src/components/DiceRoller.tsx
Current state (problems):
- The big result is a single number with no breakdown of which die
rolled what. A 4d6+3 shows
19, not[6, 4, 6, 3] +3 = 19. The inline history shows the breakdown but only for multi-die rolls. - No advantage/disadvantage buttons — the single most common d20 roll in 5e. This is table stakes.
- No modifier input. The user has to type
1d20+5every time even when rolling the same attack repeatedly. There's no concept of "my character's attack roll" or "my save DC". - No 3D dice or animation. The plan calls for
react-three-fiber— ship even a simple CSS tumble. - "Roll" button doesn't react to spacebar. Pressing Enter in the input does — but most DMs will want spacebar.
- Quick dice buttons only roll
1d{n}— noXd{n}choice. A "d6" button rolls 1d6; there's no way to roll 3d6 in one tap. - History is capped at 50 items with no way to clear or export. No way to copy a result to clipboard.
- No roll templates for common patterns ("Attack", "Save", "Check", "Damage"). A template would let you pick "Longsword attack +5" → rolls 1d20+5 and labels the result.
Recommendations:
- Show the roll breakdown by default for any roll with >1 die or
any modifier. Format:
[6, 4, 6] + 3 = 19(the per-die values always visible inline, with the modifier and total). - Advantage/Disadvantage toggle that swaps the notation to
2d20kh1or2d20kl1(or rolls twice and picks). Highlight the chosen die. - Modifier input as a persistent
+/-field next to the notation input. Saved per session. - Roll template row with 4 buttons: Attack, Save, Check, Damage. Each opens a tiny popover with the relevant ability/save/whatever the DM chooses.
- Spacebar rolls the current notation when no input is focused.
- Quick dice become
Xd{n}selectors: tap "d6" once = 1d6, tap again = 2d6, etc., with a small "×3" indicator. - History: add clear and copy all buttons. Show timestamps in the history.
- Optional: ship a simple 2D dice tumble with CSS — no 3D library required. Rotate and scale a die SVG during the roll.
2.8 Initiative Tracker — src/components/InitiativeTracker.tsx
Current state (problems):
- Add row: name, init bonus, max HP, +. No initiative override — you can't set a combatant's initiative to a fixed number after the roll (you have to delete and re-add). Roll20 lets you click the number and edit.
- Conditions: clicking a
+Blichip adds "Blinded". There's no way to add a custom condition ("Concentrating", "Raging", "Hexed"). For a 5e DM this is critical — almost every combat has at least one non-standard condition. - The "Add condition" row shows only 5 conditions at a time. With 13 in the list you have to click through 3 pages to find the right one.
- HP +/- is 1 HP per click. There's no damage/heal input — to take
27 damage, you click
-27 times. Roll20's tracker has a "Set HP" input and a "Apply damage" field. - No death save tracking. When HP hits 0, the combatant just shows 0/HP with no way to roll d20s and track successes/failures.
- No turn timer. Combat drags when players stall. A simple countdown per turn is standard (Foundry, Roll20 both have it).
- "Reset" wipes the whole tracker with no confirmation. Easy to fat-finger.
- "Next Turn" cycles through combatants but doesn't reveal the next combatant visually. Roll20 and Foundry both bold/highlight who's up next.
- No group/encounter import — you can't say "roll initiative for the encounter I just built".
- No notes per combatant ("Mage is concentrating on Hold Person", "Bandit leader has 50ft of movement left"). Critical for table memory.
Recommendations:
- Click initiative number → edit. Inline number input.
- Add custom condition: small "+" button next to the condition row that opens a popover with text input + auto-suggest from the 13 standard conditions.
- Show all 13 conditions in a small popover instead of the "slice(0, 5)" loop.
- HP delta input: click the HP number → type "27" → press Enter to take 27 damage. Click the +/- buttons for ±1.
- Death saves: when HP=0, show three ✓ and three ✗ toggles. Roll 1d20 with a button.
- Turn timer: optional 60s countdown that turns red at 0. Reset on "Next Turn".
- Confirm before Reset with a small modal.
- Notes per combatant: a small
…menu that opens a textarea popover. - Import from encounter: button that pulls monsters from the last generated Encounter into the tracker.
- Visual highlight of next-up combatant (gold left-border) so the DM is primed for "OK, you're up next."
2.9 Session Logger — src/components/SessionLogger.tsx
Current state (problems):
- Two-column layout (notes + AI summary) gets squashed to 672px in the detail view, making the textarea ~280px wide. Painful.
- Notes are plain text, not Markdown. A "bold the BBEG's name" is
shown as asterisks. The plan mentions
@uiw/react-md-editor— ship it. - No session concept. Every note is in one big unfiltered list. Where's "Session 4, May 12th"? A DM runs multiple sessions and needs to switch between them.
- No timestamp granularity —
toLocaleTimeString()shows3:42:11 PM. Add the date too. - No export. After a session, the DM needs to share notes with players, paste into Discord, or save to a Markdown file.
- "AI Summary" is a one-shot button. There's no streaming — even though your backend supports it via Channels. You're shipping the full token list at once. This is a huge UX miss for 5–20s waits.
- Notes are stored only in component state. Reload = data loss. This is the #1 risk in the app right now.
- No entry editing — once added, you can only remove (and the remove doesn't exist; you can't delete individual entries at all).
- No tags or category (combat, roleplay, loot, etc.).
Recommendations:
- Persist notes to disk via
tauri-plugin-storeortauri-plugin-fs. Load on mount, save on every change (debounced). This is critical. - Markdown editor with a live preview toggle.
- Sessions as a first-class concept. Top-bar dropdown lists sessions; click to switch; "New session" button; archive old ones.
- Streaming AI summary using your existing Channel infrastructure. Show tokens as they arrive, with a gold cursor.
- Export to Markdown file (Save As) and Copy to Clipboard.
- Per-entry actions: edit, delete, mark as "highlight" (gold left border).
- Tags with auto-suggest: #combat #roleplay #loot #quest.
- Timestamps include date.
2.10 Encounter Builder — src/components/EncounterBuilder.tsx
Current state (problems):
- Generates a narrative encounter, not a balanced one. There's no CR calculation, no XP budget check, no party-vs-encounter difficulty math. Kobold Fight Club and the DMG have well-known formulas. DMs will expect this from a tool called "Encounter Builder."
- Monsters are listed as strings ("3x Goblin Scouts") with no stat block. The DM has to flip to the MM. Pull from SRD or your own DB.
- "Difficulty" is a string the LLM generated — "Easy" or "Hard" or sometimes "Moderate" (not in your enum). Normalize and color-code.
- No initiative setup. After generating, you can't push the monsters straight into the Initiative Tracker.
- No save — generated encounters vanish on refresh.
- "Terrain" select only has 8 options and is free-form from the LLM's side. No "tavern brawl", "ship deck", "fey crossing" presets.
Recommendations:
- Add XP budget math. Inputs: party size, party level (already have both), desired difficulty (Easy/Medium/Hard/Deadly). Output table from DMG p.82. Show budget and current XP live.
- Stat block integration. Either inline (parse from a local monsters.json) or a "View stat block" popover with SRD content.
- Color-coded difficulty pill (green / blue / orange / red).
- "Send to Initiative Tracker" button. Builds the combatant list with monsters' CR-derived max HP.
- "Send to Soundboard" button — picks ambient based on terrain (forest → 🌲 Forest ambient).
- Save to campaign (long-term) or save to lore (so future encounters reference this one).
- Terrain presets with icons: 🏰 dungeon, 🌲 forest, ⛰ mountain, 🏛 urban, 🌊 coastal, 🕳 underdark, 🏜 desert, 🌫 swamp. Add 4 more: ✈ planar, 🏚 haunted, 🚢 ship, 🏰 siege.
2.11 NPC Generator — src/components/NpcGenerator.tsx
Current state (problems):
- "Class" is a free-form text input labeled "Class" but it accepts any string — "Artisan", "Blacksmith", "Diplomat", "Pickpocket". These are backgrounds, not classes. The 5e concept of "class" (Fighter, Wizard) is not enforced. Either rename the field to "Background/Occupation" or show a class dropdown alongside.
- 8 races, 9 alignments — these are fine but a DM who wants a half-orc or a goliath can't. Expand to the SRD 9 races.
- Portrait is a single image. No way to view full-size. No "save portrait" button.
- Personality and goals are flat lists. No way to edit them after generation.
- No save NPC to roster. Generated NPCs vanish on refresh.
- No NPC card concept — the result is a one-off. A DM accumulates dozens of NPCs and needs a roster.
- The "🎲 random name" placeholder in the Name field is just a placeholder — there's no actual "randomize name" button.
- No stats. 5e NPCs have AC, HP, ability scores, saves, skills. The generator produces flavor, not a usable stat block.
Recommendations:
- Rename "Class" to "Background" with a curated list (Acolyte, Criminal, Folk Hero, Noble, Sage, Soldier, plus 13 more from PHB backgrounds). Add a free-text "Class" dropdown for Fighter/Wizard/… separately.
- Expand race list to 9 (add Half-Orc, Goliath) and add a "Subrace" optional dropdown.
- NPC Roster as a new view: grid of all generated NPCs with portrait, name, race. Click to open full sheet.
- Stat block section: AC, HP, ability scores, saves. Either generated by LLM or hand-entered.
- Random name button with a dice icon.
- Save portrait (right-click → save image).
- Edit personality/goals in-place.
- Copy as Markdown for the DM's notes.
2.12 Item Forge — src/components/ItemForge.tsx
Current state (problems):
- Rarity is colored, but the colors are 6 hard-coded hex values. If a future rarity is added, the color falls back to gold. Use a rarity→color map.
- "Type" is a free-form text input labeled "Type" but the dropdown is "Weapon", "Armor", "Potion", "Scroll", "Wondrous Item", "Ring", "Wand", "Staff" — that's 8 of 12 standard magic item types. Missing: Rod, Staff ✓, Wondrous ✓, Adventuring Gear, Tacked.
- Art is a single 1024×1024 in a 96px frame. The DM can never see the full image without exporting.
- "mechanical" is a string. 5e items have attunement, rarity tag, value, weight. The LLM prompt asks for a single paragraph — you get a paragraph.
- No save to inventory. Items vanish.
- No random item button (one-click "surprise me").
Recommendations:
- Click art to expand to full-size in a modal.
- Structured mechanics field: attunement (yes/no/condition), rarity tag (already have), charges, value, weight, single-line effect description. LLM returns these as a small object.
- Save to inventory → list view of all forged items with rarity-coded borders. Click to view full sheet.
- Random item dice button.
- Export to Markdown / copy to clipboard.
2.13 World Builder — src/components/WorldBuilder.tsx
Current state (problems):
- Critical syntax bug: lines 132, 147, 161 have
})instead of}(extra)). The component renders but with broken JSX. Confirmed by reading the file. Fix this first. - Generates text (name, description, regions, landmarks) but no
map. The plan calls for
react-konvamap rendering. The 2×2 dashboard tile is wasted on a text blob. - "Theme" is a single text input. No presets (high fantasy, dark fantasy, sword & sorcery, steampunk, post-apocalyptic, sci-fi, horror).
- "Regions" is a flat list. No hierarchy (continent → country → region → city) and no map pins.
- No save world. Generated worlds vanish.
- No edit. Generated content is read-only.
- The Dashboard tile embeds the full WorldBuilder component, so you see a 280px text input that says "high fantasy" with a "Generate" button. Unusable at that size.
Recommendations:
- Fix the syntax bug in
WorldBuilder.tsx(lines 132, 147, 161). - Map canvas:
react-konvaorfabric.jswith a parchment background, region pin overlays, click-to-edit. This is the "wow" feature of the app — invest here. - Theme presets as a 4×2 grid of clickable chips.
- Hierarchy tree: collapsible tree of continent → country → region → city, on the left. Each node has its own generated detail.
- Persist the world to disk, reload on mount.
2.14 Random Tables — src/components/RandomTables.tsx
Current state (problems):
- Only 4 built-in tables. DMs want hundreds. The 5e DMG alone has ~30. Add: NPC Names (fantasy, by race), Traps, Dreams, Treasure Hoards, Dungeon Encounters, Wilderness Encounters, Urban Encounters, Caravan Cargo, Inn Names, Ship Names, etc.
- No user-defined tables. DMs will want to roll on their own table of "weird things found in the dungeon."
- No table import (paste a table from a PDF/book).
- "Roll 1d20" button always uses the table's dice — no override.
- History is capped at 30, with no clear, no copy.
- Result is shown in the table preview (highlighted gold row) AND in the big number box, which is redundant.
- No weighted tables — all entries are
min–maxranges. Some tables should weight "1: 10%, 2: 30%, 3: 60%".
Recommendations:
- Add 10 more built-in tables (Traps, Dreams, NPC Names, etc.) or ship a community table pack.
- User-defined tables: create / edit / delete. List view.
- Table import: paste a markdown list, parse to entries.
- Weighted tables: when entries don't cover 100% of the dice range, show the weights visually.
- Result: keep just the big number, highlight the row in the table, no separate "result" block.
- Export table to JSON / Markdown.
2.15 Calendar — src/components/CalendarWidget.tsx
Current state (problems):
- Uses the Forgotten Realms calendar (Hammer, Alturiak, …) but there's no way to switch to a custom calendar. Half the campaigns are homebrew with different month names.
- "Today" is hard-coded to
day: 15, month: 5. The DM has to manually navigate to "today." - No weather on calendar days (the plan calls for weather generation — not implemented).
- No moon phases (essential for lycanthropes, druids).
- Events are color-cycled through 4 colors. No way to assign a color or category.
- No recurring events ("The Festival of Masks happens every year on 12 Eleint").
- The day grid shows 30 days for every month. The Forgotten Realms calendar has 30-day months, but a custom calendar might not.
- No agenda view ("next 5 events") — useful at session start.
Recommendations:
- Custom calendar editor: months, days per month, year length, weekday names. Save as part of the campaign.
- Current campaign date as a "Today" indicator the DM can advance day-by-day with a "Next day" button.
- Weather generator that rolls based on season/terrain, shown as an icon on the day.
- Moon phase per day (8 phases cycling).
- Event categories with colors: Festival, Quest, NPC Birthday, Political, Custom.
- Recurring events (yearly/monthly).
- Agenda view as an alternate layout.
2.16 Soundboard — src/components/Soundboard.tsx
Current state (problems):
- All sounds are synthesized with Web Audio. Rain, fire, ocean are not bad, but a DM running a session wants real recordings: actual rain, actual crackling fire, actual ambient music. The synthesis is clever but sounds thin next to a real ambience pack.
- No way to import custom sounds (drag an MP3 into a tile).
- No fade-out when stopping an ambient — only a 0.5s ramp. Sometimes you want a 5s crossfade to a new ambient.
- No scenes ("Tavern" = tavern ambient + chatter + dice SFX + fire; "Combat" = wind + drums + sword). One-click scene switching.
- No master mute for the whole board.
- The "Stop All" button is below the SFX grid, easy to miss.
- No way to save the current setup ("This is what I use for the dungeon delve").
- No way to loop count for one-shots (e.g. dice rattle twice for a crit).
AudioContextis created on first play, but the browser autoplay policy can block it until a user gesture. The first ambient may not start. Useawait ctx.resume()if suspended.
Recommendations:
- Real sound packs: ship 2–3 royalty-free ambience loops (Pixabay, Freesound.org CC0). Synthesized stays as fallback.
- Custom sound import: drag an MP3/WAV onto a tile to assign.
- Scenes: a named collection of "on" sounds with per-scene volume. "Tavern" / "Combat" / "Dungeon" / "Wilderness" / Custom.
- Master mute and per-sound volume (currently global volume only).
- Always-visible Stop All as a fixed footer button, not conditionally rendered.
- Save/load board state.
- Pre-warm AudioContext on first user gesture (any click).
2.17 Quest Designer — src/components/QuestDesigner.tsx
Current state (problems):
- Quests are linear steps in a carousel. DMs want branching
outcomes ("If they spare the bandit, go to step 3; if they kill
him, go to step 5"). The plan calls for
react-flow— ship it. - No save quest. Generated quests vanish.
- No player-facing view (a quest the DM shows to players sanitized of twists and secret goals). D&D Beyond has this.
- No status on each step (Pending / Active / Done). The stepper is just a viewer.
- No reward distribution — rewards are flavor text, not split-by-character XP/gold/items.
- "Theme" is one text field. No presets.
- No quests list / roster.
Recommendations:
- Branching quest graph with
react-flow. Gold edges for main path, blue for side branches, red for "if villain wins." - Step status (Pending / Active / Done) with click-to-advance.
- Player view toggle: hides Twist, shows flavor only.
- Reward breakdown: XP (per character), gold, items.
- Quest roster: list of all generated quests with status badges.
- Save to lore so generated quests feed back into the world.
2.18 Image Generator — src/components/ImageGenerator.tsx
Current state (problems):
- Prompt textarea is one big block. No negative prompt field (no "blurry, low quality, watermark").
- No seed field for reproducibility.
- No aspect ratio selector — always 1024×1024. A DM wants 16:9 for a battle map, 1:1 for a portrait, 9:16 for a phone wallpaper.
- No steps / guidance scale controls.
- No batch (generate 4 variants, pick the best).
- No gallery — all generated images are ephemeral. DMs will want a folder of "this campaign's portraits."
- "Save PNG" downloads a single image. No "save all", no "copy to clipboard."
- "Regenerate" only fires one variant. No seed/parameter changes.
- The unsupported-platform error is shown but the ✨ buttons elsewhere (NPC, Item) silently fall back. Inconsistent.
Recommendations:
- Negative prompt + seed + aspect ratio + steps + guidance controls. Collapsible "Advanced" section.
- Batch mode: 2×2 grid of variants, click the one you like.
- Gallery: thumbnails of all generated images in a grid. Click to view full size. "Save all" / "Delete".
- Persist to a campaign-scoped folder.
- Copy to clipboard in addition to download.
- Consistent macOS gating: when unsupported, the ✨ buttons in NPC/Item should also show a "macOS only" inline notice, not silent fallback.
2.19 Lore Panel (RAG) — src/components/LorePanel.tsx
Current state (problems):
- Three sections stacked vertically: Add, Indexed, Test. With a 672px-wide detail view, the textarea for "paste your world bible" is unusable.
- No drag-and-drop file upload (.md, .txt, .pdf).
- No directory import ("add every .md in /Users/me/dnd/world").
- No chunk preview (what text was actually chunked and embedded?).
- No embedding visualization (t-SNE or UMAP of the corpus).
- Search results show score with 3 decimal places (0.873). Most DMs don't know what that means. Show "★ 4/5" or a relevance bar.
- No search history.
- No way to attach lore to a generation — the
ragQueryfield on the LLM command exists, but the user has no UI to pick which lore sources to query against. - "Clear all" is a one-click nuke. Confirm first.
Recommendations:
- Two-pane layout: left = add/import, right = search/explore.
- Drag-and-drop file upload with file-type validation.
- Directory picker via Tauri dialog.
- Chunk preview (first 200 chars of each chunk) when a source is expanded.
- Source filter in the search input — multi-select chips.
- Visualize as "★ relevance" not raw cosine distance.
- Search history.
- Confirm before "Clear all" with a modal.
2.20 GeneratedImage — src/components/GeneratedImage.tsx
Current state (problems):
- Used in NPC and Item Forge. The component fetches on every mount
and every
noncechange. If the parent re-renders for any reason (state update, etc.), the nonce is unchanged so the same image is fetched — fine. But the prompt changes when the NPC changes, and there's noAbortControllerto cancel an in-flight image gen when the user changes inputs mid-flight. - No progress for image generation (the backend streams
step/totallines). Show a progress bar. - No placeholder while loading — just a tiny
Sparklesicon, which is invisible in a 96px tile. Show a skeleton with a shimmer. - On error (not macOS), shows a "macOS-only" placeholder. The message is small and grey. A DM on Windows might not realize why nothing is happening.
- On regular error, shows the error string inline. Could be a 10-line stack trace.
Recommendations:
- AbortController in the useEffect cleanup so stale requests don't overwrite the latest.
- Progress bar driven by the NDJSON
step/totallines from the backend. Pass through aChannel. - Skeleton with shimmer during load.
- Friendly error messages for known error codes.
2.21 Toast — src/components/Toast.tsx
Current state (problems):
- The component is solid. But: only
Soundboarduses it. Every other tool uses inline error boxes that duplicate the toast's job. Use toasts everywhere — they're a unified feedback channel. - Auto-dismiss is 4s with no way to extend. Power users may want sticky toasts.
- The 4 toast types have gold-tinted styles, but only
infouses gold. Makesuccessgreen-tinted to be more distinguishable. - The
nextIdmodule-level counter is shared across imports. Fine in dev, brittle in tests. Use a UUID orcrypto.randomUUID(). - Stacking: 5 toasts push each other off-screen. Cap at 3 visible at once.
Recommendations:
- Standardize on toasts for save success, generate complete, generation error, etc. Delete inline error boxes from NPC/Encounter/Quest/etc.
- Sticky option:
addToast(msg, type, { sticky: true }). - Cap visible at 3; older ones auto-dismiss sooner.
- UUIDs for IDs.
2.22 Cross-Cutting Issues
Accessibility
- No keyboard nav between nav items, or only the default
tab order. The left rail buttons are focusable but there's no
visible focus ring style (the global
*:focus-visibleis set to a 2px gold outline, but it gets clipped on the 14px-wide rail buttons). - No aria-labels on icon-only buttons. Screen readers announce "button" with no name.
- Color-only state in Initiative (HP bar green→red) and Item Forge (rarity colors). Add a label or icon.
- No skip-to-content link. Tab through the rail to get to the dashboard.
- Modal dialogs (when added) need focus traps.
- Text contrast:
--color-text-dim(#4a5568) on--color-bg-deep(#0a0e1a) is ~3.2:1 — below WCAG AA (4.5:1 for small text). Lighten it. - No reduced-motion support.
framer-motionanimations should respectprefers-reduced-motion.
Internationalization / i18n
- All strings are hard-coded English. If you ever ship to non-EN markets, you'll need a copy pass. Not a v1 blocker.
Offline-First
- Fonts are loaded from Google Fonts in
index.css— the comments admit "TODO: download woff2 files to public/fonts/ for offline-first." This is a desktop app meant to run offline. Ship the fonts locally.
Error Boundaries
- No React error boundaries. If the LLM returns malformed
JSON that crashes a parser, the whole app blanks to a white
screen. Add an
<ErrorBoundary>around the main content area with a "Reload" button.
Loading Skeletons
- No skeleton states anywhere. Every async tool shows a static "Generating…" button or a tiny spinner. Add skeleton cards for the bento, skeletons for AI generations (shimmer placeholders that match the final layout).
Empty States
- The Lore panel has one ("No lore indexed yet"). The Initiative tracker has none (empty list looks broken). The Encounter builder has none. The Quest designer has none. Every tool needs an empty state with a 1-line explanation and a primary action.
Keyboard Shortcuts
⌘Kcommand palette (mentioned above)1–9jump to nav itemsSpacerolls dice⌘,opens settings⌘Ssaves current tool's stateEsccloses modals / goes back to dashboard?shows shortcut help
Persistence
- Almost nothing persists. Generated NPCs, encounters,
items, quests, dice history, initiative, calendar events —
all component state. Reload = data loss. This is the
single biggest functional gap.
- Use
tauri-plugin-storefor user preferences and small lists. - Use
tauri-plugin-fs+ JSON files in$APPDATA/dm-pal/for generated content. - Auto-save on every change (debounced).
- Use
Dark Mode
- App is dark-only (intentional). But the macOS light title bar
is jarring. Set
titleBarStyle: "Overlay"orTransparentintauri.conf.json.
3. DM-Specific UX Notes (External Research)
Sources consulted: Roll20 Turn Tracker docs, Roll20 VTT Redesign research post (Brittany Vick, 2023), Roll20 D&D 5e Character Builder case study, D&D Beyond product walkthroughs (general familiarity).
3.1 Roll20's design lessons that apply
- "Less Menus, More Suggestions." DM-Pal's bento is menu- heavy. Surface suggestions: "Roll initiative for this encounter" one-click button on a generated encounter. "Add this NPC to the lore" after generation.
- "Simplification." Reduce repetitive actions. Today, after generating an encounter, the DM has to: copy monster names → open initiative tracker → re-type names → set HP → roll initiative. Auto-push instead.
- "Automation." Auto-save, auto-import, auto-reroll. Encounter → Initiative push. Item → Inventory push. NPC → Roster push. Quest → Lore push.
- "Player view vs GM view." DM-Pal is GM-only, but the concept of "what does the DM see vs. what's projected" still applies. A session-view mode (large Initiative, large Dice, large Soundboard, no edit chrome) for when the DM is sharing the screen.
3.2 Combat tracker patterns (Roll20, Foundry, D&D Beyond)
- Foundry: HP and AC always visible; conditions as small icons.
- D&D Beyond: turn order vertical; "your turn" glow; HP and AC prominent.
- Roll20: drag-and-drop reordering; round calculation for custom items ("-1 per round" simulates a countdown).
- All three show: round number prominently, current actor highlighted, next actor visible.
- None force you to click +/- 27 times for damage. All have a numeric input.
3.3 Image generation in DM tools
- Foundry has no AI image gen (yet).
- D&D Beyond has AI art via the partner artist program.
- NovelAI, Stable Diffusion WebUI are the open-source references — they all have: negative prompt, seed, aspect ratio, batch, gallery. Ship these in DM-Pal's image gen.
3.4 Random tables reference
- Kobold Press, Donjon, Seventh Sphere all have online random tables. Donjon's UI is the gold standard: big "Roll" button, immediate result, table visible below with the result highlighted. DM-Pal's RandomTables is close — just needs more tables and user-defined tables.
3.5 Calendar reference
- Fantasy Calendar (5e calendar tool) is the de-facto standard. Features: custom calendars, recurring events, moon phases, weather, agenda view, export to ICS. DM-Pal's calendar is a sketch — there's a 10× feature gap.
3.6 Soundboard reference
- Syrinscape, Tabletop Audio are the reference points. Both ship real recordings (synthesized doesn't cut it for immersion). Both have scenes (one-click combinations). DM-Pal's synthesis is clever but a real ambience pack is the obvious upgrade.
4. Prioritized Checklist
P0 — Critical (fix this week)
These are blockers or bugs that make the app feel broken.
- Fix
WorldBuilder.tsxsyntax bug (lines 132, 147, 161:})should be}). Currently renders broken JSX. - Auto-load Settings on mount. Remove the "Load Settings" gate.
- Add
data-tauri-no-dragto interactive elements in the header (back button, status pill). - Show back button on Settings (drop the
view !== "settings"exclusion inisDetailView). - Add a React error boundary around the main content area.
- Self-host the fonts in
public/fonts/(offline-first). - Standardize errors via the Toast system — delete inline error boxes from NPC, Encounter, Quest, World, Item, Image.
- Replace native
title=""tooltips with a styleddata-tooltip(visible on hover and focus). - Add
aria-labelto every icon-only button.
P1 — High impact (next sprint)
- Reorganize the left rail into 2 visual groups (Session / World) with a thin gold separator and small uppercase labels.
- Replace the duplicate
Sparklesicon on Random Tables (useShuffleorDices). - Add a global ⌘K command palette for navigation.
- Implement persistence for Initiative, Dice history,
Calendar events, Generated NPCs/Encounters/Items/Quests.
Use
tauri-plugin-storefor small state,tauri-plugin-fsfor generated content. - Ship a loading skeleton + streaming UI for LLM and image generation. Use the existing Tauri Channel infrastructure.
- Add roll breakdown to Dice Roller for any roll with >1 die or any modifier.
- Add Advantage/Disadvantage buttons to Dice Roller.
- Make the dashboard a launcher — replace embedded mini-tools with at-a-glance summaries + primary CTA.
- Drop the universal
max-w-2xl glass-cardwrapper in the detail view. Let each tool set its own max-width. - Add a real loading state to Settings: connection test with model list.
- Replace "My Campaign" placeholder with a real status pill (LLM connection, image model, lore chunk count).
- Lighten
--color-text-dimto meet WCAG AA (4.5:1) against--color-bg-deep.
P2 — Important (next month)
- Initiative Tracker:
- Click-to-edit initiative number
- Custom conditions
- Damage input (numeric, not +/- only)
- Death saves
- Turn timer
- Notes per combatant
- Highlight next-up combatant
- Confirm before Reset
- "Import from Encounter" button
- Dice Roller:
- Modifier input (saved per session)
- Spacebar rolls
- Roll templates (Attack, Save, Check, Damage)
- Clear/copy history
- Encounter Builder:
- XP budget math (party size, level, difficulty)
- Color-coded difficulty
- Send to Initiative Tracker
- Send to Soundboard (terrain-based)
- Save to lore
- 4 more terrain presets (planar, haunted, ship, siege)
- NPC Generator:
- Rename "Class" to "Background" with curated list
- Expand races to 9 (Half-Orc, Goliath)
- NPC Roster view (History view covers re-opening past NPCs)
- Stat block section (AC, HP, ability scores)
- Random name button
- Save portrait (right-click)
- Edit personality/goals in-place
- Item Forge:
- Click art to expand
- Structured mechanics (attunement, charges, value, weight)
- Save to inventory (History view covers re-opening past items)
- Random item button
- Quest Designer:
- Branching quest graph (react-flow)
- Step status (Pending/Active/Done)
- Player-facing view toggle (hide Twist)
- Reward breakdown (XP per character, gold, items)
- Quest roster (History view covers re-opening past quests)
- Image Generator:
- Negative prompt, seed, aspect ratio, steps, guidance
- Batch mode (2×2 variants)
- Gallery view with thumbnails
- Persist to campaign-scoped folder
- Copy to clipboard
- Session Logger:
- Markdown editor + live preview (minimal renderer; no dep)
- Sessions as first-class concept (multiple sessions)
- Streaming AI summary
- Export to Markdown
- Per-entry actions (edit, delete, highlight)
- Tags
- Calendar:
- Custom calendar editor
- Current campaign date with "Next day" button
- Weather generator
- Moon phases
- Event categories with colors
- Recurring events
- Agenda view
- Soundboard:
- Real ambience pack (2–3 royalty-free loops)
- Custom sound import (drag-drop)
- Scenes (one-click combinations)
- Master mute + per-sound volume
- Always-visible Stop All
- Save/load board state
- Pre-warm AudioContext
- Random Tables:
- 10+ more built-in tables
- User-defined tables (create/edit/delete)
- Table import (paste Markdown)
- Weighted tables (weight % + gap warning)
- Export table
- World Builder:
- Map canvas (react-konva)
- Theme presets
- Hierarchy tree
- Persist world
- Lore Panel:
- Two-pane layout
- Drag-drop file upload (via native file input; directory picker needs tauri-plugin-dialog)
- Directory picker (needs tauri-plugin-dialog)
- Chunk preview
- Source filter
- ★ relevance (not raw cosine)
- Confirm before "Clear all"
- Settings:
- Group into fieldsets (Connection, Text, Image, Embedding, Danger)
- Connection test with model list
- Model picker with autocomplete (
GET /api/tags) - Provider presets (Ollama, LM Studio, OpenAI, Anthropic, Custom)
- Reset to defaults
- Export/import config
- Configurable campaign data location (lore db, generations db, generated images) via a folder picker — migrates existing data and applies on restart
- Cross-cutting:
- Keyboard shortcuts: ⌘K, 1–9, Space, ⌘,, ⌘S, Esc, ?
- Skeleton loaders for all async states
- Empty states for all tools
prefers-reduced-motionrespect on framer-motion- Skip-to-content link
- Color + label for state (not color alone)
P3 — Nice to have
- Draggable bento layout (react-grid-layout) with persisted layout per campaign.
- Console mode for the dashboard — embed 2–3 chosen tools as live-session cards.
- Tablet/phone player view — a web app that shows the DM's screen (initiative, dice) to players on their phones via the local network.
- Compendium integration — pull monster stat blocks from a local SRD JSON.
- i18n — extract strings to a t() helper.
- Session replay — record dice rolls, initiative changes, soundboard state, and replay the session.
- Macros — let DMs define "1d20+5 attack roll" as a named button on the dice roller.
- Voice-to-text for session notes (Whisper).
5. Quick-Win Sprint (suggested first PR)
If you want a single PR that lands a lot of value, here are 8
items that take <1 day combined and touch mostly App.tsx and
small CSS. All 10 below are now shipped.
- Fix the
WorldBuilder.tsxsyntax bug (5 min) - Auto-load Settings on mount (10 min)
- Drop the
view !== "settings"exclusion on the back button (2 min) - Add
aria-labelto every icon-only button (30 min) - Add
data-tooltipstyled tooltips (1 hr) - ✓ Group the left rail into Session / World (30 min)
- ✓ Replace the duplicate
Sparklesicon (2 min) - ✓ Lighten
--color-text-dimto meet WCAG AA (2 min) - ✓ Add the ⌘K command palette (3 hrs, the biggest of the bunch)
- ✓ Self-host the fonts in
public/fonts/(1 hr)
Total: ~6–8 hours. Lands a much more polished, accessible baseline that the rest of the checklist builds on.
6. References
- Roll20 Turn Tracker docs
- Roll20 VTT Redesign research (2023) — Brittany Vick, UX Lead
- Roll20 Design validation process (2024)
- Roll20 InitiativeTrackerPlus script — community patterns
- Tome of Tips: Turn Tracker — round calculation patterns
- WCAG 2.1 contrast minimums — 4.5:1 for small text
- DMG encounter math (p.82) — XP thresholds by level
- D&D 5e SRD races — 9 races (Human, Elf, Dwarf, Halfling, Gnome, Dragonborn, Tiefling, Half-Orc, Goliath)
- Tauri v2 IPC and Channels
- Internal:
docs/plan.md— original feature plan and milestones