Update summary.md for Phase 19
Documents changes from the Phase 19 session: - Light mode indigo theme: #6366f1 primary, #f5f5fb/#ededf5 off-white backgrounds - OLLAMA_NUM_CTX configurable env var (default 8192, replaces hardcoded 32768) - OLLAMA_MODEL default → qwen3:latest, OLLAMA_INTENT_MODEL default → qwen2.5:1.5b - GET /api/settings/models endpoint: installed models + configured defaults - Chat/intent model dropdowns in Settings UI (populated from installed models) - Optimistic streaming in generation_task.py: races intent classification against stream start via asyncio.Queue + asyncio.wait; user sees first token immediately for pure-chat messages, tool path unchanged when intent wins the race Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+28
-19
@@ -12,7 +12,7 @@
|
||||
> Include file-level details in the commit body when the change is non-trivial.
|
||||
|
||||
## Last Updated
|
||||
2026-02-25 — Phase 18b: Favicon contrast fix + email template redesign
|
||||
2026-02-26 — Phase 19: Light mode indigo theme, OLLAMA_NUM_CTX, model dropdowns, optimistic streaming
|
||||
|
||||
## Project Overview
|
||||
Fabled Assistant is a self-hosted note-taking and task-tracking application with
|
||||
@@ -255,7 +255,7 @@ fabledassistant/
|
||||
│ ├── __init__.py
|
||||
│ ├── app.py # Quart app factory: SPA via 404 handler, JSON 404/500 for API, request logging, security headers (after_request)
|
||||
│ ├── auth.py # Auth decorators: login_required, admin_required, get_current_user_id — shared _check_auth() helper
|
||||
│ ├── config.py # Config from env vars + Docker secrets file support (_read_secret) + SECURE_COOKIES + TRUST_PROXY_HEADERS + OIDC_ISSUER/CLIENT_ID/CLIENT_SECRET/SCOPES + LOCAL_AUTH_ENABLED + oidc_enabled() classmethod
|
||||
│ ├── config.py # Config from env vars + Docker secrets file support (_read_secret) + SECURE_COOKIES + TRUST_PROXY_HEADERS + OLLAMA_NUM_CTX (KV cache window, default 8192) + OIDC_ISSUER/CLIENT_ID/CLIENT_SECRET/SCOPES + LOCAL_AUTH_ENABLED + oidc_enabled() classmethod
|
||||
│ ├── rate_limit.py # In-memory sliding-window rate limiter (asyncio.Lock + defaultdict); is_rate_limited(key, max, window)
|
||||
│ ├── models/
|
||||
│ │ ├── __init__.py # async_session factory, Base, imports all models
|
||||
@@ -273,16 +273,16 @@ fabledassistant/
|
||||
│ │ ├── chat.py # /api/chat blueprint: conversations CRUD, SSE message streaming (all @login_required)
|
||||
│ │ ├── notes.py # /api/notes CRUD + wikilinks + backlinks + tag suggestions (all @login_required)
|
||||
│ │ ├── tasks.py # /api/tasks CRUD + PATCH status (all @login_required)
|
||||
│ │ └── settings.py # /api/settings GET/PUT — per-user settings (@login_required)
|
||||
│ │ └── settings.py # /api/settings GET/PUT, GET /models — per-user settings + installed model list (@login_required)
|
||||
│ ├── services/
|
||||
│ │ ├── auth.py # Auth business logic: hash_password, verify_password, create_user (password optional, oauth_sub kwarg), authenticate (returns None for OAuth-only users), get_user_by_id/username/email/oauth_sub, update_user_email, link_oauth_sub, is_registration_open, list_users, delete_user, password reset tokens, invitation tokens
|
||||
│ │ ├── oauth.py # OIDC/OAuth2 service: get_oidc_config (discovery, cached), build_auth_url (PKCE), exchange_code, get_userinfo, find_or_create_oauth_user (sub lookup → email auto-link → create)
|
||||
│ │ ├── backup.py # Backup/restore: export_full_backup, export_user_backup, restore_full_backup
|
||||
│ │ ├── notes.py # CRUD with user_id isolation, is_task filter, convert, backlinks, search_notes_for_context
|
||||
│ │ ├── llm.py # Ollama interaction: build_context with user_id, streaming (stream_chat + stream_chat_with_tools), ChatChunk dataclass, URL fetching
|
||||
│ │ ├── llm.py # Ollama interaction: build_context with user_id, streaming (stream_chat + stream_chat_with_tools), ChatChunk dataclass, URL fetching; uses Config.OLLAMA_NUM_CTX for KV cache window
|
||||
│ │ ├── chat.py # Conversation CRUD with user_id isolation, add_message, save/summarize as note (LLM-titled, chat-tagged)
|
||||
│ │ ├── generation_buffer.py # In-memory SSE event buffer with cancel_event, reconnect support, auto-cleanup; supports chat (int keys) and assist (string keys)
|
||||
│ │ ├── generation_task.py # Background asyncio tasks: run_generation (chat, DB flush, titles, intent routing + tool loop) + run_assist_generation (lightweight, no DB)
|
||||
│ │ ├── generation_task.py # Background asyncio tasks: run_generation (chat, DB flush, titles, optimistic streaming + intent racing + tool loop) + run_assist_generation (lightweight, no DB); _drain_queue() async generator helper
|
||||
│ │ ├── intent.py # Intent routing: classify_intent() makes fast non-streaming LLM call to detect tool intent before streaming
|
||||
│ │ ├── tools.py # LLM tool definitions (create/delete note+task, update_note w/tag management, get_note, list_notes, search_notes w/type filter, list_tasks, full CalDAV suite incl. search_todos) + execute_tool dispatcher
|
||||
│ │ ├── tag_suggestions.py # LLM-powered tag suggestions: suggest_tags() builds prompt with existing tags, calls generate_completion, parses JSON response
|
||||
@@ -303,7 +303,7 @@ fabledassistant/
|
||||
│ ├── App.vue # Shell: .app-shell (100dvh flex column) with AppHeader + .app-content (flex:1, scrollable) wrapping router-view; 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, design tokens, responsive breakpoints (480/768/1024), utility classes (.hide-mobile/.hide-desktop), mobile touch targets
|
||||
│ │ ├── theme.css # CSS custom properties: light/dark themes, body reset, design tokens, responsive breakpoints (480/768/1024), utility classes (.hide-mobile/.hide-desktop), mobile touch targets; light mode uses indigo brand color (#6366f1) + indigo-tinted off-white backgrounds (#f5f5fb/#ededf5)
|
||||
│ │ └── editor-shared.css # Shared editor styles for NoteEditorView + TaskEditorView: layout, toolbar, tag suggestions, assist panel, diff view, modal, responsive, .inline-assist-btn (global — required for teleported elements)
|
||||
│ ├── api/
|
||||
│ │ └── client.ts # ApiError class, apiGet/apiPost/apiPut/apiPatch/apiDelete + apiSSEStream (fetch+ReadableStream), apiStreamPost (legacy), auto 401→login redirect
|
||||
@@ -317,7 +317,7 @@ fabledassistant/
|
||||
│ │ ├── notes.ts # CRUD + tag filter, resolveTitle, convertToTask, convertToNote, fetchBacklinks, fetchAllTags (with toast errors)
|
||||
│ │ ├── tasks.ts # CRUD + status/priority filter, patchStatus (with toast errors)
|
||||
│ │ ├── chat.ts # Conversation CRUD, sendMessage (SSE streaming), status polling (memory-leak-safe _pollUntilLoaded), running models, model warming, updateConversationModel (with toast errors)
|
||||
│ │ ├── settings.ts # App settings: assistantName, defaultModel, pullModel, deleteModel (with toast errors)
|
||||
│ │ ├── settings.ts # App settings: assistantName, defaultModel, installedModels, defaultChatModel, defaultIntentModel, pullModel, deleteModel (with toast errors)
|
||||
│ │ └── toast.ts # Toast notification state (success/error/warning), 4s auto-dismiss, dismiss(id)
|
||||
│ ├── types/
|
||||
│ │ ├── auth.ts # User interface (incl. has_password bool), AuthStatus interface (incl. oauth_enabled, local_auth_enabled)
|
||||
@@ -343,7 +343,7 @@ fabledassistant/
|
||||
│ │ ├── UserManagementView.vue # Admin user management: registration toggle, invitations (send/revoke), user list with delete
|
||||
│ │ ├── ChatView.vue # Dedicated /chat page: responsive sidebar (overlay on mobile), bubble messages, note picker, persistent context sidebar (right panel, hidden mobile), model selector in header
|
||||
│ │ ├── HomeView.vue # Chat-first dashboard: quick actions + chat widget (top, full-width), inline response panel, two-column grid (3fr tasks / 2fr notes); task sections: Overdue, Due Today, Due This Week, High Priority, In Progress, Other (capped 10, due-dated first); 8 recent notes; model warming on mount
|
||||
│ │ ├── SettingsView.vue # Settings page: assistant name, email change (with password confirmation for local-auth users), change password, notifications, CalDAV, SMTP (admin), base URL (admin), data export/restore (admin)
|
||||
│ │ ├── SettingsView.vue # Settings page: assistant name, chat/intent model dropdowns (populated from installed models), email change (with password confirmation for local-auth users), change password, notifications, CalDAV, SMTP (admin), base URL (admin), data export/restore (admin)
|
||||
│ │ ├── NotesListView.vue # Note list: search, sort, tag filter pills, pagination
|
||||
│ │ ├── NoteEditorView.vue # Create/edit: Tiptap editor, sticky toolbar, AI Assist panel (right-side 320px, collapsible), line-level diff view, Proofread action, floating inline ✨ pill on text selection, LLM tag suggestions, Ctrl+S, auto-save (5min), unsaved guard; styles from editor-shared.css
|
||||
│ │ ├── NoteViewerView.vue # Markdown render, wikilink auto-create, convert-to-task, backlinks, table of contents sidebar
|
||||
@@ -446,6 +446,7 @@ fabledassistant/
|
||||
| POST | `/api/admin/smtp/test` | Send test email (admin only, body: `{recipient}`) |
|
||||
| GET | `/api/settings` | Get all app settings as `{key: value}` dict |
|
||||
| PUT | `/api/settings` | Update settings (body: `{key: value, ...}`) |
|
||||
| GET | `/api/settings/models` | Get installed Ollama models + configured defaults (`{models, default_chat_model, default_intent_model}`) |
|
||||
|
||||
## Alembic Migrations
|
||||
|
||||
@@ -558,7 +559,7 @@ When adding a new migration, follow these conventions:
|
||||
- **Keyboard shortcuts overlay:** Press `?` or click `?` in nav bar. State shared via `useShortcuts` composable.
|
||||
|
||||
### LLM Chat
|
||||
- Ollama integration via async HTTP (httpx), default model qwen3 (better tool support than mistral), auto-pull on startup
|
||||
- Ollama integration via async HTTP (httpx), default model `qwen3:latest` (better tool support than mistral), auto-pull on startup
|
||||
- Background generation with `GenerationBuffer` (in-memory SSE fan-out, `Last-Event-ID` reconnect, 60s cleanup)
|
||||
- Stop generation with partial content preservation
|
||||
- Note-aware context building: current note + keyword search for related notes + URL fetching
|
||||
@@ -611,21 +612,29 @@ When adding a new migration, follow these conventions:
|
||||
(`search_todos` keyword-filters the todo list — companion to `list_todos`)
|
||||
- **Streaming status transparency:** The backend emits `status` SSE events at each pipeline stage
|
||||
so the user always sees what's happening instead of a blank progress dot. Stages:
|
||||
(1) `"Analyzing your request..."` before intent classification; (2) human-readable tool label
|
||||
(e.g. `"Creating calendar event..."`) before each tool executes (both intent-routed and native);
|
||||
(3) `"Generating response..."` / `"Composing response..."` before each LLM streaming round.
|
||||
(1) `"Generating response..."` immediately after context is built (no blocking wait for intent);
|
||||
(2) human-readable tool label (e.g. `"Creating calendar event..."`) before each tool executes
|
||||
(both intent-routed and native); (3) `"Composing response..."` before tool follow-up rounds.
|
||||
Frontend: `chat.ts` stores `streamingStatus` ref, cleared on first content chunk or on done/error.
|
||||
`ChatView.vue` shows a pulsing dot + italic label above the content while status is active, then
|
||||
falls back to the blinking cursor when content streams in. `HomeView.vue` dashboard panel shows
|
||||
the status label in place of `...` before any content arrives.
|
||||
- **Intent routing:** Before streaming, a fast non-streaming LLM call classifies user intent and
|
||||
extracts tool parameters (`services/intent.py`). If a tool call is detected, it executes directly
|
||||
— bypassing the model's native (sometimes unreliable) tool calling API. Falls through to normal
|
||||
streaming when no tool is detected or classification fails. Only runs on first round of tool loop.
|
||||
- **Intent routing (optimistic streaming):** On the first round, the backend races intent
|
||||
classification against the start of the LLM stream using `asyncio.wait(FIRST_COMPLETED)`.
|
||||
The LLM stream is immediately started into an `asyncio.Queue` while `classify_intent()` runs
|
||||
concurrently. If the stream produces its first token before classification completes, the intent
|
||||
task is cancelled and the user sees tokens immediately (zero blocking for pure chat). If
|
||||
classification finishes first and detects a tool call, the stream is cancelled and the tool
|
||||
executes directly — bypassing the model's native (sometimes unreliable) tool calling API. Falls
|
||||
through to normal streaming when no tool is detected or classification fails.
|
||||
Dedicated intent model configurable via `OLLAMA_INTENT_MODEL` env var (default `qwen2.5:1.5b`)
|
||||
or per-user `intent_model` setting — smaller/faster model for routing. Main model default
|
||||
is `qwen3:latest` (configurable via `OLLAMA_MODEL` env var or per-user `default_model` setting
|
||||
— both settable as dropdowns in the Settings UI populated from installed models).
|
||||
`OLLAMA_NUM_CTX` env var (default 8192) controls the KV cache context window for all generation
|
||||
calls; reducing from 32768 cuts VRAM usage ~4x without affecting most conversations.
|
||||
Supports confidence levels ("high"/"medium"/"low") — low-confidence intents fall through to streaming.
|
||||
Passes last 6 user/assistant turns as history for anaphora resolution ("move it", "cancel that").
|
||||
Dedicated intent model configurable via `OLLAMA_INTENT_MODEL` env var or per-user `intent_model`
|
||||
setting — allows a smaller/faster model for routing while the main model handles responses.
|
||||
Intent router rules cover: update/delete events, CalDAV todos, time-period → list_events (not
|
||||
search_events), update_note vs create_note disambiguation, reminder_minutes conversion,
|
||||
delete_note vs delete_task disambiguation, get_note for "read/show me this note", list_notes for
|
||||
@@ -684,7 +693,7 @@ When adding a new migration, follow these conventions:
|
||||
- Configurable base URL for email links (admin setting, env var fallback)
|
||||
|
||||
### UI & Theming
|
||||
- Dark/light theme with CSS custom properties, design tokens, `prefers-color-scheme` detection
|
||||
- Dark/light theme with CSS custom properties, design tokens, `prefers-color-scheme` detection; light mode uses indigo brand color (`--color-primary: #6366f1`) and indigo-tinted off-white backgrounds (`--color-bg: #f5f5fb`, `--color-bg-secondary: #ededf5`) with white cards to pop content
|
||||
- Responsive design: breakpoints (480/768/1024), hamburger menu, mobile touch targets (44px), sidebar overlays
|
||||
- App shell: navbar always visible, 100dvh flex layout, all views fit within viewport
|
||||
- Toast notifications (success/error/warning, 4s auto-dismiss)
|
||||
|
||||
Reference in New Issue
Block a user