Add multi-user auth, background generation, and chat UX improvements
Phase 5: Multi-user authentication with session cookies, bcrypt passwords, first-user-is-admin pattern, per-user data isolation, backup/restore, Docker Swarm production stack with secrets and network isolation. Phase 5.1: Chat UX improvements: - Background generation architecture (GenerationBuffer + asyncio task) with SSE fan-out, reconnect support, and periodic DB flushes - LLM-generated conversation titles (first exchange + every 10th message) - Stop generation button with cancel_event and partial content preservation - Relative timestamps in sidebar (5m ago, 3h ago, then dates) - Empty chat auto-cleanup on navigation away - Save-as-note uses LLM for title generation, tags notes with "chat" - Summarize-as-note also tags with "chat" Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+133
-79
@@ -12,7 +12,7 @@
|
||||
> Include file-level details in the commit body when the change is non-trivial.
|
||||
|
||||
## Last Updated
|
||||
2026-02-11 — Phase 4.8: Backend efficiency & consistency pass
|
||||
2026-02-11 — Phase 5.1: Chat UX Improvements
|
||||
|
||||
## Project Overview
|
||||
Fabled Assistant is a self-hosted note-taking and task-tracking application with
|
||||
@@ -61,10 +61,15 @@ for AI-assisted features.
|
||||
- **Wikilinks:** Obsidian-style `[[Title]]` and `[[Title|Display Text]]` in markdown
|
||||
bodies. Clicking a wikilink uses `POST /api/notes/resolve-title` to auto-create
|
||||
missing notes.
|
||||
- **SSE over WebSockets for LLM streaming:** Uses `POST` with `text/event-stream`
|
||||
response (not EventSource). Frontend uses `fetch()` + `ReadableStream` since we
|
||||
need to POST the message body. Simpler than WebSockets; Quart supports async
|
||||
generators natively for SSE.
|
||||
- **Background generation architecture:** LLM streaming runs in a detached
|
||||
`asyncio.Task` that writes into an in-memory `GenerationBuffer`. SSE clients
|
||||
tail the buffer and can reconnect mid-stream without data loss. Buffer has
|
||||
`cancel_event` for user-initiated stop. Completed buffers are cleaned up after
|
||||
60s grace period. Periodic DB flushes every 5s preserve partial content.
|
||||
- **SSE over WebSockets for LLM streaming:** SSE clients connect via
|
||||
`GET /api/chat/conversations/:id/generation/stream` with `Last-Event-ID`
|
||||
reconnection support. Frontend uses `fetch()` + `ReadableStream`. 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 + optional exclude list.
|
||||
Keyword extraction uses simple word splitting with stopword filtering (no embeddings).
|
||||
@@ -115,9 +120,18 @@ for AI-assisted features.
|
||||
|
||||
## Data Model
|
||||
|
||||
### Users
|
||||
- `id` (int PK), `username` (text UNIQUE NOT NULL), `email` (text nullable),
|
||||
`password_hash` (text NOT NULL), `role` (text NOT NULL DEFAULT 'user'),
|
||||
`created_at` (timestamptz)
|
||||
- First registered user auto-assigned `role='admin'`; claims orphaned data
|
||||
- Passwords hashed with bcrypt
|
||||
- `to_dict()` excludes `password_hash`
|
||||
|
||||
### Notes (unified — includes tasks)
|
||||
- `id` (int PK), `title` (str), `body` (markdown str), `tags` (ARRAY[str]),
|
||||
`parent_id` (nullable FK to self), `status` (nullable str — `todo`/`in_progress`/`done`),
|
||||
`parent_id` (nullable FK to self), `user_id` (nullable FK to users, CASCADE),
|
||||
`status` (nullable str — `todo`/`in_progress`/`done`),
|
||||
`priority` (nullable str — `none`/`low`/`medium`/`high`), `due_date` (nullable date),
|
||||
`created_at`, `updated_at`
|
||||
- **A note is a task when `status IS NOT NULL`** — the `is_task` property checks this
|
||||
@@ -138,24 +152,26 @@ for AI-assisted features.
|
||||
- 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)`, `set_settings_batch(dict)`, `get_all_settings()`
|
||||
- Composite PK: `(user_id, key)` — `user_id` FK to users (CASCADE), `key` (text)
|
||||
- `value` (text)
|
||||
- Per-user key-value store for settings (assistant name, default model, etc.)
|
||||
- CRUD via `services/settings.py`: all functions take `user_id` as first parameter
|
||||
|
||||
### Conversations
|
||||
- `id` (int PK), `title` (str), `model` (str), `created_at`, `updated_at`
|
||||
- `id` (int PK), `title` (str), `model` (str), `user_id` (nullable FK to users, CASCADE), `created_at`, `updated_at`
|
||||
- Has many messages (cascade delete)
|
||||
- Title auto-generated from first user message if not set
|
||||
- Title auto-generated by LLM on first exchange and re-generated every 10th message
|
||||
- Index on `updated_at` for list ordering
|
||||
- `to_dict()` returns: `id`, `title`, `model`, `message_count`, `created_at`, `updated_at`
|
||||
- `list_conversations()` uses a subquery for `message_count` instead of eager-loading all messages
|
||||
|
||||
### Messages
|
||||
- `id` (int PK), `conversation_id` (FK to conversations, CASCADE), `role` (str: system/user/assistant),
|
||||
`content` (str), `context_note_id` (nullable FK to notes, SET NULL), `created_at`
|
||||
`content` (str), `status` (str, default `'complete'` — `complete`/`generating`/`error`),
|
||||
`context_note_id` (nullable FK to notes, SET NULL), `created_at`
|
||||
- Index on `conversation_id`
|
||||
- `context_note_id` tracks which note was attached as context when message was sent
|
||||
- `to_dict()` returns: `id`, `conversation_id`, `role`, `content`, `context_note_id`, `created_at`
|
||||
- `to_dict()` returns: `id`, `conversation_id`, `role`, `content`, `status`, `context_note_id`, `created_at`
|
||||
|
||||
## Project Structure (Current)
|
||||
```
|
||||
@@ -164,6 +180,7 @@ fabledassistant/
|
||||
├── pyproject.toml # Python project config
|
||||
├── Dockerfile # Multi-stage build (Node → Python)
|
||||
├── docker-compose.yml # Dev compose (app, PostgreSQL, Ollama)
|
||||
├── docker-compose.prod.yml # Production stack (Docker Swarm, secrets, network isolation)
|
||||
├── alembic.ini # Alembic config (prepend_sys_path = src)
|
||||
├── alembic/
|
||||
│ ├── env.py # Async migration runner
|
||||
@@ -174,29 +191,39 @@ fabledassistant/
|
||||
│ ├── 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
|
||||
│ ├── 0006_add_settings_table.py # Settings key-value table
|
||||
│ └── 0007_add_title_and_updated_at_indexes.py # B-tree indexes on notes.title and conversations.updated_at
|
||||
│ ├── 0007_add_title_and_updated_at_indexes.py # B-tree indexes on notes.title and conversations.updated_at
|
||||
│ ├── 0008_add_users_and_user_id.py # Users table, user_id FKs on notes/conversations/settings, composite PK for settings
|
||||
│ └── 0009_add_message_status.py # Add status column to messages table (default 'complete')
|
||||
├── src/
|
||||
│ └── fabledassistant/
|
||||
│ ├── __init__.py
|
||||
│ ├── app.py # Quart app factory: SPA via 404 handler, JSON 404/500 for API
|
||||
│ ├── config.py # Config from env vars
|
||||
│ ├── app.py # Quart app factory: SPA via 404 handler, JSON 404/500 for API, request logging middleware
|
||||
│ ├── auth.py # Auth decorators: login_required, admin_required, get_current_user_id
|
||||
│ ├── config.py # Config from env vars + Docker secrets file support (_read_secret)
|
||||
│ ├── models/
|
||||
│ │ ├── __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()
|
||||
│ │ └── setting.py # Setting model (key TEXT PK, value TEXT)
|
||||
│ │ ├── __init__.py # async_session factory, Base, imports all models
|
||||
│ │ ├── user.py # User model (id, username, email, password_hash, role, created_at)
|
||||
│ │ ├── note.py # Note model (unified: id, title, body, tags[], parent_id, user_id, status, priority, due_date, timestamps)
|
||||
│ │ ├── conversation.py # Conversation + Message models with user_id
|
||||
│ │ └── setting.py # Setting model (composite PK: user_id + key, 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/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 (imports directly from services.notes, accepts body not description)
|
||||
│ │ └── settings.py # /api/settings GET/PUT — app-wide settings
|
||||
│ │ ├── api.py # /api blueprint with /health endpoint (public)
|
||||
│ │ ├── auth.py # /api/auth blueprint: register, login, logout, me, status
|
||||
│ │ ├── admin.py # /api/admin blueprint: backup (user/full), restore (admin only)
|
||||
│ │ ├── chat.py # /api/chat blueprint: conversations CRUD, SSE message streaming (all @login_required)
|
||||
│ │ ├── notes.py # /api/notes CRUD + wikilinks + backlinks (all @login_required)
|
||||
│ │ ├── tasks.py # /api/tasks CRUD + PATCH status (all @login_required)
|
||||
│ │ └── settings.py # /api/settings GET/PUT — per-user settings (@login_required)
|
||||
│ ├── services/
|
||||
│ │ ├── notes.py # CRUD, is_task filter, status/priority filters, convert_note_to_task, convert_task_to_note, get_backlinks, search_notes_for_context (OR-keyword search), get_all_tags (SQL unnest)
|
||||
│ │ ├── llm.py # Ollama interaction: get_installed_models (shared helper), ensure_model, stream_chat, generate_completion, fetch_url_content, build_context (keyword extraction, single OR-query note search, exclude_note_ids support, returns (messages, context_meta) tuple, URL fetching, configurable assistant name)
|
||||
│ │ ├── chat.py # Conversation CRUD (list uses subquery count), add_message, save_response_as_note, summarize_conversation_as_note
|
||||
│ │ └── settings.py # Settings CRUD: get_setting, set_setting, set_settings_batch, get_all_settings
|
||||
│ │ ├── auth.py # Auth business logic: hash_password, verify_password, create_user, authenticate, get_user_by_id
|
||||
│ │ ├── 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, URL fetching
|
||||
│ │ ├── 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
|
||||
│ │ ├── generation_task.py # Background asyncio task: streams LLM into buffer, periodic DB flush, LLM title generation
|
||||
│ │ └── settings.py # Settings CRUD with user_id isolation: get_setting, set_setting, set_settings_batch, get_all_settings
|
||||
│ ├── utils/
|
||||
│ │ ├── __init__.py
|
||||
│ │ └── tags.py # extract_tags() — regex #tag extraction, skips code fences
|
||||
@@ -209,19 +236,21 @@ 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, design tokens (radius, semantic colors, focus ring, overlay), global focus-visible styles
|
||||
│ │ └── 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
|
||||
│ ├── api/
|
||||
│ │ └── client.ts # apiGet/apiPost/apiPut/apiPatch/apiDelete + apiStreamPost (SSE via fetch + ReadableStream)
|
||||
│ │ └── client.ts # ApiError class, apiGet/apiPost/apiPut/apiPatch/apiDelete + apiSSEStream (EventSource-based), auto 401→login redirect
|
||||
│ ├── composables/
|
||||
│ │ ├── useTheme.ts # Theme toggle, localStorage, prefers-color-scheme
|
||||
│ │ └── useAutocomplete.ts # #tag + [[wikilink]] autocomplete: Tab cycling, debounced search
|
||||
│ ├── 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, 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
|
||||
│ │ ├── auth.ts # Auth state: user, isAuthenticated, isAdmin, login/register/logout/checkAuth
|
||||
│ │ ├── 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 (with toast errors)
|
||||
│ │ ├── settings.ts # App settings: assistantName, defaultModel, pullModel, deleteModel (with toast errors)
|
||||
│ │ └── toast.ts # Toast notification state (success/error/warning), 4s auto-dismiss, dismiss(id)
|
||||
│ ├── types/
|
||||
│ │ ├── auth.ts # User interface, AuthStatus interface
|
||||
│ │ ├── note.ts # Note interface (with status, priority, due_date, is_task) + TaskStatus, TaskPriority types + NoteListResponse
|
||||
│ │ ├── chat.ts # Message, Conversation, ConversationDetail, ContextMeta, OllamaModel, OllamaStatus interfaces
|
||||
│ │ ├── settings.ts # AppSettings interface, ModelInfo interface (name, description, size, bestFor, category)
|
||||
@@ -230,17 +259,19 @@ fabledassistant/
|
||||
│ │ ├── tags.ts # extractTags(), linkifyTags() (with (?<!&) to skip HTML entities), linkifyWikilinks()
|
||||
│ │ └── markdown.ts # renderMarkdown() (full) + renderPreview() (strips links/images) — decodes HTML entities before marked, replaces ' after sanitization
|
||||
│ ├── views/
|
||||
│ │ ├── 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
|
||||
│ │ ├── LoginView.vue # Login form with error display, link to register
|
||||
│ │ ├── RegisterView.vue # Register form (username, password, email), first-user setup
|
||||
│ │ ├── ChatView.vue # Dedicated /chat page: responsive sidebar (overlay on mobile), bubble messages, note picker, context pills
|
||||
│ │ ├── HomeView.vue # Landing page: recent notes + tasks + recent chats
|
||||
│ │ ├── SettingsView.vue # Settings page: assistant name, model catalog, data export/restore (admin)
|
||||
│ │ ├── 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
|
||||
│ │ ├── TasksListView.vue # Task list: search, status/priority filters, sort, pagination, status toggle
|
||||
│ │ ├── 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
|
||||
│ │ ├── NoteViewerView.vue # Markdown render, wikilink auto-create, convert-to-task, backlinks
|
||||
│ │ ├── TasksListView.vue # Task list: search, status/priority filters, sort, pagination
|
||||
│ │ ├── TaskEditorView.vue # Create/edit task: Ctrl+S, dirty guard, autocomplete
|
||||
│ │ └── TaskViewerView.vue # Task detail: rendered markdown, badges, convert-to-note, backlinks
|
||||
│ ├── components/
|
||||
│ │ ├── AppHeader.vue # Nav bar: brand, Notes + Tasks + Chat + Settings links, status indicator dot, chat panel toggle, theme toggle
|
||||
│ │ ├── AppHeader.vue # Nav bar: brand, nav links, status indicator, theme toggle, user info + logout, hamburger menu (mobile)
|
||||
│ │ ├── 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
|
||||
@@ -251,9 +282,9 @@ fabledassistant/
|
||||
│ │ ├── SearchBar.vue # Debounced search input
|
||||
│ │ ├── TagPill.vue # Clickable/dismissible tag pill
|
||||
│ │ ├── PaginationBar.vue # Prev/next + page numbers
|
||||
│ │ └── ToastNotification.vue # Fixed-position toast container
|
||||
│ │ └── ToastNotification.vue # Fixed-position toast container with close button, warning support
|
||||
│ └── router/
|
||||
│ └── index.ts # Routes: /, /notes/*, /tasks/*, /chat, /chat/:id, /settings
|
||||
│ └── index.ts # Routes: /, /login, /register, /notes/*, /tasks/*, /chat, /chat/:id, /settings; beforeEach auth guard
|
||||
└── public/
|
||||
```
|
||||
|
||||
@@ -261,7 +292,14 @@ fabledassistant/
|
||||
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| GET | `/api/health` | Health check |
|
||||
| GET | `/api/health` | Health check (public) |
|
||||
| GET | `/api/auth/status` | Check if any users exist (public) |
|
||||
| POST | `/api/auth/register` | Register new user (first user becomes admin) |
|
||||
| POST | `/api/auth/login` | Login with username/password |
|
||||
| POST | `/api/auth/logout` | Logout (clear session) |
|
||||
| GET | `/api/auth/me` | Get current user info |
|
||||
| GET | `/api/admin/backup` | Export backup (`?scope=user` for own data, full requires admin) |
|
||||
| POST | `/api/admin/restore` | Restore from JSON backup (admin only) |
|
||||
| GET | `/api/notes` | List notes (params: `q`, `tag`, `sort`, `order`, `limit`, `offset`; defaults to `is_task=false` — plain notes only; `?is_task=true` for tasks, `?all=true` for everything) |
|
||||
| POST | `/api/notes` | Create note (body: `{title, body, status?, priority?, due_date?}` — tags auto-extracted) |
|
||||
| GET | `/api/notes/tags` | List all tags from notes table (param: `q` for filter) |
|
||||
@@ -284,8 +322,10 @@ 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?, 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/messages` | Start generation: save user message, launch background task, return 202 (body: `{content, context_note_id?, exclude_note_ids?}`) |
|
||||
| GET | `/api/chat/conversations/:id/generation/stream` | SSE endpoint tailing generation buffer; supports `Last-Event-ID` reconnection; emits `context`, `chunk`, `done`, `error` events |
|
||||
| POST | `/api/chat/conversations/:id/generation/cancel` | Cancel active generation (sets cancel_event, saves partial content) |
|
||||
| POST | `/api/chat/messages/:id/save-as-note` | Save assistant message as a new note (LLM-generated title, tagged `chat`) |
|
||||
| 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 |
|
||||
@@ -303,7 +343,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 → 0006_add_settings_table.py → 0007_add_title_and_updated_at_indexes.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 → 0007_add_title_and_updated_at_indexes.py → 0008_add_users_and_user_id.py → 0009_add_message_status.py
|
||||
```
|
||||
|
||||
### How Migrations Run
|
||||
@@ -438,13 +478,13 @@ When adding a new migration, follow these conventions:
|
||||
- [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)
|
||||
- [x] **Save as note:** Save any assistant message as a new note (LLM-generated title, tagged `chat`)
|
||||
- [x] **Summarize conversation:** Send full conversation to LLM with summarize prompt, save result as note
|
||||
- [x] **Model management:** `GET /api/chat/models` lists available Ollama models; auto-pull default model (`llama3.1`) on app startup
|
||||
- [x] **Dedicated chat page:** `/chat` with sidebar (conversation list, create/delete) + message thread + markdown rendering + streaming indicator
|
||||
- [x] **Slide-out chat panel:** Toggle from header, receives `contextNoteId` from current route (`/notes/:id` or `/tasks/:id`), auto-creates conversation on first message
|
||||
- [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
|
||||
- [x] **Auto-title:** Conversation title generated by LLM (concise 3-8 words)
|
||||
|
||||
### Phase 4.5 — Chat UX + Settings + Model Management ✓
|
||||
- [x] **Settings infrastructure:** Key-value `settings` table, `GET/PUT /api/settings`, Pinia store with defaults
|
||||
@@ -495,12 +535,28 @@ When adding a new migration, follow these conventions:
|
||||
- [x] **Database indexes:** Added B-tree indexes on `notes.title` (for `get_note_by_title` + backlinks) and `conversations.updated_at` (for list ordering)
|
||||
- [x] **Safe `Conversation.to_dict()`:** Handles case where messages relationship is not loaded
|
||||
|
||||
### Phase 5 — Polish & Production Hardening
|
||||
- [ ] Authentication (single-user or multi-user, TBD)
|
||||
- [ ] Docker Swarm production stack (secrets, volumes, networking)
|
||||
- [ ] Backup/restore strategy for data
|
||||
- [ ] UI/UX refinements, responsive design
|
||||
- [ ] Error handling, logging, monitoring
|
||||
### Phase 5 — Polish & Production Hardening ✓
|
||||
- [x] **Multi-user authentication:** Session cookie auth with bcrypt, first-user-is-admin pattern, orphaned data claiming
|
||||
- [x] **Per-user data isolation:** All notes, conversations, settings filtered by `user_id`
|
||||
- [x] **Auth decorators:** `@login_required`, `@admin_required` on all API routes (except health + auth status)
|
||||
- [x] **Frontend auth flow:** Login/register views, router beforeEach guard, ApiError with auto-redirect on 401
|
||||
- [x] **Error handling & logging:** Request logging with timing, sanitized 500 errors, LOG_LEVEL config, toast errors on all store actions
|
||||
- [x] **Toast improvements:** Warning type, dismiss button, 4s auto-dismiss
|
||||
- [x] **Responsive design:** Breakpoints (480/768/1024), utility classes (.hide-mobile/.hide-desktop), 44px mobile touch targets
|
||||
- [x] **Responsive header:** Hamburger menu at ≤768px, vertical nav dropdown, close on route change
|
||||
- [x] **Responsive chat:** Sidebar overlay on mobile with toggle button, slides in/out
|
||||
- [x] **Backup/restore:** Export user data (any user), full backup (admin), restore from JSON (admin)
|
||||
- [x] **Docker Swarm production stack:** `docker-compose.prod.yml` with Docker secrets, internal network isolation, health checks, resource limits, restart policies
|
||||
- [x] **Docker secrets support:** `_read_secret()` helper in config.py for SECRET_KEY_FILE, DATABASE_URL_FILE
|
||||
|
||||
### Phase 5.1 — Chat UX Improvements ✓
|
||||
- [x] **LLM-generated titles:** Conversation titles generated by LLM (3-8 words) on first exchange and re-generated every 10th message to reflect evolved topics; falls back to truncation on failure
|
||||
- [x] **Background generation architecture:** `GenerationBuffer` (in-memory event buffer with SSE fan-out, reconnect via `Last-Event-ID`, auto-cleanup after 60s) + `run_generation()` asyncio task (streams from Ollama, periodic DB flushes every 5s)
|
||||
- [x] **Stop generation:** `cancel_event` on `GenerationBuffer`; `POST .../generation/cancel` endpoint; red stop button replaces send button during streaming; partial content saved as complete
|
||||
- [x] **Date/time in sidebar:** Relative time for recent (`Just now`, `5m ago`, `3h ago` up to 10h), then date (`Jan 15` this year, `Jan 15, 2025` older)
|
||||
- [x] **Empty chat cleanup:** Navigating away from an empty conversation (message_count === 0) auto-deletes it
|
||||
- [x] **Save-as-note LLM titles:** `save_response_as_note` generates title via LLM (same pattern as chat titles); falls back to first-line extraction on failure
|
||||
- [x] **Chat tag on saved notes:** Both `save_response_as_note` and `summarize_conversation_as_note` tag created notes with `chat`
|
||||
|
||||
### Future / Stretch
|
||||
- Tagging/labeling system with LLM-suggested tags
|
||||
@@ -516,27 +572,25 @@ When adding a new migration, follow these conventions:
|
||||
Quart serves them
|
||||
- To reset database: `docker compose down -v && docker compose up --build`
|
||||
|
||||
## Open Questions
|
||||
- Authentication model: single-user (password-only) vs multi-user?
|
||||
|
||||
## Current Status
|
||||
**Phase:** Phase 4.8 complete. Backend efficiency & consistency pass.
|
||||
**Phase:** Phase 5.1 complete. Chat UX Improvements.
|
||||
- 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 (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
|
||||
- Slide-out chat panel accessible from any page, auto-includes note/task context
|
||||
- Settings page: configurable assistant name (default "Fable"), model catalog with 18 models in 3 categories
|
||||
- Model management: download (with live SSE progress percentage), 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
|
||||
- **Multi-user authentication** with session cookies, bcrypt passwords, admin role
|
||||
- **Per-user data isolation** across notes, conversations, and settings
|
||||
- **First-user-is-admin** pattern; orphaned pre-auth data claimed by first user
|
||||
- LLM chat via Ollama with background generation task + SSE streaming
|
||||
- **LLM-generated titles** on first exchange and every 10th message
|
||||
- **Stop generation** button with partial content preservation
|
||||
- **Relative timestamps** in sidebar (5m ago, 3h ago, then dates)
|
||||
- **Empty chat cleanup** on navigation away
|
||||
- Note-aware context: auto-includes current note + searches related notes by keyword
|
||||
- Context pills with promote/exclude controls; note picker in chat input
|
||||
- Save assistant messages as notes (LLM-titled, chat-tagged); summarize conversations as notes
|
||||
- Dedicated `/chat` page with responsive sidebar (overlay on mobile)
|
||||
- Settings page: assistant name, model catalog, **data export/restore (admin)**
|
||||
- **Request logging** with timing, sanitized 500 errors, configurable LOG_LEVEL
|
||||
- **Responsive design**: hamburger menu, mobile touch targets, responsive breakpoints
|
||||
- **Toast notifications** with success/error/warning types and dismiss button
|
||||
- **Docker Swarm production stack** with secrets, network isolation, health checks, resource limits
|
||||
- Dark/light theme with CSS custom properties and design tokens
|
||||
|
||||
Reference in New Issue
Block a user