Add settings page, model management, and chat UX improvements

- Settings infrastructure: key-value settings table, GET/PUT API, Pinia store
- Configurable assistant name (default "Fable") in settings and LLM system prompt
- Model catalog with 18 models in 3 categories (General Purpose, Coding,
  Uncensored / Creative Writing) with download/select/remove functionality
- Move Ollama status indicator from chat views to global nav bar
- Chat bubble layout: user messages right-aligned, assistant left-aligned
- Floating dark input bar with auto-focus and circular send button
- Fix HTML entity rendering (' apostrophe issue in marked/DOMPurify pipeline)
- Fix new chat button navigation (fetchConversation before router.push)
- Recent chats section on home page with "New Chat" button
- Update summary.md with Phase 4.5 changes

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-10 21:32:02 -05:00
parent 834fd80640
commit 38b1ac933e
20 changed files with 1257 additions and 236 deletions
+54 -21
View File
@@ -12,7 +12,7 @@
> Include file-level details in the commit body when the change is non-trivial.
## Last Updated
2026-02-10 — Phase 4 + Ollama health check (status indicator on chat views showing Ollama/model readiness)
2026-02-10 — Phase 4.5: Chat UX improvements, settings page, model management, entity rendering fix
## Project Overview
Fabled Assistant is a self-hosted note-taking and task-tracking application with
@@ -128,6 +128,11 @@ for AI-assisted features.
- "Convert to note" = `update_note(id, status=None, priority=None, due_date=None)`
- Task body field is `body` (not `description`) — standardized with notes
### Settings
- `key` (text PK), `value` (text)
- Key-value store for app-wide settings (assistant name, default model, etc.)
- CRUD via `services/settings.py`: `get_setting(key, default)`, `set_setting(key, value)`, `get_all_settings()`
### Conversations
- `id` (int PK), `title` (str), `model` (str), `created_at`, `updated_at`
- Has many messages (cascade delete)
@@ -156,27 +161,31 @@ fabledassistant/
│ ├── 0002_create_tasks_table.py # Tasks table + enums (raw SQL, idempotent)
│ ├── 0003_task_note_companion.py # Data migration: create companion notes for existing tasks
│ ├── 0004_merge_tasks_into_notes.py # Add task columns to notes, migrate data, drop tasks table
── 0005_add_chat_tables.py # Conversations + messages tables with FKs and indexes
── 0005_add_chat_tables.py # Conversations + messages tables with FKs and indexes
│ └── 0006_add_settings_table.py # Settings key-value table
├── src/
│ └── fabledassistant/
│ ├── __init__.py
│ ├── app.py # Quart app factory: SPA via 404 handler, JSON 404/500 for API
│ ├── config.py # Config from env vars
│ ├── models/
│ │ ├── __init__.py # async_session factory, Base, imports Note + TaskStatus + TaskPriority + Conversation + Message
│ │ ├── __init__.py # async_session factory, Base, imports Note + TaskStatus + TaskPriority + Conversation + Message + Setting
│ │ ├── note.py # Note model (unified: id, title, body, tags[], parent_id, status, priority, due_date, timestamps) + TaskStatus/TaskPriority enums + is_task property
│ │ ── conversation.py # Conversation + Message models with relationships and to_dict()
│ │ ── conversation.py # Conversation + Message models with relationships and to_dict()
│ │ └── setting.py # Setting model (key TEXT PK, value TEXT)
│ ├── routes/
│ │ ├── __init__.py
│ │ ├── api.py # /api blueprint with /health endpoint
│ │ ├── chat.py # /api/chat blueprint: conversations CRUD, SSE message streaming, save/summarize as note, models list, status check
│ │ ├── chat.py # /api/chat blueprint: conversations CRUD, SSE message streaming, save/summarize as note, models list/pull/delete, status check
│ │ ├── notes.py # /api/notes CRUD + /by-title + /resolve-title + /convert-to-task + /convert-to-note + /backlinks
│ │ ── tasks.py # /api/tasks CRUD + PATCH status (thin wrappers, accepts body not description)
│ │ ── tasks.py # /api/tasks CRUD + PATCH status (thin wrappers, accepts body not description)
│ │ └── settings.py # /api/settings GET/PUT — app-wide settings
│ ├── services/
│ │ ├── notes.py # CRUD, is_task filter, status/priority filters, convert_note_to_task, convert_task_to_note, get_backlinks
│ │ ├── tasks.py # Thin wrappers around notes.py (create_task, list_tasks with is_task=True, etc.)
│ │ ├── llm.py # Ollama interaction: ensure_model, stream_chat, generate_completion, fetch_url_content, build_context (keyword extraction, note search, URL fetching)
│ │ ── chat.py # Conversation CRUD, add_message, save_response_as_note, summarize_conversation_as_note
│ │ ├── llm.py # Ollama interaction: ensure_model, stream_chat, generate_completion, fetch_url_content, build_context (keyword extraction, note search, URL fetching, configurable assistant name)
│ │ ── chat.py # Conversation CRUD, add_message, save_response_as_note, summarize_conversation_as_note
│ │ └── settings.py # Settings CRUD: get_setting, set_setting, get_all_settings
│ ├── utils/
│ │ ├── __init__.py
│ │ └── tags.py # extract_tags() — regex #tag extraction, skips code fences
@@ -186,7 +195,7 @@ fabledassistant/
├── vite.config.ts
├── tsconfig.json
├── src/
│ ├── App.vue # Shell: AppHeader + router-view + ChatPanel (slide-out) + ToastNotification
│ ├── App.vue # Shell: AppHeader + router-view + ChatPanel (slide-out) + ToastNotification; starts status polling + loads settings on mount
│ ├── main.ts # App init, imports theme.css
│ ├── assets/
│ │ └── theme.css # CSS custom properties: light/dark themes, body reset
@@ -199,17 +208,20 @@ fabledassistant/
│ │ ├── notes.ts # CRUD + tag filter, resolveTitle, convertToTask, convertToNote, fetchBacklinks, fetchAllTags
│ │ ├── tasks.ts # CRUD + status/priority filter, patchStatus (uses body not description)
│ │ ├── chat.ts # Conversation CRUD, sendMessage (SSE streaming), saveMessageAsNote, summarizeAsNote, fetchModels, status polling (ollamaStatus, modelStatus, chatReady)
│ │ ├── settings.ts # App settings: assistantName, defaultModel, fetchInstalledModels, pullModel, deleteModel
│ │ └── toast.ts # Toast notification state, 3s auto-dismiss
│ ├── types/
│ │ ├── note.ts # Note interface (with status, priority, due_date, is_task) + TaskStatus, TaskPriority types + NoteListResponse
│ │ ├── chat.ts # Message, Conversation, ConversationDetail, OllamaModel, OllamaStatus interfaces
│ │ ├── settings.ts # AppSettings interface, ModelInfo interface (name, description, size, bestFor, category)
│ │ └── task.ts # Task = re-export of Note; TaskListResponse
│ ├── utils/
│ │ ├── tags.ts # extractTags(), linkifyTags(), linkifyWikilinks()
│ │ └── markdown.ts # renderMarkdown() (full), renderPreview() (strips links/images for cards)
│ │ └── markdown.ts # renderMarkdown() (full) + renderPreview() (strips links/images) — decodes HTML entities before marked, replaces &#39; after sanitization
│ ├── views/
│ │ ├── ChatView.vue # Dedicated /chat page: sidebar (conversation list) + message thread + input + summarize + Ollama status indicator
│ │ ├── HomeView.vue # Landing page: recent notes + tasks (independent error handling)
│ │ ├── ChatView.vue # Dedicated /chat page: sidebar + bubble-style messages (user right, assistant left) + floating dark input bar + auto-focus
│ │ ├── HomeView.vue # Landing page: recent notes + tasks + recent chats (3 most recent) + new chat button
│ │ ├── SettingsView.vue # Settings page: assistant name config + model catalog (18 models in 3 categories) with download/select/remove
│ │ ├── NotesListView.vue # Note list: search, sort, tag filter pills, pagination
│ │ ├── NoteEditorView.vue # Create/edit: Ctrl+S, unsaved guard, toasts, autocomplete
│ │ ├── NoteViewerView.vue # Markdown render, wikilink auto-create, convert-to-task (only when !is_task), backlinks
@@ -217,9 +229,9 @@ fabledassistant/
│ │ ├── TaskEditorView.vue # Create/edit task: fields (body not description), Ctrl+S, dirty guard, autocomplete
│ │ └── TaskViewerView.vue # Task detail: rendered markdown, badges, convert-to-note button, backlinks
│ ├── components/
│ │ ├── AppHeader.vue # Nav bar: brand, Notes + Tasks + Chat links, chat panel toggle, theme toggle
│ │ ├── ChatPanel.vue # Slide-out chat panel (right side overlay, receives contextNoteId prop) + Ollama status indicator
│ │ ├── ChatMessage.vue # Message bubble: markdown rendering, role label, "Save as Note" action on assistant messages
│ │ ├── AppHeader.vue # Nav bar: brand, Notes + Tasks + Chat + Settings links, status indicator dot, chat panel toggle, theme toggle
│ │ ├── ChatPanel.vue # Slide-out chat panel (right side overlay, receives contextNoteId prop), bubble-style messages, floating dark input
│ │ ├── ChatMessage.vue # Message bubble: markdown rendering, configurable assistant name label, "Save as Note" action on assistant messages
│ │ ├── NoteCard.vue # Card with rendered markdown preview (v-html), TagPill, tag-click emit
│ │ ├── TaskCard.vue # Card with rendered preview (body not description), StatusBadge (clickable), PriorityBadge, due date, tags
│ │ ├── StatusBadge.vue # Color-coded status badge, optional clickable cycling
@@ -230,7 +242,7 @@ fabledassistant/
│ │ ├── PaginationBar.vue # Prev/next + page numbers
│ │ └── ToastNotification.vue # Fixed-position toast container
│ └── router/
│ └── index.ts # Routes: /, /notes/*, /tasks/*, /chat, /chat/:id
│ └── index.ts # Routes: /, /notes/*, /tasks/*, /chat, /chat/:id, /settings
└── public/
```
@@ -266,6 +278,10 @@ fabledassistant/
| POST | `/api/chat/conversations/:id/summarize` | Summarize conversation via LLM, save as note |
| GET | `/api/chat/status` | Check Ollama availability and model readiness (`{ollama, model, default_model}`) |
| GET | `/api/chat/models` | List available Ollama models |
| POST | `/api/chat/models/pull` | Pull/download a model from Ollama (background, body: `{model}`) |
| POST | `/api/chat/models/delete` | Delete a model from Ollama (body: `{model}`) |
| GET | `/api/settings` | Get all app settings as `{key: value}` dict |
| PUT | `/api/settings` | Update settings (body: `{key: value, ...}`) |
## Alembic Migrations
@@ -276,7 +292,7 @@ container startup.
### Migration Chain
```
0001_create_notes_table.py → 0002_create_tasks_table.py → 0003_task_note_companion.py → 0004_merge_tasks_into_notes.py → 0005_add_chat_tables.py
0001_create_notes_table.py → 0002_create_tasks_table.py → 0003_task_note_companion.py → 0004_merge_tasks_into_notes.py → 0005_add_chat_tables.py → 0006_add_settings_table.py
```
### How Migrations Run
@@ -419,6 +435,20 @@ When adding a new migration, follow these conventions:
- [x] **Frontend SSE client:** `apiStreamPost()` uses fetch + ReadableStream to parse SSE data lines
- [x] **Auto-title:** Conversation title auto-set from first user message content
### Phase 4.5 — Chat UX + Settings + Model Management ✓
- [x] **Settings infrastructure:** Key-value `settings` table, `GET/PUT /api/settings`, Pinia store with defaults
- [x] **Configurable assistant name:** Default "Fable", editable in settings, injected into LLM system prompt and chat message labels
- [x] **Settings page:** `/settings` route with assistant name form + model catalog
- [x] **Model catalog:** 18 models across 3 categories (General Purpose, Coding, Uncensored / Creative Writing) with descriptions, sizes, and best-for labels
- [x] **Model management:** Download (pull), select (set as default), and remove (delete from Ollama) with confirmation UI
- [x] **Status indicator in nav bar:** Moved from per-component (ChatView/ChatPanel) to global AppHeader; green/yellow/red dot with label
- [x] **Chat bubble layout:** User messages right-aligned (primary color bg), assistant messages left-aligned (card bg), speech bubble tails
- [x] **Floating dark input bar:** `#1c1c1e` background, rounded corners, circular send button with arrow
- [x] **Auto-focus input:** Chat input auto-focuses on mount, conversation switch, and after sending
- [x] **HTML entity fix:** Three-layer approach — decode entities before marked, DOMPurify sanitization, explicit `&#39;` → `'` replacement after sanitization
- [x] **New chat navigation fix:** `fetchConversation()` called before `router.push()` so `currentConversation` is set immediately
- [x] **Recent chats on home page:** 3 most recent conversations shown as clickable cards + "New Chat" button
### Phase 5 — Polish & Production Hardening
- [ ] Authentication (single-user or multi-user, TBD)
- [ ] Docker Swarm production stack (secrets, volumes, networking)
@@ -444,16 +474,19 @@ When adding a new migration, follow these conventions:
- Authentication model: single-user (password-only) vs multi-user?
## Current Status
**Phase:** Phase 4 complete. LLM chat integration with streaming responses.
**Phase:** Phase 4.5 complete. Chat UX improvements, settings page, model management.
- Full note-taking and task-tracking CRUD with markdown, wikilinks, backlinks, tags
- Unified note/task model (a task is a note with `status IS NOT NULL`)
- LLM chat via Ollama with SSE streaming responses
- Note-aware context: auto-includes current note + searches related notes by keyword
- URL content fetching: detects URLs in messages, fetches and includes content
- Save assistant messages as notes; summarize entire conversations as notes
- Dedicated `/chat` page with conversation sidebar + message thread
- Dedicated `/chat` page with bubble-style messages (user right, assistant left), floating dark input bar
- Slide-out chat panel accessible from any page, auto-includes note/task context
- Auto-pull default model (`llama3.1`) on startup
- Ollama health check: status indicator on chat views (green/yellow/red dot) with 30s polling; disables send when not ready
- Settings page: configurable assistant name (default "Fable"), model catalog with 18 models in 3 categories
- Model management: download, select, and remove models from the settings page
- Ollama status indicator in global nav bar (green/yellow/red dot) with 30s polling
- Recent chats section on home page with quick "New Chat" button
- HTML entity rendering fix (apostrophes in marked → DOMPurify pipeline)
- Dark/light theme with CSS custom properties
- Ready for Phase 5: Polish & Production Hardening