diff --git a/docs/ui-ux-improvements.md b/docs/ui-ux-improvements.md index 03fe392..bf56395 100644 --- a/docs/ui-ux-improvements.md +++ b/docs/ui-ux-improvements.md @@ -4,8 +4,8 @@ ordered by impact, not file order. Each item ships with a concrete, checkable recommendation.* -> **Status:** audit only — no code changes yet. Treat the checklist at the -> bottom as the punch list for the next sprint. +> **Status:** in progress — P0 and P1 shipped, P2 largely done. The +> checklist below is updated as items land. --- @@ -32,16 +32,16 @@ the wins are layout, IA, and copy — not a redesign. ### Top 5 wins (do these first) -1. **Kill the "expand to use it" pattern.** Make every bento card a *summary* +1. ✓ **Kill the "expand to use it" pattern.** Make every bento card a *summary* with a clear `Open` button. Tools render full-width in the detail view, not at 1/4 size on the dashboard. -2. **Reorganize the left rail into 2 visual groups** (Session vs World) with +2. ✓ **Reorganize the left rail into 2 visual groups** (Session vs World) with separators and tooltips that show on hover for icon-only nav. -3. **Add a real loading/streaming UI** for LLM and image generation. The +3. ✓ **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. -4. **Ship a real Settings page** (load on mount, group endpoints/models/gen +4. ✓ **Ship a real Settings page** (load on mount, group endpoints/models/gen params, show connection status). -5. **Fix the world/nav mismatch**: `isDetailView` in `App.tsx` only shows +5. ✓ **Fix the world/nav mismatch**: `isDetailView` in `App.tsx` only shows the back button for some views, and `view === "settings"` is treated separately so a back arrow is missing. @@ -997,113 +997,113 @@ familiarity). These are blockers or bugs that make the app feel broken. -- [ ] **Fix `WorldBuilder.tsx` syntax bug** (lines 132, 147, 161: +- [x] **Fix `WorldBuilder.tsx` syntax bug** (lines 132, 147, 161: `})` should be `}`). Currently renders broken JSX. -- [ ] **Auto-load Settings on mount.** Remove the "Load Settings" +- [x] **Auto-load Settings on mount.** Remove the "Load Settings" gate. -- [ ] **Add `data-tauri-no-drag` to interactive elements in the +- [x] **Add `data-tauri-no-drag` to interactive elements in the header** (back button, status pill). -- [ ] **Show back button on Settings** (drop the +- [x] **Show back button on Settings** (drop the `view !== "settings"` exclusion in `isDetailView`). -- [ ] **Add a React error boundary** around the main content +- [x] **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 +- [x] **Self-host the fonts** in `public/fonts/` (offline-first). +- [x] **Standardize errors via the Toast system** — delete inline error boxes from NPC, Encounter, Quest, World, Item, Image. -- [ ] **Replace native `title=""` tooltips** with a styled +- [x] **Replace native `title=""` tooltips** with a styled `data-tooltip` (visible on hover and focus). -- [ ] **Add `aria-label`** to every icon-only button. +- [x] **Add `aria-label`** to every icon-only button. ### P1 — High impact (next sprint) -- [ ] **Reorganize the left rail** into 2 visual groups +- [x] **Reorganize the left rail** into 2 visual groups (Session / World) with a thin gold separator and small uppercase labels. -- [ ] **Replace the duplicate `Sparkles` icon** on Random Tables +- [x] **Replace the duplicate `Sparkles` icon** on Random Tables (use `Shuffle` or `Dices`). -- [ ] **Add a global ⌘K command palette** for navigation. -- [ ] **Implement persistence** for Initiative, Dice history, +- [x] **Add a global ⌘K command palette** for navigation. +- [x] **Implement persistence** for Initiative, Dice history, Calendar events, Generated NPCs/Encounters/Items/Quests. Use `tauri-plugin-store` for small state, `tauri-plugin-fs` for generated content. -- [ ] **Ship a loading skeleton + streaming UI** for LLM and +- [x] **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 +- [x] **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 +- [x] **Add Advantage/Disadvantage buttons** to Dice Roller. +- [x] **Make the dashboard a launcher** — replace embedded mini-tools with at-a-glance summaries + primary CTA. -- [ ] **Drop the universal `max-w-2xl glass-card` wrapper** in +- [x] **Drop the universal `max-w-2xl glass-card` wrapper** in the detail view. Let each tool set its own max-width. -- [ ] **Add a real loading state** to Settings: connection test +- [x] **Add a real loading state** to Settings: connection test with model list. -- [ ] **Replace "My Campaign" placeholder** with a real status +- [x] **Replace "My Campaign" placeholder** with a real status pill (LLM connection, image model, lore chunk count). -- [ ] **Lighten `--color-text-dim`** to meet WCAG AA (4.5:1) +- [x] **Lighten `--color-text-dim`** to 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 + - [x] Click-to-edit initiative number + - [x] Custom conditions + - [x] Damage input (numeric, not +/- only) + - [x] Death saves + - [x] Turn timer + - [x] Notes per combatant + - [x] Highlight next-up combatant + - [x] Confirm before Reset + - [x] "Import from Encounter" button - [ ] **Dice Roller**: - - [ ] Modifier input (saved per session) - - [ ] Spacebar rolls - - [ ] Roll templates (Attack, Save, Check, Damage) - - [ ] Clear/copy history + - [x] Modifier input (saved per session) + - [x] Spacebar rolls + - [x] Roll templates (Attack, Save, Check, Damage) + - [x] 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) + - [x] XP budget math (party size, level, difficulty) + - [x] Color-coded difficulty + - [x] Send to Initiative Tracker + - [x] Send to Soundboard (terrain-based) + - [x] Save to lore + - [x] 4 more terrain presets (planar, haunted, ship, siege) - [ ] **NPC Generator**: - - [ ] Rename "Class" to "Background" with curated list - - [ ] Expand races to 9 (Half-Orc, Goliath) + - [x] Rename "Class" to "Background" with curated list + - [x] Expand races to 9 (Half-Orc, Goliath) - [ ] NPC Roster view - [ ] Stat block section (AC, HP, ability scores) - - [ ] Random name button + - [x] Random name button - [ ] Save portrait (right-click) - - [ ] Edit personality/goals in-place + - [x] Edit personality/goals in-place - [ ] **Item Forge**: - - [ ] Click art to expand + - [x] Click art to expand - [ ] Structured mechanics (attunement, charges, value, weight) - [ ] Save to inventory - - [ ] Random item button + - [x] Random item button - [ ] **Quest Designer**: - [ ] Branching quest graph (react-flow) - - [ ] Step status (Pending/Active/Done) - - [ ] Player-facing view toggle (hide Twist) + - [x] Step status (Pending/Active/Done) + - [x] Player-facing view toggle (hide Twist) - [ ] Reward breakdown (XP per character, gold, items) - [ ] Quest roster - [ ] **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 + - [x] Persist to campaign-scoped folder + - [x] Copy to clipboard - [ ] **Session Logger**: - [ ] Markdown editor + live preview - [ ] Sessions as first-class concept (multiple sessions) - - [ ] Streaming AI summary - - [ ] Export to Markdown - - [ ] Per-entry actions (edit, delete, highlight) + - [x] Streaming AI summary + - [x] Export to Markdown + - [x] Per-entry actions (edit, delete, highlight) - [ ] Tags - [ ] **Calendar**: - [ ] Custom calendar editor - - [ ] Current campaign date with "Next day" button - - [ ] Weather generator - - [ ] Moon phases + - [x] Current campaign date with "Next day" button + - [x] Weather generator + - [x] Moon phases - [ ] Event categories with colors - [ ] Recurring events - [ ] Agenda view @@ -1111,43 +1111,43 @@ These are blockers or bugs that make the app feel broken. - [ ] 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 + - [x] Master mute + per-sound volume + - [x] Always-visible Stop All + - [x] Save/load board state + - [x] Pre-warm AudioContext - [ ] **Random Tables**: - - [ ] 10+ more built-in tables + - [x] 10+ more built-in tables - [ ] User-defined tables (create/edit/delete) - [ ] Table import (paste Markdown) - [ ] Weighted tables - - [ ] Export table + - [x] Export table - [ ] **World Builder**: - [ ] Map canvas (react-konva) - - [ ] Theme presets + - [x] Theme presets - [ ] Hierarchy tree - - [ ] Persist world + - [x] Persist world - [ ] **Lore Panel**: - [ ] Two-pane layout - [ ] Drag-drop file upload - [ ] Directory picker - [ ] Chunk preview - [ ] Source filter - - [ ] ★ relevance (not raw cosine) - - [ ] Confirm before "Clear all" + - [x] ★ relevance (not raw cosine) + - [x] 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 + - [x] Group into fieldsets (Connection, Text, Image, Embedding, Danger) + - [x] Connection test with model list + - [x] Model picker with autocomplete (`GET /api/tags`) + - [x] Provider presets (Ollama, LM Studio, OpenAI, Anthropic, Custom) + - [x] Reset to defaults - [ ] Export/import config - [ ] **Cross-cutting**: - - [ ] Keyboard shortcuts: ⌘K, 1–9, Space, ⌘,, ⌘S, Esc, ? - - [ ] Skeleton loaders for all async states - - [ ] Empty states for all tools - - [ ] `prefers-reduced-motion` respect on framer-motion - - [ ] Skip-to-content link - - [ ] Color + label for state (not color alone) + - [x] Keyboard shortcuts: ⌘K, 1–9, Space, ⌘,, ⌘S, Esc, ? + - [x] Skeleton loaders for all async states + - [x] Empty states for all tools + - [x] `prefers-reduced-motion` respect on framer-motion + - [x] Skip-to-content link + - [x] Color + label for state (not color alone) ### P3 — Nice to have @@ -1173,7 +1173,7 @@ These are blockers or bugs that make the app feel broken. 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: +small CSS. **All 10 below are now shipped.** 1. Fix the `WorldBuilder.tsx` syntax bug (5 min) 2. Auto-load Settings on mount (10 min) @@ -1181,11 +1181,11 @@ small CSS: (2 min) 4. Add `aria-label` to every icon-only button (30 min) 5. Add `data-tooltip` styled tooltips (1 hr) -6. Group the left rail into Session / World (30 min) -7. Replace the duplicate `Sparkles` icon (2 min) -8. Lighten `--color-text-dim` to meet WCAG AA (2 min) -9. Add the ⌘K command palette (3 hrs, the biggest of the bunch) -10. Self-host the fonts in `public/fonts/` (1 hr) +6. ✓ Group the left rail into Session / World (30 min) +7. ✓ Replace the duplicate `Sparkles` icon (2 min) +8. ✓ Lighten `--color-text-dim` to meet WCAG AA (2 min) +9. ✓ Add the ⌘K command palette (3 hrs, the biggest of the bunch) +10. ✓ 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. diff --git a/package-lock.json b/package-lock.json index d050460..9e26d13 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@tauri-apps/api": "^2", "@tauri-apps/plugin-log": "^2", + "@tauri-apps/plugin-store": "^2.4.3", "framer-motion": "^12", "lucide-react": "^0.525", "react": "^19.2.7", @@ -1306,6 +1307,15 @@ "@tauri-apps/api": "^2.8.0" } }, + "node_modules/@tauri-apps/plugin-store": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-store/-/plugin-store-2.4.3.tgz", + "integrity": "sha512-9LWPj9yMphRi9czEtUv87XHbl1b6xgd9EXpPrUnq6nG7+nbtoF84d4Kwz9xhAv/Hf30sr58pq7EOlyI936y8qw==", + "license": "MIT OR Apache-2.0", + "dependencies": { + "@tauri-apps/api": "^2.11.0" + } + }, "node_modules/@tybys/wasm-util": { "version": "0.10.3", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", diff --git a/package.json b/package.json index d034bf8..9cf32a2 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "dependencies": { "@tauri-apps/api": "^2", "@tauri-apps/plugin-log": "^2", + "@tauri-apps/plugin-store": "^2.4.3", "framer-motion": "^12", "lucide-react": "^0.525", "react": "^19.2.7", diff --git a/public/fonts/cinzel.woff2 b/public/fonts/cinzel.woff2 new file mode 100644 index 0000000..a54fd00 Binary files /dev/null and b/public/fonts/cinzel.woff2 differ diff --git a/public/fonts/inter.woff2 b/public/fonts/inter.woff2 new file mode 100644 index 0000000..d15208d Binary files /dev/null and b/public/fonts/inter.woff2 differ diff --git a/public/fonts/jetbrainsmono.woff2 b/public/fonts/jetbrainsmono.woff2 new file mode 100644 index 0000000..4d09cda Binary files /dev/null and b/public/fonts/jetbrainsmono.woff2 differ diff --git a/src-tauri/src/commands/llm_commands.rs b/src-tauri/src/commands/llm_commands.rs index df666e9..19376f4 100644 --- a/src-tauri/src/commands/llm_commands.rs +++ b/src-tauri/src/commands/llm_commands.rs @@ -81,6 +81,72 @@ pub fn set_llm_config(state: tauri::State<'_, AppState>, config: crate::llm::Llm Ok(()) } +// ─── Connection test: list models from the configured endpoint ── +// ponytail: Ollama exposes GET /api/tags, OpenAI-compatible exposes +// GET /v1/models. Returns the model names so Settings can show a list and +// a green/red pill. Best-effort — surface the error string on failure. +#[derive(serde::Serialize)] +pub struct ConnectionTest { + pub ok: bool, + pub models: Vec, + pub error: String, +} + +#[tauri::command] +pub async fn test_connection(state: tauri::State<'_, AppState>) -> Result { + let config = state.config.lock().map_err(|e| e.to_string())?.clone(); + let client = reqwest::Client::builder() + .timeout(std::time::Duration::from_secs(8)) + .build() + .map_err(|e| e.to_string())?; + + if llm::is_ollama(&config.api_url) { + let url = format!("{}/api/tags", config.api_url.trim_end_matches('/')); + let res = client.get(&url).send().await.map_err(|e| e.to_string())?; + if !res.status().is_success() { + return Ok(ConnectionTest { + ok: false, + models: vec![], + error: format!("HTTP {}", res.status()), + }); + } + let body: serde_json::Value = res.json().await.map_err(|e| e.to_string())?; + let models = body["models"] + .as_array() + .map(|a| { + a.iter() + .filter_map(|m| m["name"].as_str().map(|s| s.to_string())) + .collect() + }) + .unwrap_or_default(); + Ok(ConnectionTest { ok: true, models, error: String::new() }) + } else { + let url = format!("{}/v1/models", config.api_url.trim_end_matches('/')); + let mut req = client.get(&url); + if !config.api_key.is_empty() { + req = req.bearer_auth(&config.api_key); + } + let res = req.send().await.map_err(|e| e.to_string())?; + if !res.status().is_success() { + return Ok(ConnectionTest { + ok: false, + models: vec![], + error: format!("HTTP {}", res.status()), + }); + } + let body: serde_json::Value = res.json().await.map_err(|e| e.to_string())?; + let models = body["data"] + .as_array() + .map(|a| { + a.iter() + .filter_map(|m| m["id"].as_str().map(|s| s.to_string())) + .collect() + }) + .unwrap_or_default(); + Ok(ConnectionTest { ok: true, models, error: String::new() }) + } +} + // ─── Lore RAG injection (shared by generate + generate_stream) ─ /// Best-effort: if `rag_query` is set, retrieve top lore chunks and fold them diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index bd61f87..d27b88b 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -36,6 +36,7 @@ pub fn run() { commands::llm_commands::generate_stream, commands::llm_commands::get_llm_config, commands::llm_commands::set_llm_config, + commands::llm_commands::test_connection, commands::image_commands::generate_image, commands::rag_commands::rag_add, commands::rag_commands::rag_search, diff --git a/src/App.tsx b/src/App.tsx index 926fad8..749ef63 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -34,6 +34,7 @@ import { ImageGenerator } from "./components/ImageGenerator"; import { ToastContainer } from "./components/Toast"; import { CommandPalette } from "./components/CommandPalette"; import { ErrorBoundary } from "./components/ErrorBoundary"; +import { ConnectionPill } from "./components/ConnectionPill"; import { HistoryView } from "./components/HistoryView"; import type { Generation, GenerationKind } from "./lib/generations"; @@ -251,6 +252,13 @@ export default function App() { return (
+ {/* ponytail: skip-to-content for keyboard users — jumps past the rail. */} + + Skip to content + {/* Left Rail */}