mirror of
https://github.com/lfnovo/open-notebook.git
synced 2026-05-05 07:08:28 +00:00
* 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>
230 lines
8 KiB
Python
230 lines
8 KiB
Python
from typing import List, Optional
|
|
|
|
from fastapi import APIRouter, HTTPException, Query
|
|
from loguru import logger
|
|
|
|
from api.models import NotebookCreate, NotebookResponse, NotebookUpdate
|
|
from open_notebook.database.repository import ensure_record_id, repo_query
|
|
from open_notebook.domain.notebook import Notebook
|
|
from open_notebook.exceptions import InvalidInputError
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
@router.get("/notebooks", response_model=List[NotebookResponse])
|
|
async def get_notebooks(
|
|
archived: Optional[bool] = Query(None, description="Filter by archived status"),
|
|
order_by: str = Query("updated desc", description="Order by field and direction"),
|
|
):
|
|
"""Get all notebooks with optional filtering and ordering."""
|
|
try:
|
|
# 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:
|
|
result = [nb for nb in result if nb.get("archived") == archived]
|
|
|
|
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),
|
|
)
|
|
for nb in result
|
|
]
|
|
except Exception as e:
|
|
logger.error(f"Error fetching notebooks: {str(e)}")
|
|
raise HTTPException(
|
|
status_code=500, detail=f"Error fetching notebooks: {str(e)}"
|
|
)
|
|
|
|
|
|
@router.post("/notebooks", response_model=NotebookResponse)
|
|
async def create_notebook(notebook: NotebookCreate):
|
|
"""Create a new notebook."""
|
|
try:
|
|
new_notebook = Notebook(
|
|
name=notebook.name,
|
|
description=notebook.description,
|
|
)
|
|
await new_notebook.save()
|
|
|
|
return NotebookResponse(
|
|
id=new_notebook.id or "",
|
|
name=new_notebook.name,
|
|
description=new_notebook.description,
|
|
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))
|
|
except Exception as e:
|
|
logger.error(f"Error creating notebook: {str(e)}")
|
|
raise HTTPException(
|
|
status_code=500, detail=f"Error creating notebook: {str(e)}"
|
|
)
|
|
|
|
|
|
@router.get("/notebooks/{notebook_id}", response_model=NotebookResponse)
|
|
async def get_notebook(notebook_id: str):
|
|
"""Get a specific notebook by ID."""
|
|
try:
|
|
# 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=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
|
|
except Exception as e:
|
|
logger.error(f"Error fetching notebook {notebook_id}: {str(e)}")
|
|
raise HTTPException(
|
|
status_code=500, detail=f"Error fetching notebook: {str(e)}"
|
|
)
|
|
|
|
|
|
@router.put("/notebooks/{notebook_id}", response_model=NotebookResponse)
|
|
async def update_notebook(notebook_id: str, notebook_update: NotebookUpdate):
|
|
"""Update a notebook."""
|
|
try:
|
|
notebook = await Notebook.get(notebook_id)
|
|
if not notebook:
|
|
raise HTTPException(status_code=404, detail="Notebook not found")
|
|
|
|
# Update only provided fields
|
|
if notebook_update.name is not None:
|
|
notebook.name = notebook_update.name
|
|
if notebook_update.description is not None:
|
|
notebook.description = notebook_update.description
|
|
if notebook_update.archived is not None:
|
|
notebook.archived = notebook_update.archived
|
|
|
|
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,
|
|
description=notebook.description,
|
|
archived=notebook.archived or False,
|
|
created=str(notebook.created),
|
|
updated=str(notebook.updated),
|
|
source_count=0,
|
|
note_count=0,
|
|
)
|
|
except HTTPException:
|
|
raise
|
|
except InvalidInputError as e:
|
|
raise HTTPException(status_code=400, detail=str(e))
|
|
except Exception as e:
|
|
logger.error(f"Error updating notebook {notebook_id}: {str(e)}")
|
|
raise HTTPException(
|
|
status_code=500, detail=f"Error updating notebook: {str(e)}"
|
|
)
|
|
|
|
|
|
@router.delete("/notebooks/{notebook_id}/sources/{source_id}")
|
|
async def remove_source_from_notebook(notebook_id: str, source_id: str):
|
|
"""Remove a source from a notebook (delete the reference)."""
|
|
try:
|
|
# Check if notebook exists
|
|
notebook = await Notebook.get(notebook_id)
|
|
if not notebook:
|
|
raise HTTPException(status_code=404, detail="Notebook not found")
|
|
|
|
# Delete the reference record linking source to notebook
|
|
await repo_query(
|
|
"DELETE FROM reference WHERE out = $notebook_id AND in = $source_id",
|
|
{
|
|
"notebook_id": ensure_record_id(notebook_id),
|
|
"source_id": ensure_record_id(source_id),
|
|
},
|
|
)
|
|
|
|
return {"message": "Source removed from notebook successfully"}
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(
|
|
f"Error removing source {source_id} from notebook {notebook_id}: {str(e)}"
|
|
)
|
|
raise HTTPException(
|
|
status_code=500, detail=f"Error removing source from notebook: {str(e)}"
|
|
)
|
|
|
|
|
|
@router.delete("/notebooks/{notebook_id}")
|
|
async def delete_notebook(notebook_id: str):
|
|
"""Delete a notebook."""
|
|
try:
|
|
notebook = await Notebook.get(notebook_id)
|
|
if not notebook:
|
|
raise HTTPException(status_code=404, detail="Notebook not found")
|
|
|
|
await notebook.delete()
|
|
|
|
return {"message": "Notebook deleted successfully"}
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Error deleting notebook {notebook_id}: {str(e)}")
|
|
raise HTTPException(
|
|
status_code=500, detail=f"Error deleting notebook: {str(e)}"
|
|
)
|