feat: Knowledge view + entity types (People, Places, Lists)

Data model:
- Migration 0036: adds note_type TEXT (default 'note') and metadata JSONB
  to the notes table; index on note_type
- Note model: entity_type property, note_type/metadata in to_dict()
- create_note() accepts note_type and metadata params

Backend:
- /api/knowledge — unified paginated endpoint: type/tag/sort/q filters,
  semantic search via embeddings, excludes tasks
- /api/knowledge/tags — distinct tags across knowledge objects
- New LLM tools: create_person, create_place, create_list, add_to_list,
  clear_checked_items — all wired into execute_tool()
- People and places auto-injected as compact summary into LLM system prompt

Frontend:
- KnowledgeView replaces HomeView at /; left filter panel (type+tag),
  toolbar (search, sort, graph toggle), card grid with type-aware cards
  (indigo=note, emerald=person, amber=place, sky=list), load-more pagination
- Today bar: upcoming events, overdue task count, Briefing/Chat links
- Floating mini-chat sticky to bottom: creates/continues a conversation
  inline, message history expands upward, close button ends session
- Graph panel: toggles as a 420px right panel at full viewport width
- AppHeader: Knowledge, Chat, Briefing, Calendar, Tasks, Projects
- Router: / → KnowledgeView; /knowledge redirect; HomeView import removed

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-31 18:01:03 -04:00
parent 425d307180
commit 80f30b705d
11 changed files with 1593 additions and 17 deletions
+12
View File
@@ -51,6 +51,10 @@ class Note(Base, TimestampMixin):
recurrence_next_spawn_at: Mapped[datetime | None] = mapped_column(
DateTime(timezone=True), nullable=True
)
# Entity type — 'note' (default), 'person', 'place', 'list'
note_type: Mapped[str] = mapped_column(Text, default="note", server_default="note")
# Structured metadata for entity types (person/place/list)
metadata: Mapped[dict | None] = mapped_column(JSONB, nullable=True)
__table_args__ = (
Index("ix_notes_tags", "tags", postgresql_using="gin"),
@@ -59,12 +63,18 @@ class Note(Base, TimestampMixin):
Index("ix_notes_user_id", "user_id"),
Index("ix_notes_project_id", "project_id"),
Index("ix_notes_milestone_id", "milestone_id"),
Index("ix_notes_note_type", "note_type"),
)
@property
def is_task(self) -> bool:
return self.status is not None
@property
def entity_type(self) -> str:
"""Normalised type: 'note', 'person', 'place', or 'list'."""
return self.note_type or "note"
def to_dict(self) -> dict:
return {
"id": self.id,
@@ -86,6 +96,8 @@ class Note(Base, TimestampMixin):
else None
),
"is_task": self.is_task,
"note_type": self.entity_type,
"metadata": self.metadata or {},
"created_at": self.created_at.isoformat(),
"updated_at": self.updated_at.isoformat(),
}