From ac4b2359a5c029392adae3283c31e75f12479172 Mon Sep 17 00:00:00 2001 From: bvandeusen Date: Sat, 4 Apr 2026 22:30:44 -0400 Subject: [PATCH] docs: add Knowledge View design spec for Android app Covers four-tab navigation (Briefing/Knowledge/Chat/Projects), two-tier pagination (50 IDs / 12-item batches), type-aware cards, bottom sheet type picker, Projects screen with sorted project list, ProjectEditScreen, and manifest/config checklist. Co-Authored-By: Claude Sonnet 4.6 --- .../specs/2026-04-04-knowledge-view-design.md | 455 ++++++++++++++++++ 1 file changed, 455 insertions(+) create mode 100644 docs/superpowers/specs/2026-04-04-knowledge-view-design.md diff --git a/docs/superpowers/specs/2026-04-04-knowledge-view-design.md b/docs/superpowers/specs/2026-04-04-knowledge-view-design.md new file mode 100644 index 0000000..1da61e5 --- /dev/null +++ b/docs/superpowers/specs/2026-04-04-knowledge-view-design.md @@ -0,0 +1,455 @@ +# Knowledge View — Android App Design Spec + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Replace the Library tab with a typed Knowledge view and add a dedicated Projects tab, bringing the Android app to parity with the web Knowledge view while using the existing backend `/api/knowledge` and `/api/projects` endpoints. + +**Architecture:** Two-tier pagination — fetch IDs cheaply (50 at a time), hydrate visible items in batches of 12. Type tabs drive server-side filtering. A new Projects tab replaces the project section formerly in Library. All data access goes through the existing Riverpod/Dio/repository pattern. + +**Tech Stack:** Flutter 3, Riverpod 3, GoRouter 17, Dio 5, existing backend REST API (`/api/knowledge`, `/api/projects`, `/api/notes`) + +--- + +## Manifest & Configuration Checklist + +These must be verified before any code is written. Failures here cause silent runtime errors. + +- [ ] `android/app/src/main/AndroidManifest.xml` — confirm `` is present +- [ ] `android/app/src/main/AndroidManifest.xml` — confirm `android:usesCleartextTraffic="true"` is set on the `` tag (required for HTTP dev server connections; if already using HTTPS only, leave as-is but document the decision) +- [ ] `pubspec.yaml` — no new dependencies required for Knowledge View +- [ ] `android/app/build.gradle` — no changes required for Knowledge View + +--- + +## File Map + +### New files +| Path | Responsibility | +|---|---| +| `lib/data/models/knowledge_item.dart` | Unified model for all knowledge types (note, person, place, list, task) | +| `lib/data/api/knowledge_api.dart` | Two API methods: fetch IDs, batch-hydrate items | +| `lib/data/repositories/knowledge_repository.dart` | Thin repo wrapping KnowledgeApi | +| `lib/providers/knowledge_provider.dart` | StateNotifier with two-tier pagination state | +| `lib/screens/knowledge/knowledge_screen.dart` | Main Knowledge screen with type tabs, tag chips, search, infinite scroll | +| `lib/widgets/knowledge_item_card.dart` | Type-aware card widget (different icon/subtitle per type) | +| `lib/screens/projects/projects_screen.dart` | Project list sorted by updated_at desc | +| `lib/screens/projects/project_edit_screen.dart` | Create/edit project (title, description, goal, status) | + +### Modified files +| Path | Change | +|---|---| +| `lib/data/models/note.dart` | Add `noteType` field (String, default `'note'`) | +| `lib/data/models/project.dart` | Add `status`, `goal`, `color`, `autoSummary`, `updatedAt` fields | +| `lib/data/api/notes_api.dart` | Pass `note_type` in create and update request bodies | +| `lib/data/api/projects_api.dart` | Add `sort=updated_at&order=desc` to list call; add `status`, `goal`, `color` to create/update | +| `lib/screens/notes/note_edit_screen.dart` | Accept optional `noteType` parameter; show type badge in app bar; pass `note_type` to API | +| `lib/screens/library/project_tasks_screen.dart` | Add edit button in app bar pushing to `ProjectEditScreen` | +| `lib/app.dart` | Replace `Routes.library` with `Routes.knowledge` + `Routes.projects`; update shell to 4 tabs | +| `lib/core/constants.dart` | Add `Routes.knowledge`, `Routes.projects`; remove `Routes.library` | + +### Retired files +| Path | Replacement | +|---|---| +| `lib/screens/library/library_screen.dart` | `KnowledgeScreen` + `ProjectsScreen` | +| `lib/widgets/library_item_card.dart` | `KnowledgeItemCard` | + +--- + +## Section 1: Navigation + +Four tabs replace the existing three: + +``` +Briefing | Knowledge | Chat | Projects +``` + +**`lib/core/constants.dart`:** +- Remove `static const library = '/library'` +- Add `static const knowledge = '/knowledge'` +- `projects` already exists as `'/projects'` — no change needed +- Add `static const projectEdit = '/projects/:id/edit'` + +**`lib/app.dart` — `_ShellState`:** +```dart +static const _tabs = [ + Routes.briefing, + Routes.knowledge, + Routes.conversations, + Routes.projects, +]; +``` + +Shell `NavigationBar` / `NavigationRail` entries: +```dart +// Bottom nav +NavigationDestination(icon: Icon(Icons.wb_sunny_outlined), selectedIcon: Icon(Icons.wb_sunny), label: 'Briefing'), +NavigationDestination(icon: Icon(Icons.menu_book_outlined), selectedIcon: Icon(Icons.menu_book), label: 'Knowledge'), +NavigationDestination(icon: Icon(Icons.chat_bubble_outline), selectedIcon: Icon(Icons.chat_bubble), label: 'Chat'), +NavigationDestination(icon: Icon(Icons.folder_outlined), selectedIcon: Icon(Icons.folder), label: 'Projects'), +``` + +**`_QuickCaptureBar._hintForLocation` update:** +```dart +String _hintForLocation(String location) { + if (location.startsWith(Routes.knowledge)) return 'Capture a note…'; + if (location.startsWith(Routes.projects)) return 'Capture a note…'; + if (location.startsWith(Routes.conversations)) return 'Ask Fabled…'; + return 'Capture a note…'; +} +``` + +**GoRouter shell routes** — replace `Routes.library` route with: +```dart +GoRoute(path: Routes.knowledge, builder: (_, _) => const KnowledgeScreen()), +GoRoute(path: Routes.projects, builder: (_, _) => const ProjectsScreen()), +``` + +--- + +## Section 2: Data Models + +### `lib/data/models/knowledge_item.dart` +```dart +class KnowledgeItem { + final int id; + final String noteType; // 'note' | 'person' | 'place' | 'list' | 'task' + final String title; + final String body; + final List tags; + final int? projectId; + final int? milestoneId; + final int? parentId; + // Task-only fields (null for non-tasks) + final String? status; // 'todo' | 'in_progress' | 'done' | 'cancelled' + final String? priority; // 'low' | 'normal' | 'high' + final String? dueDate; + final DateTime createdAt; + final DateTime updatedAt; + + const KnowledgeItem({...}); + + factory KnowledgeItem.fromJson(Map json) => KnowledgeItem( + id: json['id'] as int, + noteType: json['note_type'] as String? ?? 'note', + title: json['title'] as String? ?? '', + body: json['body'] as String? ?? '', + tags: (json['tags'] as List?)?.map((e) => e as String).toList() ?? [], + projectId: json['project_id'] as int?, + milestoneId: json['milestone_id'] as int?, + parentId: json['parent_id'] as int?, + status: json['status'] as String?, + priority: json['priority'] as String?, + dueDate: json['due_date'] as String?, + createdAt: DateTime.parse(json['created_at'] as String), + updatedAt: DateTime.parse(json['updated_at'] as String), + ); +} +``` + +### `lib/data/models/note.dart` — add `noteType` +```dart +final String noteType; // new field + +// In constructor: +required this.noteType, + +// In fromJson: +noteType: json['note_type'] as String? ?? 'note', + +// In toJson: +'note_type': noteType, + +// In copyWith: add noteType parameter +``` + +### `lib/data/models/project.dart` — add missing fields +```dart +final String status; // 'active' | 'completed' | 'archived' +final String? goal; +final String? color; +final String? autoSummary; +final DateTime updatedAt; + +// In fromJson: +status: json['status'] as String? ?? 'active', +goal: json['goal'] as String?, +color: json['color'] as String?, +autoSummary: json['auto_summary'] as String?, +updatedAt: DateTime.parse(json['updated_at'] as String), +``` + +--- + +## Section 3: API Layer + +### `lib/data/api/knowledge_api.dart` +```dart +class KnowledgeApi { + final Dio _dio; + const KnowledgeApi(this._dio); + + /// Fetch page of IDs. Returns (ids, total). + Future<(List, int)> fetchIds({ + String? noteType, + List tags = const [], + String sort = 'modified', + String? q, + int limit = 50, + int offset = 0, + }) async { + final params = { + 'limit': limit, + 'offset': offset, + 'sort': sort, + if (noteType != null) 'type': noteType, + if (tags.isNotEmpty) 'tags': tags.join(','), + if (q != null && q.isNotEmpty) 'q': q, + }; + final response = await _dio.get('/api/knowledge/ids', queryParameters: params); + final data = response.data as Map; + final ids = (data['ids'] as List).map((e) => e as int).toList(); + final total = data['total'] as int; + return (ids, total); + } + + /// Batch-hydrate up to 100 IDs into full items. + Future> fetchBatch(List ids) async { + if (ids.isEmpty) return []; + final response = await _dio.get( + '/api/knowledge/batch', + queryParameters: {'ids': ids.join(',')}, + ); + final data = response.data as Map; + return (data['items'] as List) + .map((e) => KnowledgeItem.fromJson(e as Map)) + .toList(); + } + + /// Per-type counts for tab labels. + Future> fetchCounts({List tags = const []}) async { + final params = { + if (tags.isNotEmpty) 'tags': tags.join(','), + }; + final response = await _dio.get('/api/knowledge/counts', queryParameters: params); + return (response.data as Map).map( + (k, v) => MapEntry(k, (v as num).toInt()), + ); + } + + /// All tags for the current type filter. + Future> fetchTags({String? noteType}) async { + final response = await _dio.get( + '/api/knowledge/tags', + queryParameters: {if (noteType != null) 'type': noteType}, + ); + return (response.data['tags'] as List).map((e) => e as String).toList(); + } +} +``` + +### `lib/data/api/projects_api.dart` — add sort params + missing fields +Add `sort: 'updated_at'` and `order: 'desc'` to the `getProjects` query parameters. +Add `status`, `goal`, `color` to `createProject` and `updateProject` request bodies. + +### `lib/data/api/notes_api.dart` — pass `note_type` +```dart +// In createNote and updateNote bodies: +if (noteType != null) 'note_type': noteType, +``` + +--- + +## Section 4: Provider + +### `lib/providers/knowledge_provider.dart` + +```dart +@immutable +class KnowledgeState { + final List ids; // all fetched IDs so far + final Map items; // hydrated items + final int totalIds; + final bool isLoadingIds; + final bool isLoadingBatch; + final bool hasMore; + final String? noteType; // active type filter + final List activeTags; + final String? searchQuery; + final Map counts; // per-type counts + + bool get canLoadMore => hasMore && !isLoadingIds; + // Items in ID order, only those hydrated + List get orderedItems => + ids.where(items.containsKey).map((id) => items[id]!).toList(); +} + +class KnowledgeNotifier extends StateNotifier { + // On filter change: reset state, fetch IDs from offset 0 + void setTypeFilter(String? noteType) { ... } + void toggleTag(String tag) { ... } + void setSearch(String? q) { ... } // debounce handled in UI + + // Fetch next page of IDs (50 at a time) + Future loadMoreIds() { ... } + + // Hydrate the next 12 un-hydrated IDs from the current list + Future hydrateNext() { ... } + + Future refresh() { ... } // reset + reload +} + +// Provider +final knowledgeProvider = + StateNotifierProvider(...); +``` + +**Scroll trigger:** `KnowledgeScreen` attaches a `ScrollController` listener. When `position.pixels >= maxScrollExtent - 300`: +1. Call `hydrateNext()` if there are un-hydrated IDs in the list +2. Call `loadMoreIds()` if all fetched IDs are hydrated and `hasMore` is true + +--- + +## Section 5: Knowledge Screen + +### `lib/screens/knowledge/knowledge_screen.dart` + +**Structure:** +``` +Scaffold + AppBar + title: 'Knowledge' + actions: [search icon → expand TextField] + Column + TabBar (All | Notes | People | Places | Lists | Tasks) + — tab labels show counts: "Notes (12)" + AnimatedContainer (tag filter chip row, hidden when empty) + — horizontal SingleChildScrollView of FilterChip widgets + Expanded + ListView.builder + — items from knowledgeState.orderedItems + — trailing: loading indicator when isLoadingBatch + FAB (pen icon) + — Tasks tab → push TaskEditScreen + — all other tabs → showModalBottomSheet (type picker) +``` + +**Pull-to-refresh:** `RefreshIndicator` wrapping the `ListView`. + +**Empty state:** Centered column with type-appropriate icon and "No [type] yet" text. + +**Error state:** Centered text with retry button calling `ref.invalidate(knowledgeProvider)`. + +### `lib/widgets/knowledge_item_card.dart` + +`ListTile`-based card. Leading icon varies by type: +- note → `Icons.description_outlined` +- person → `Icons.person_outlined` +- place → `Icons.place_outlined` +- list → `Icons.checklist_outlined` +- task → `Icons.task_alt_outlined` (with status colour on leading) + +Subtitle shows truncated body (max 2 lines) or due date for tasks. +Trailing shows tag chips (up to 2, then "+N more"). + +Tap → `NoteDetailScreen(noteId: item.id)` for knowledge types; `TaskEditScreen(taskId: item.id)` for tasks. + +--- + +## Section 6: Type Picker Bottom Sheet + +Shown when FAB is tapped on any non-Tasks tab. + +```dart +showModalBottomSheet( + context: context, + builder: (_) => Column( + mainAxisSize: MainAxisSize.min, + children: [ + _TypeRow(Icons.description_outlined, 'Note', 'General note or document', 'note'), + _TypeRow(Icons.person_outlined, 'Person', 'Contact, colleague, or reference person', 'person'), + _TypeRow(Icons.place_outlined, 'Place', 'Location, venue, or place of interest', 'place'), + _TypeRow(Icons.checklist_outlined, 'List', 'Checklist or structured list', 'list'), + ], + ), +); +``` + +Each `_TypeRow` on tap: +1. `Navigator.pop(context)` +2. `context.push(Routes.noteNew, extra: {'noteType': selectedType})` + +### `lib/screens/notes/note_edit_screen.dart` changes +- Accept `noteType` from `GoRouterState.extra` or route parameter (default `'note'`) +- Display a read-only type badge chip in the app bar subtitle +- Pass `noteType` to `notes_api.createNote` / `notes_api.updateNote` + +--- + +## Section 7: Projects Screen + +### `lib/screens/projects/projects_screen.dart` + +``` +Scaffold + AppBar: 'Projects' + RefreshIndicator + ListView.builder + — projects from projectsProvider (sorted updated_at desc via API param) + — each item: _ProjectCard + FAB → push ProjectEditScreen() +``` + +`_ProjectCard` (inline widget): +- Title + status badge (`active` = green, `completed` = blue, `archived` = grey) +- Description (1 line, truncated) +- Goal text if present (italic, muted) +- Milestone progress: `"3 / 5 milestones done"` using milestone count from project data if available, otherwise omitted + +Tap → `context.push('/projects/${project.id}/tasks')` — `ProjectTasksScreen` reads `projectId` from the path parameter, unchanged except for the added edit button. + +### `lib/screens/projects/project_edit_screen.dart` + +Fields: Title (required), Description, Goal, Status dropdown (`active` / `completed` / `archived`). +Used for both create (no `projectId`) and edit (with `projectId`). +On save: POST `/api/projects` or PATCH `/api/projects/:id` via `projectsApi`. +On success: `ref.invalidate(projectsProvider)` then `Navigator.pop`. + +### `lib/screens/library/project_tasks_screen.dart` change +Add an edit `IconButton` in the `AppBar.actions`: +```dart +IconButton( + icon: const Icon(Icons.edit_outlined), + onPressed: () => context.push('/projects/$projectId/edit'), +) +``` + +--- + +## Section 8: Provider & API Wiring + +New providers to add to `lib/providers/`: + +```dart +// knowledge_api_provider.dart (or in api_client_provider.dart) +final knowledgeApiProvider = Provider((ref) => + KnowledgeApi(ref.watch(dioProvider))); + +final knowledgeRepositoryProvider = Provider((ref) => + KnowledgeRepository(ref.watch(knowledgeApiProvider))); +``` + +`projectsProvider` — add query parameters to the underlying `getProjects` call: +```dart +await api.getProjects(sort: 'updated_at', order: 'desc'); +``` + +--- + +## What This Does NOT Include + +- Voice I/O (separate spec) +- Backend parity pass (separate spec) +- Editing knowledge item type after creation +- Bulk delete / multi-select in Knowledge screen +- Knowledge graph view +- Milestone detail screen (projects show milestone count only)