This repository has been archived on 2026-06-02. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
FabledApp/docs/superpowers/specs/2026-04-04-knowledge-view-design.md
bvandeusen ac4b2359a5 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 <noreply@anthropic.com>
2026-04-04 22:30:44 -04:00

456 lines
16 KiB
Markdown

# 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 `<uses-permission android:name="android.permission.INTERNET" />` is present
- [ ] `android/app/src/main/AndroidManifest.xml` — confirm `android:usesCleartextTraffic="true"` is set on the `<application>` 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<String> 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<String, dynamic> 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<dynamic>?)?.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>, int)> fetchIds({
String? noteType,
List<String> tags = const [],
String sort = 'modified',
String? q,
int limit = 50,
int offset = 0,
}) async {
final params = <String, dynamic>{
'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<String, dynamic>;
final ids = (data['ids'] as List<dynamic>).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<List<KnowledgeItem>> fetchBatch(List<int> ids) async {
if (ids.isEmpty) return [];
final response = await _dio.get(
'/api/knowledge/batch',
queryParameters: {'ids': ids.join(',')},
);
final data = response.data as Map<String, dynamic>;
return (data['items'] as List<dynamic>)
.map((e) => KnowledgeItem.fromJson(e as Map<String, dynamic>))
.toList();
}
/// Per-type counts for tab labels.
Future<Map<String, int>> fetchCounts({List<String> tags = const []}) async {
final params = <String, dynamic>{
if (tags.isNotEmpty) 'tags': tags.join(','),
};
final response = await _dio.get('/api/knowledge/counts', queryParameters: params);
return (response.data as Map<String, dynamic>).map(
(k, v) => MapEntry(k, (v as num).toInt()),
);
}
/// All tags for the current type filter.
Future<List<String>> fetchTags({String? noteType}) async {
final response = await _dio.get(
'/api/knowledge/tags',
queryParameters: {if (noteType != null) 'type': noteType},
);
return (response.data['tags'] as List<dynamic>).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<int> ids; // all fetched IDs so far
final Map<int, KnowledgeItem> items; // hydrated items
final int totalIds;
final bool isLoadingIds;
final bool isLoadingBatch;
final bool hasMore;
final String? noteType; // active type filter
final List<String> activeTags;
final String? searchQuery;
final Map<String, int> counts; // per-type counts
bool get canLoadMore => hasMore && !isLoadingIds;
// Items in ID order, only those hydrated
List<KnowledgeItem> get orderedItems =>
ids.where(items.containsKey).map((id) => items[id]!).toList();
}
class KnowledgeNotifier extends StateNotifier<KnowledgeState> {
// 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<void> loadMoreIds() { ... }
// Hydrate the next 12 un-hydrated IDs from the current list
Future<void> hydrateNext() { ... }
Future<void> refresh() { ... } // reset + reload
}
// Provider
final knowledgeProvider =
StateNotifierProvider<KnowledgeNotifier, KnowledgeState>(...);
```
**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)