Fix user bubble text contrast, Chat nav active state, and update summary.md

- theme.css: darken user bubble text (#3a3a4a light / #b0b0c8 dark) for better readability
- AppHeader: highlight Chat nav link when on any /chat/* route (was only active on exact /chat)
- summary.md: document workspace, graph, queue persistence, ToolCallCard direct-API, dedup scoping

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-10 18:00:04 -04:00
parent ffe1087aac
commit 16220c57a0
3 changed files with 106 additions and 20 deletions
+5 -5
View File
@@ -41,8 +41,8 @@
--color-input-bar-placeholder: rgba(0, 0, 0, 0.4);
--color-overlay: rgba(0, 0, 0, 0.45);
--color-bubble-user-bg: rgba(0, 0, 0, 0.04);
--color-bubble-user-border: rgba(0, 0, 0, 0.08);
--color-bubble-user-text: #555566;
--color-bubble-user-border: rgba(0, 0, 0, 0.10);
--color-bubble-user-text: #3a3a4a;
--color-bubble-asst-shadow: 0 2px 16px rgba(99, 102, 241, 0.10), 0 1px 4px rgba(0, 0, 0, 0.06);
--radius-sm: 6px;
--radius-md: 12px;
@@ -91,9 +91,9 @@
--color-input-bar-text: #e4e4f0;
--color-input-bar-placeholder: rgba(228, 228, 240, 0.35);
--color-overlay: rgba(0, 0, 0, 0.65);
--color-bubble-user-bg: rgba(255, 255, 255, 0.03);
--color-bubble-user-border: rgba(255, 255, 255, 0.07);
--color-bubble-user-text: #5a5a72;
--color-bubble-user-bg: rgba(255, 255, 255, 0.04);
--color-bubble-user-border: rgba(255, 255, 255, 0.10);
--color-bubble-user-text: #b0b0c8;
--color-bubble-asst-shadow: 0 4px 28px rgba(99, 102, 241, 0.14), 0 2px 8px rgba(0, 0, 0, 0.4);
}
+6 -3
View File
@@ -1,6 +1,6 @@
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted } from "vue";
import { useRouter } from "vue-router";
import { useRouter, useRoute } from "vue-router";
import { useTheme } from "@/composables/useTheme";
import { useShortcuts } from "@/composables/useShortcuts";
import { useAuthStore } from "@/stores/auth";
@@ -12,6 +12,9 @@ const { toggleShortcuts } = useShortcuts();
const authStore = useAuthStore();
const chatStore = useChatStore();
const router = useRouter();
const route = useRoute();
const isChatActive = computed(() => route.path.startsWith("/chat"));
const mobileMenuOpen = ref(false);
const gearOpen = ref(false);
@@ -87,7 +90,7 @@ onUnmounted(() => document.removeEventListener("click", handleClickOutside));
<router-link to="/notes" class="nav-link">Notes</router-link>
<router-link to="/projects" class="nav-link">Projects</router-link>
<router-link to="/tasks" class="nav-link">Tasks</router-link>
<router-link to="/chat" class="nav-link">Chat</router-link>
<router-link to="/chat" :class="['nav-link', { 'router-link-active': isChatActive }]">Chat</router-link>
<router-link to="/graph" class="nav-link">Graph</router-link>
</div>
@@ -139,7 +142,7 @@ onUnmounted(() => document.removeEventListener("click", handleClickOutside));
<router-link to="/notes" class="nav-link">Notes</router-link>
<router-link to="/projects" class="nav-link">Projects</router-link>
<router-link to="/tasks" class="nav-link">Tasks</router-link>
<router-link to="/chat" class="nav-link">Chat</router-link>
<router-link to="/chat" :class="['nav-link', { 'router-link-active': isChatActive }]">Chat</router-link>
<router-link to="/graph" class="nav-link">Graph</router-link>
<div class="mobile-divider"></div>
<router-link to="/settings" class="nav-link">Settings</router-link>
+95 -12
View File
@@ -12,7 +12,7 @@
> Include file-level details in the commit body when the change is non-trivial.
## Last Updated
2026-03-10 — UI polish pass: "Illuminated Transcript" design language, Fraunces typography, indigo gradient buttons, shadow-based card depth, bubble hierarchy flip.
2026-03-10 — Feature batch: workspace view, graph view, settings tabs, message queue persistence, ToolCallCard direct-API confirm, duplicate detection scoping, push notification fixes.
**UI polish pass — design language:**
- `frontend/src/assets/theme.css`: added Fraunces Google Font import; `h1/h2/h3` use `font-family: 'Fraunces', Georgia, serif`; `--radius-md` 8→12px, `--radius-lg` 12→18px; added thin indigo-tinted scrollbars (4px); added bubble-specific CSS variables (`--color-bubble-user-*`, `--color-bubble-asst-shadow`); dark palette shifted from generic navy (#1a1a2e family) to slate-indigo (#111113 family).
@@ -28,6 +28,55 @@
---
**Previous session (2026-03-10):** Feature batch: workspace view, graph view, settings tabs, message queue persistence, ToolCallCard direct-API confirm, duplicate detection scoping, push notification fixes.
**Project Workspace (`/workspace/:projectId`):**
- `frontend/src/views/WorkspaceView.vue` (new): 3-panel layout (tasks / chat / notes) with CSS grid collapse. Panel toggles in ws-header. Conversation persisted to `localStorage` as `workspace_conv_{projectId}`; deleted on unmount only if brand-new and empty. `sendMessage()` passes `projectId` as both `ragProjectId` and `workspaceProjectId`; `think: true` always enabled. SSE watcher auto-loads note on `create_note`/`update_note`, refreshes task panel on `create_task`/`update_task`.
- `frontend/src/components/WorkspaceTaskPanel.vue` (new): tasks grouped by milestone (No Milestone first), collapsible groups, task detail slide-over with status cycle + milestone `<select>` + read-only markdown body + TaskLogSection + inline-confirm delete.
- `frontend/src/components/WorkspaceNoteEditor.vue` (new): list view (search, inline tags, inline delete) ↔ editor view (TipTap + TagInput + WordCount + Suggest button + autosave via `useAutoSave`).
- Entry point: "Open Workspace" button on `ProjectView.vue` page header.
- Height fix: `workspace-root` uses `height: 100%`; `app-content:has(.workspace-root)` switches to `overflow: hidden`.
- Backend: `workspace_project_id` accepted in POST body (`routes/chat.py`), threaded to `run_generation` and `build_context()`; injects system prompt with project title so agent passes `project="Title"` (not ID) to tools.
**Graph View (`/graph`):**
- `frontend/src/views/GraphView.vue` (new): D3 force-directed graph of all notes/tasks. Tag nodes as first-class nodes (string IDs `"tag:name"`); invisible project hub nodes attract project members. Physics panel with sliders: repulsion (110), link distance (80), link strength (0.50), hub pull (0.05), gravity (0.03). `forceX`/`forceY` for gravity (not `forceCenter`). Clicking tag node → `/notes?tags=name`. Note/task node → 340px slide-in peek panel (type badge, tags, rendered body, linked nodes; Escape/✕ closes). `peek-slide` CSS transition.
- Router: `/graph` route added to `frontend/src/router/index.ts`.
- Keyboard shortcut: bare `g` navigates to `/graph` (added to `App.vue`).
**Settings page tabs:**
- `frontend/src/views/SettingsView.vue`: fully rewritten with 6 tabs — General, Account, Notifications, Integrations, Data, Admin. Tab state persisted to `localStorage` key `settings_tab`. Admin tab only shown when `authStore.isAdmin`. Session invalidation button added in Admin tab (`POST /api/auth/invalidate-sessions`).
**Push notification fixes:**
- `frontend/public/sw.js`: suppresses notification if the relevant chat tab is already focused (`clients.matchAll()` visibility check before `showNotification`). Works over HTTPS only — Firefox shows "not supported" over plain HTTP.
- `src/fabledassistant/services/push.py`: errors in `_send_sync` promoted to `logger.error` with `exc_info=True`; successful sends logged at `logger.info`.
- `src/fabledassistant/services/generation_task.py`: tool-only responses (empty `content_so_far`) now synthesise push notification body from tool names (`"Completed: create_note, create_task"`) instead of sending an empty notification.
**Message queue improvements:**
- `frontend/src/components/DashboardChatInput.vue`: removed `|| store.streaming` from disabled checks — queue works on the dashboard widget.
- `frontend/src/stores/chat.ts`: queue persisted to `localStorage` key `fa_conv_queue_{convId}`; saved on enqueue/dequeue, restored in `fetchConversation`, removed on conversation delete. `queuedMessages` computed returns full `QueuedMessage[]` for current conv.
- `frontend/src/views/ChatView.vue` + `WorkspaceView.vue`: queued messages rendered as user-style bubbles (`linear-gradient(135deg, #6366f1, #4f46e5)`, `border-bottom-right-radius: 4px`, 18px radius, opacity 0.45, right-aligned via `.chat-message.role-user { justify-content: flex-end }`). "QUEUED" label in `rgba(255,255,255,0.75)`. Cancel button clears entire queue.
**ToolCallCard confirm/deny — direct API:**
- `frontend/src/components/ToolCallCard.vue`: confirm/deny buttons moved **inline in the header row** (no separate block). "Create anyway" posts directly to `POST /api/notes` or `POST /api/tasks` (inferred from `toolCall.function`); shows "✓ Created" on success, "Skipped" on deny — no LLM round-trip. Removed `useChatStore` dependency for these actions. `confirmState: 'idle' | 'creating' | 'created' | 'denied'` local ref.
- `src/fabledassistant/routes/notes.py` + `routes/tasks.py`: POST handlers now accept `project` (name string) and resolve to `project_id` via `get_project_by_title` when `project_id` not supplied directly.
**Duplicate detection fixes:**
- `src/fabledassistant/services/tools.py`: `create_note` and `create_task` now scope all three duplicate checks (exact title, fuzzy title, semantic similarity) to the same type only — `is_task=True` for `create_task`, `is_task=False` for `create_note`. A note and a task can share the same title without conflict.
- `src/fabledassistant/services/embeddings.py`: `semantic_search_notes()` gains `is_task: bool | None = None` parameter; filters SQL join to `Note.status.isnot(None)` / `Note.status.is_(None)` when set.
- Semantic similarity threshold raised 0.87 → 0.90; minimum body length raised 80 → 200 chars. Catches near-identical content (accidentally duplicated notes) while allowing topically-related notes (e.g. enemy design templates) to coexist.
**Abort button:**
- ■ Stop button in `ChatView.vue` header and `WorkspaceView.vue` input bar; calls `store.cancelGeneration()`.
**CI fix (ruff E741):**
- `src/fabledassistant/routes/task_logs.py`: renamed ambiguous variable `l``log` in list comprehension.
---
**Previous session (2026-03-10):** UI polish pass: "Illuminated Transcript" design language, Fraunces typography, indigo gradient buttons, shadow-based card depth, bubble hierarchy flip.
---
**Previous session (2026-03-10):** CI/CD optimization: custom runner base image, explicit label naming, pip caching, Node 22.
**CI/CD infrastructure overhaul:**
@@ -423,10 +472,15 @@ fabledassistant/
│ │ ├── admin.py # /api/admin blueprint: backup, restore, user management, registration toggle, invitations, base URL, SMTP (admin only)
│ │ ├── chat.py # /api/chat blueprint: conversations CRUD, SSE message streaming (all @login_required)
│ │ ├── images.py # /api/images/<id> GET — serve locally-cached images (no auth required; IDs are opaque)
│ │ ├── notes.py # /api/notes CRUD + wikilinks + backlinks + tag suggestions (all @login_required)
│ │ ├── quick_capture.py # /api/quick-capture POST — mobile/external single-shot item creation (session auth); uses classify_capture_intent(); supports create_note/task/event, update_note, research_topic; fallback always preserves full text as note body
│ │ ├── tasks.py # /api/tasks CRUD + PATCH status (all @login_required)
│ │ ── settings.py # /api/settings GET/PUT, GET /models — per-user settings + installed model list (@login_required)
│ │ ├── notes.py # /api/notes CRUD + wikilinks + backlinks + tag suggestions + link suggestions + assist; POST accepts `project` name string (resolves to project_id)
│ │ ├── quick_capture.py # /api/quick-capture POST — mobile/external single-shot item creation
│ │ ├── tasks.py # /api/tasks CRUD + PATCH status; POST accepts `project` name string
│ │ ── task_logs.py # /api/tasks/<id>/logs GET/POST/PATCH/<log_id>/DELETE/<log_id>
│ │ ├── projects.py # /api/projects CRUD
│ │ ├── milestones.py # /api/projects/<pid>/milestones CRUD
│ │ ├── push.py # /api/push — VAPID public key, subscribe/unsubscribe
│ │ ├── export.py # /api/export?format=markdown|json — ZIP or JSON export
│ │ └── settings.py # /api/settings GET/PUT, GET /models
│ ├── 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)
@@ -435,17 +489,22 @@ fabledassistant/
│ │ ├── llm.py # Ollama interaction: build_context with user_id, streaming (stream_chat + stream_chat_with_tools), ChatChunk dataclass, URL fetching; generate_completion accepts num_ctx kwarg; 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-first pipeline + tool loop) + run_assist_generation (lightweight, no DB); _INTENT_TRIGGER_WORDS + _should_skip_intent() for skipping intent on conversational messages
│ │ ├── generation_task.py # Background asyncio tasks: run_generation + run_assist_generation. Tool-only responses synthesise push notification body from tool names. workspace_project_id threaded through to build_context().
│ │ ├── intent.py # Intent routing: classify_intent() makes fast non-streaming LLM call (num_ctx=4096); IntentResult has ack field; classify_capture_intent() uses dedicated _CAPTURE_SYSTEM_PROMPT (always routes to a tool, never null)
│ │ ├── images.py # Image cache service: fetch_and_store_image (SHA-256 dedup, content-type validation, 5MB cap), get_image_record, get_image_path
│ │ ├── 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, CalDAV event suite (no todos), search_web, research_topic, search_images when SearXNG enabled) + execute_tool dispatcher
│ │ ├── tools.py # LLM tool definitions + execute_tool dispatcher. Duplicate detection in create_note/create_task: exact title, fuzzy (SequenceMatcher ≥0.82), semantic (cosine ≥0.90, body ≥200 chars) — all scoped to same type (is_task). POST /api/notes|tasks accept `project` name string.
│ │ ├── research.py # SearXNG research pipeline: parallel query/fetch, streaming synthesis; run_research_pipeline(buf=None) → Note (buf optional — no status events when omitted); _search_searxng_images() for image category search; constants SEARXNG_QUERIES=5, PAGES_PER_QUERY=3, MAX_SYNTHESIS_SOURCES=12, CHARS_PER_SOURCE=2000
│ │ ├── tag_suggestions.py # LLM-powered tag suggestions: suggest_tags() builds prompt with existing tags, calls generate_completion, parses JSON response
│ │ ├── caldav.py # CalDAV integration: full event lifecycle (create/list/search/update/delete), list_calendars, timezone (ZoneInfo), reminders (VALARM), attendees, multi-calendar search. Todo functions exist in code but are NOT exposed as LLM tools.
│ │ ├── settings.py # Settings CRUD with user_id isolation: get_setting, set_setting, set_settings_batch, get_all_settings
│ │ ├── logging.py # App logging: log_audit, log_usage, log_error, get_logs, get_log_stats, delete_old_logs, start_log_retention_loop
│ │ ├── email.py # SMTP email service: get_smtp_config, is_smtp_configured, send_email, send_test_email
│ │ ── notifications.py # Notification service: notify_security_event, check_due_tasks, start_notification_loop
│ │ ── notifications.py # Notification service: notify_security_event, check_due_tasks, start_notification_loop
│ │ ├── push.py # VAPID key generation/loading, send_push_notification (fire-and-forget); pywebpush 2.x API (webpush() function). sw.js suppresses notification when relevant chat tab is focused.
│ │ ├── projects.py # Project CRUD + get_project_by_title + get_project_summary (includes milestone_summary)
│ │ ├── milestones.py # Milestone CRUD + get_milestone_progress
│ │ ├── task_logs.py # create_log, list_logs, update_log (_UNSET sentinel for optional duration_minutes), delete_log — all ownership-checked
│ │ └── assist.py # Writing assistant: build_assist_messages (section/whole-doc modes, project context injection)
│ ├── utils/
│ │ ├── __init__.py
│ │ └── tags.py # extract_tags() — regex #tag extraction, skips code fences
@@ -496,9 +555,11 @@ fabledassistant/
│ │ ├── RegisterView.vue # Register form with password confirmation; shows "closed" message when registration disabled
│ │ ├── RegisterInviteView.vue # Invitation-based registration: validates token, creates account with pre-set email
│ │ ├── 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, context sidebar with “In Context” (user-included, ×) + “Suggested” (auto-found, +), model selector in header
│ │ ├── ChatView.vue # Dedicated /chat page: responsive sidebar, bubble messages, note picker, context sidebar (auto-included + suggested), model selector, abort button, bulk-select conversations, per-conv message queue with localStorage persistence; queued bubbles indigo gradient right-aligned
│ │ ├── WorkspaceView.vue # /workspace/:projectId — 3-panel (tasks/chat/notes), CSS grid collapse, SSE tool-call watcher auto-loads notes + refreshes tasks, conv persisted to localStorage
│ │ ├── GraphView.vue # /graph — D3 force-directed graph: note/task/tag/project-hub nodes, physics panel, peek panel slide-in on click, tag-click nav to /notes?tags=
│ │ ├── 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, 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)
│ │ ├── SettingsView.vue # Tabbed settings (6 tabs: General, Account, Notifications, Integrations, Data, Admin); tab state in localStorage key `settings_tab`; Admin tab: session invalidation button, SMTP, base URL, data export/restore, user management links
│ │ ├── NotesListView.vue # Note list: search, sort, tag filter pills, pagination
│ │ ├── NoteEditorView.vue # Create/edit: Tiptap editor, sticky toolbar, TagInput chip field (between title and body), ProjectSelector + MilestoneSelector (milestone resets when project changes), AI Assist panel (right-side 320px, collapsible), line-level diff view, Proofread action, floating inline ✨ pill on text selection, LLM tag suggestions (adds chips), 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
@@ -511,7 +572,9 @@ fabledassistant/
│ │ ├── ChatPanel.vue # Slide-out chat panel (right side overlay, receives contextNoteId prop), bubble-style messages, floating dark input, note picker, context pills with promote (+) only (no exclude)
│ │ ├── ModelSelector.vue # Model dropdown (v-model pattern): fetches installed + running models, hot/cold indicators
│ │ ├── DashboardChatInput.vue # Inline chat bar: ModelSelector + note picker + textarea + send button; emits submit event
│ │ ├── ToolCallCard.vue # Compact card for tool call results (created/deleted task/note, note content, notes list, search results, CalDAV events/todos, errors) + suggested tag pills with apply-on-click
│ │ ├── WorkspaceTaskPanel.vue # Tasks grouped by milestone, collapsible, detail slide-over (status cycle + milestone select + body + TaskLogSection + delete)
│ │ ├── WorkspaceNoteEditor.vue # List view (search, inline tags, delete) ↔ TipTap editor (TagInput + WordCount + autosave)
│ │ ├── ToolCallCard.vue # Compact card for tool call results + suggested tag pills. Duplicate `requires_confirmation` state: inline confirm/deny buttons in header row; "Create anyway" → direct POST /api/notes or /api/tasks (no LLM round-trip); `confirmState: idle|creating|created|denied`
│ │ ├── ChatMessage.vue # Message bubble: markdown rendering, tool call cards, configurable assistant name label, "Save as Note" action on assistant messages
│ │ ├── NoteCard.vue # Card with rendered markdown preview (v-html), TagPill, tag-click emit, hover edit button
│ │ ├── TaskCard.vue # Card with rendered preview (body not description), StatusBadge (clickable), PriorityBadge, due date, tags, hover edit button
@@ -527,7 +590,7 @@ fabledassistant/
│ │ ├── TableOfContents.vue # Sticky sidebar TOC: parses markdown headings, smooth-scroll on click, hidden ≤1200px
│ │ └── ToastNotification.vue # Fixed-position toast container with close button, warning support
│ └── router/
│ └── index.ts # Routes: /, /login, /register, /register-invite, /notes/*, /tasks/*, /chat, /chat/:id, /settings, /admin/users, /admin/logs; beforeEach auth guard
│ └── index.ts # Routes: /, /login, /register, /register-invite, /notes/*, /tasks/*, /chat, /chat/:id, /workspace/:projectId, /graph, /projects, /projects/:id, /settings, /admin/users, /admin/logs; beforeEach auth guard
└── public/
```
@@ -604,6 +667,26 @@ fabledassistant/
| 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}`) |
| GET | `/api/projects` | List projects |
| POST | `/api/projects` | Create project |
| GET | `/api/projects/:id` | Get project with milestone summary |
| PATCH | `/api/projects/:id` | Update project |
| DELETE | `/api/projects/:id` | Delete project |
| GET | `/api/projects/:id/milestones` | List milestones for project |
| POST | `/api/projects/:id/milestones` | Create milestone |
| PATCH | `/api/projects/:id/milestones/:mid` | Update milestone |
| DELETE | `/api/projects/:id/milestones/:mid` | Delete milestone |
| GET | `/api/tasks/:id/logs` | List work logs for task |
| POST | `/api/tasks/:id/logs` | Create work log (body: `{content, duration_minutes?}`) |
| PATCH | `/api/tasks/:id/logs/:log_id` | Update work log |
| DELETE | `/api/tasks/:id/logs/:log_id` | Delete work log |
| GET | `/api/push/vapid-public-key` | Get VAPID public key for push subscription |
| POST | `/api/push/subscribe` | Register push subscription |
| DELETE | `/api/push/subscribe` | Unregister push subscription |
| GET | `/api/export` | Export notes/tasks (`?format=markdown` → ZIP, `?format=json` → JSON array) |
| POST | `/api/auth/invalidate-sessions` | Bump `session_version` — invalidates all other sessions, keeps current alive |
| POST | `/api/chat/conversations/:id/messages` | Start generation (body: `{content, context_note_id?, include_note_ids?, rag_project_id?, workspace_project_id?}`) — workspace_project_id injects project system prompt |
| POST | `/api/chat/conversations/bulk-delete` | Delete multiple conversations (body: `{ids: number[]}`) |
## Alembic Migrations