Feature/oss 312 notebook item counts (#175)

* fix: small issue where users cant change podcast segments

* feat: display source and note counts on notebook cards (OSS-312)

Add item counters to notebook listing page showing the number of sources
and notes in each notebook. Counts are displayed in a footer section with
FileText and StickyNote icons for visual consistency with ContextIndicator.

Backend changes:
- Add source_count and note_count to NotebookResponse model
- Update /notebooks endpoint to use SurrealDB graph traversal query
- Query: count(<-reference.in) for sources, count(<-artifact.in) for notes
- Update all notebook endpoints to include counts

Frontend changes:
- Add source_count and note_count to TypeScript NotebookResponse interface
- Add footer section to NotebookCard component
- Display counts with FileText and StickyNote icons (h-3 w-3)
- Use border-top separator and muted-foreground styling

Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* style: use colorful badges for notebook counts matching ContextIndicator

Update notebook card counts to use Badge components with primary color
styling instead of plain text, matching the visual style of the
ContextIndicator component in the chat window.

Changes:
- Replace plain text divs with Badge components
- Apply text-primary and border-primary/50 styling
- Use same spacing (gap-1.5, px-1.5, py-0.5) as ContextIndicator
- Remove bullet separator (not needed with badge layout)

Visual result matches the colorful badges shown in chat context.

Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Luis Novo 2025-10-19 16:03:36 -03:00 committed by GitHub
parent 1a67f1f912
commit df0986cee0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 85 additions and 20 deletions

View file

@ -24,6 +24,8 @@ class NotebookResponse(BaseModel):
archived: bool
created: str
updated: str
source_count: int
note_count: int
# Search models

View file

@ -18,22 +18,33 @@ async def get_notebooks(
):
"""Get all notebooks with optional filtering and ordering."""
try:
notebooks = await Notebook.get_all(order_by=order_by)
# Build the query with counts
query = f"""
SELECT *,
count(<-reference.in) as source_count,
count(<-artifact.in) as note_count
FROM notebook
ORDER BY {order_by}
"""
result = await repo_query(query)
# Filter by archived status if specified
if archived is not None:
notebooks = [nb for nb in notebooks if nb.archived == archived]
result = [nb for nb in result if nb.get("archived") == archived]
return [
NotebookResponse(
id=nb.id or "",
name=nb.name,
description=nb.description,
archived=nb.archived or False,
created=str(nb.created),
updated=str(nb.updated),
id=str(nb.get("id", "")),
name=nb.get("name", ""),
description=nb.get("description", ""),
archived=nb.get("archived", False),
created=str(nb.get("created", "")),
updated=str(nb.get("updated", "")),
source_count=nb.get("source_count", 0),
note_count=nb.get("note_count", 0),
)
for nb in notebooks
for nb in result
]
except Exception as e:
logger.error(f"Error fetching notebooks: {str(e)}")
@ -59,6 +70,8 @@ async def create_notebook(notebook: NotebookCreate):
archived=new_notebook.archived or False,
created=str(new_notebook.created),
updated=str(new_notebook.updated),
source_count=0, # New notebook has no sources
note_count=0, # New notebook has no notes
)
except InvalidInputError as e:
raise HTTPException(status_code=400, detail=str(e))
@ -73,17 +86,28 @@ async def create_notebook(notebook: NotebookCreate):
async def get_notebook(notebook_id: str):
"""Get a specific notebook by ID."""
try:
notebook = await Notebook.get(notebook_id)
if not notebook:
# Query with counts for single notebook
query = """
SELECT *,
count(<-reference.in) as source_count,
count(<-artifact.in) as note_count
FROM $notebook_id
"""
result = await repo_query(query, {"notebook_id": ensure_record_id(notebook_id)})
if not result:
raise HTTPException(status_code=404, detail="Notebook not found")
nb = result[0]
return NotebookResponse(
id=notebook.id or "",
name=notebook.name,
description=notebook.description,
archived=notebook.archived or False,
created=str(notebook.created),
updated=str(notebook.updated),
id=str(nb.get("id", "")),
name=nb.get("name", ""),
description=nb.get("description", ""),
archived=nb.get("archived", False),
created=str(nb.get("created", "")),
updated=str(nb.get("updated", "")),
source_count=nb.get("source_count", 0),
note_count=nb.get("note_count", 0),
)
except HTTPException:
raise
@ -112,6 +136,29 @@ async def update_notebook(notebook_id: str, notebook_update: NotebookUpdate):
await notebook.save()
# Query with counts after update
query = """
SELECT *,
count(<-reference.in) as source_count,
count(<-artifact.in) as note_count
FROM $notebook_id
"""
result = await repo_query(query, {"notebook_id": ensure_record_id(notebook_id)})
if result:
nb = result[0]
return NotebookResponse(
id=str(nb.get("id", "")),
name=nb.get("name", ""),
description=nb.get("description", ""),
archived=nb.get("archived", False),
created=str(nb.get("created", "")),
updated=str(nb.get("updated", "")),
source_count=nb.get("source_count", 0),
note_count=nb.get("note_count", 0),
)
# Fallback if query fails
return NotebookResponse(
id=notebook.id or "",
name=notebook.name,
@ -119,6 +166,8 @@ async def update_notebook(notebook_id: str, notebook_update: NotebookUpdate):
archived=notebook.archived or False,
created=str(notebook.created),
updated=str(notebook.updated),
source_count=0,
note_count=0,
)
except HTTPException:
raise