Add note context visibility in chat and standardize UI design tokens

Improve chat context: build_context() now returns metadata about auto-found
notes, emitted as an SSE event so the frontend can display context pills
showing which notes influenced the response. Users can promote notes for
deeper context (+) or exclude irrelevant ones (x). A note picker lets users
manually attach notes. Multi-word search uses per-term AND matching, and
auto-search iterates keywords individually for broader OR-style coverage.

Standardize styling: introduce CSS design tokens (--radius-sm/md/lg/pill,
--color-success/warning/overlay, --focus-ring) and migrate all components
to use them. Fix header alignment to full-width, add active nav link state,
replace hardcoded colors with CSS variables, and normalize button padding.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-11 06:49:12 -05:00
parent 39bcd7a8fa
commit fb18d2c41d
26 changed files with 1070 additions and 237 deletions
+47 -13
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.5: Chat UX improvements, settings page, model management, entity rendering fix
2026-02-10 — Phase 4.7: Styling consistency pass — design tokens, header alignment, standardized UI
## Project Overview
Fabled Assistant is a self-hosted note-taking and task-tracking application with
@@ -50,6 +50,10 @@ for AI-assisted features.
`LIKE` prefix.
- **Dark-first theming:** CSS custom properties on `:root` (light) and
`[data-theme="dark"]`, with `prefers-color-scheme` detection defaulting to dark.
- **Design tokens:** Standardized CSS custom properties for border-radius
(`--radius-sm: 6px`, `--radius-md: 8px`, `--radius-lg: 12px`), semantic colors
(`--color-success`, `--color-warning`, `--color-overlay`), and focus ring
(`--focus-ring`). All components reference tokens instead of hardcoded values.
- **Unified note/task model:** A task is just a note with task attributes enabled.
A note has `status IS NOT NULL` ⟹ it's a task. "Convert to task" sets
`status='todo'`; "convert to note" clears `status`, `priority`, `due_date`.
@@ -62,8 +66,11 @@ for AI-assisted features.
need to POST the message body. Simpler than WebSockets; Quart supports async
generators natively for SSE.
- **Context building server-side:** Backend fetches URL content and searches notes —
frontend just sends the message text + optional note ID. Keyword extraction uses
simple word splitting with stopword filtering (no embeddings).
frontend just sends the message text + optional note ID + optional exclude list.
Keyword extraction uses simple word splitting with stopword filtering (no embeddings).
`build_context()` returns `(messages, context_meta)` tuple; metadata includes
auto-found note IDs/titles sent to frontend via SSE `context` event before streaming.
Multi-word search splits terms into per-word ILIKE with AND logic (not adjacent match).
- **No blocking long-running operations:** Any potentially slow operation (model
pulls, LLM calls, URL fetching, etc.) must never block app startup or freeze the
UI. Backend uses SSE streaming for incremental responses (chat messages and model
@@ -185,7 +192,7 @@ fabledassistant/
│ ├── 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, configurable assistant name)
│ │ ├── llm.py # Ollama interaction: ensure_model, stream_chat, generate_completion, fetch_url_content, build_context (keyword extraction, note search with 5 keywords + 2000-char previews, exclude_note_ids support, returns (messages, context_meta) tuple, 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/
@@ -200,7 +207,7 @@ fabledassistant/
│ ├── 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
│ │ └── theme.css # CSS custom properties: light/dark themes, body reset, design tokens (radius, semantic colors, focus ring, overlay), global focus-visible styles
│ ├── api/
│ │ └── client.ts # apiGet/apiPost/apiPut/apiPatch/apiDelete + apiStreamPost (SSE via fetch + ReadableStream)
│ ├── composables/
@@ -209,19 +216,19 @@ fabledassistant/
│ ├── stores/
│ │ ├── 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)
│ │ ├── chat.ts # Conversation CRUD, sendMessage (SSE streaming, handles context event, accepts excludeNoteIds), lastContextMeta ref, saveMessageAsNote, summarizeAsNote, fetchModels, status polling (ollamaStatus, modelStatus, chatReady)
│ │ ├── settings.ts # App settings: assistantName, defaultModel, fetchInstalledModels, pullModel (SSE streaming with onProgress callback), 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
│ │ ├── chat.ts # Message, Conversation, ConversationDetail, ContextMeta, 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() (with (?<!&) to skip HTML entities), linkifyWikilinks()
│ │ └── markdown.ts # renderMarkdown() (full) + renderPreview() (strips links/images) — decodes HTML entities before marked, replaces &#39; after sanitization
│ ├── views/
│ │ ├── ChatView.vue # Dedicated /chat page: sidebar + bubble-style messages (user right, assistant left) + floating dark input bar + auto-focus
│ │ ├── ChatView.vue # Dedicated /chat page: sidebar + bubble-style messages (user right, assistant left) + floating dark input bar + auto-focus + note picker + context pills (auto-found notes with promote/exclude) + exclude tracking
│ │ ├── 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
@@ -232,7 +239,7 @@ fabledassistant/
│ │ └── TaskViewerView.vue # Task detail: rendered markdown, badges, convert-to-note button, backlinks
│ ├── components/
│ │ ├── 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
│ │ ├── ChatPanel.vue # Slide-out chat panel (right side overlay, receives contextNoteId prop), bubble-style messages, floating dark input, note picker, context pills with promote/exclude
│ │ ├── 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
@@ -275,7 +282,7 @@ fabledassistant/
| GET | `/api/chat/conversations/:id` | Get conversation with all messages |
| DELETE | `/api/chat/conversations/:id` | Delete conversation (cascades to messages) |
| PATCH | `/api/chat/conversations/:id` | Update conversation title (body: `{title}`) |
| POST | `/api/chat/conversations/:id/messages` | Send message + stream LLM response via SSE (body: `{content, context_note_id?}`) |
| POST | `/api/chat/conversations/:id/messages` | Send message + stream LLM response via SSE (body: `{content, context_note_id?, exclude_note_ids?}`); emits `{context: ContextMeta}` SSE event before streaming chunks |
| POST | `/api/chat/messages/:id/save-as-note` | Save assistant message as a new note |
| 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}`) |
@@ -426,7 +433,7 @@ When adding a new migration, follow these conventions:
### Phase 4 — LLM Chat Integration ✓
- [x] **Ollama client:** async HTTP via httpx — `ensure_model()` (auto-pull on startup), `stream_chat()` (streaming NDJSON), `generate_completion()` (non-streaming)
- [x] **Context building:** System prompt with note-aware context — includes current note (via `context_note_id`), keyword-extracted related note search (top 3), URL content fetching (regex detection, HTML stripping, truncated to 4000 chars)
- [x] **Context building:** System prompt with note-aware context — includes current note (via `context_note_id`), keyword-extracted related note search (top 3, 5 keywords, 2000-char previews), URL content fetching (regex detection, HTML stripping, truncated to 4000 chars), returns `(messages, context_meta)` tuple with auto-found note IDs/titles, accepts `exclude_note_ids` to skip notes during auto-search
- [x] **Conversation persistence:** `conversations` + `messages` tables in PostgreSQL, full CRUD
- [x] **SSE streaming endpoint:** `POST /api/chat/conversations/:id/messages` streams LLM response chunks as SSE events, saves complete response to DB on finish
- [x] **Save as note:** Save any assistant message as a new note (first line as title)
@@ -451,6 +458,29 @@ When adding a new migration, follow these conventions:
- [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 4.6 — Improved Note Context in Chat ✓
- [x] **Multi-word search:** `list_notes()` splits multi-word queries into per-term ILIKE filters with AND logic (each word matched independently, not adjacent)
- [x] **Context metadata:** `build_context()` returns `(messages, context_meta)` tuple with auto-found note IDs/titles and attached note info
- [x] **Fuller auto-context:** Auto-found note previews increased from 300 to 2000 chars; uses all 5 extracted keywords instead of 3
- [x] **Context SSE event:** Backend emits `{context: ContextMeta}` SSE event before streaming LLM response chunks
- [x] **Exclude note IDs:** Backend accepts `exclude_note_ids` in request body, skips those notes during auto-search
- [x] **Context pills:** ChatView and ChatPanel show auto-found notes as pills above streaming response with note title as router-link
- [x] **Promote/exclude controls:** "+" button on context pills promotes note to attached context for next message; "x" button excludes note from future auto-searches in the session
- [x] **Note picker:** Paperclip button in chat input opens dropdown with debounced search, selecting a note attaches it as context (shown as pill above input)
- [x] **ContextMeta type:** New TypeScript interface for context metadata (`context_note_id`, `context_note_title`, `auto_notes`)
- [x] **Session-scoped excludes:** Excluded note IDs tracked per conversation session, cleared on conversation switch
### Phase 4.7 — Styling Consistency Pass ✓
- [x] **Design tokens:** Added `--radius-sm/md/lg/pill`, `--color-success/warning/overlay`, `--focus-ring` to theme.css
- [x] **Header alignment fix:** Removed `max-width: 960px` from header nav — now full-width with consistent padding, matching modern app layout
- [x] **Active nav link state:** Added `router-link-active` styling with primary color + card background
- [x] **Hardcoded colors replaced:** Status indicator dots, settings saved message, remove/confirm-delete buttons now use CSS variables
- [x] **Border-radius standardized:** All components migrated from mixed 3px/4px/6px/8px to `var(--radius-sm)` (6px) for controls, `var(--radius-md)` (8px) for cards/containers
- [x] **Button padding standardized:** All primary/CTA buttons use `0.45rem 1rem`; small buttons use `0.3rem 0.75rem`
- [x] **Settings max-width fixed:** Changed from 700px to 720px to match all other views
- [x] **Focus-visible states:** Global `focus-visible` rule with `--focus-ring` shadow on inputs, textareas, selects, buttons
- [x] **Modal overlays:** Replaced hardcoded `rgba(0,0,0,0.5)` with `var(--color-overlay)` across all components
### Phase 5 — Polish & Production Hardening
- [ ] Authentication (single-user or multi-user, TBD)
- [ ] Docker Swarm production stack (secrets, volumes, networking)
@@ -476,11 +506,15 @@ When adding a new migration, follow these conventions:
- Authentication model: single-user (password-only) vs multi-user?
## Current Status
**Phase:** Phase 4.5 complete. Chat UX improvements, settings page, model management.
**Phase:** Phase 4.7 complete. Styling consistency pass with design tokens and standardized UI.
- 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
- Note-aware context: auto-includes current note + searches related notes by keyword (5 keywords, 2000-char previews)
- Context visibility: auto-found notes shown as pills with title links, promote (+) and exclude (x) controls
- Note picker: attach any note as context from chat input via search dropdown
- Exclude tracking: session-scoped per conversation, excluded notes skipped in future auto-searches
- Multi-word search: splits query into per-term ILIKE with AND logic (words match independently)
- 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 bubble-style messages (user right, assistant left), floating dark input bar