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:
parent
1a67f1f912
commit
df0986cee0
5 changed files with 85 additions and 20 deletions
|
|
@ -24,6 +24,8 @@ class NotebookResponse(BaseModel):
|
||||||
archived: bool
|
archived: bool
|
||||||
created: str
|
created: str
|
||||||
updated: str
|
updated: str
|
||||||
|
source_count: int
|
||||||
|
note_count: int
|
||||||
|
|
||||||
|
|
||||||
# Search models
|
# Search models
|
||||||
|
|
|
||||||
|
|
@ -18,22 +18,33 @@ async def get_notebooks(
|
||||||
):
|
):
|
||||||
"""Get all notebooks with optional filtering and ordering."""
|
"""Get all notebooks with optional filtering and ordering."""
|
||||||
try:
|
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
|
# Filter by archived status if specified
|
||||||
if archived is not None:
|
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 [
|
return [
|
||||||
NotebookResponse(
|
NotebookResponse(
|
||||||
id=nb.id or "",
|
id=str(nb.get("id", "")),
|
||||||
name=nb.name,
|
name=nb.get("name", ""),
|
||||||
description=nb.description,
|
description=nb.get("description", ""),
|
||||||
archived=nb.archived or False,
|
archived=nb.get("archived", False),
|
||||||
created=str(nb.created),
|
created=str(nb.get("created", "")),
|
||||||
updated=str(nb.updated),
|
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:
|
except Exception as e:
|
||||||
logger.error(f"Error fetching notebooks: {str(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,
|
archived=new_notebook.archived or False,
|
||||||
created=str(new_notebook.created),
|
created=str(new_notebook.created),
|
||||||
updated=str(new_notebook.updated),
|
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:
|
except InvalidInputError as e:
|
||||||
raise HTTPException(status_code=400, detail=str(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):
|
async def get_notebook(notebook_id: str):
|
||||||
"""Get a specific notebook by ID."""
|
"""Get a specific notebook by ID."""
|
||||||
try:
|
try:
|
||||||
notebook = await Notebook.get(notebook_id)
|
# Query with counts for single notebook
|
||||||
if not 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")
|
raise HTTPException(status_code=404, detail="Notebook not found")
|
||||||
|
|
||||||
|
nb = result[0]
|
||||||
return NotebookResponse(
|
return NotebookResponse(
|
||||||
id=notebook.id or "",
|
id=str(nb.get("id", "")),
|
||||||
name=notebook.name,
|
name=nb.get("name", ""),
|
||||||
description=notebook.description,
|
description=nb.get("description", ""),
|
||||||
archived=notebook.archived or False,
|
archived=nb.get("archived", False),
|
||||||
created=str(notebook.created),
|
created=str(nb.get("created", "")),
|
||||||
updated=str(notebook.updated),
|
updated=str(nb.get("updated", "")),
|
||||||
|
source_count=nb.get("source_count", 0),
|
||||||
|
note_count=nb.get("note_count", 0),
|
||||||
)
|
)
|
||||||
except HTTPException:
|
except HTTPException:
|
||||||
raise
|
raise
|
||||||
|
|
@ -112,6 +136,29 @@ async def update_notebook(notebook_id: str, notebook_update: NotebookUpdate):
|
||||||
|
|
||||||
await notebook.save()
|
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(
|
return NotebookResponse(
|
||||||
id=notebook.id or "",
|
id=notebook.id or "",
|
||||||
name=notebook.name,
|
name=notebook.name,
|
||||||
|
|
@ -119,6 +166,8 @@ async def update_notebook(notebook_id: str, notebook_update: NotebookUpdate):
|
||||||
archived=notebook.archived or False,
|
archived=notebook.archived or False,
|
||||||
created=str(notebook.created),
|
created=str(notebook.created),
|
||||||
updated=str(notebook.updated),
|
updated=str(notebook.updated),
|
||||||
|
source_count=0,
|
||||||
|
note_count=0,
|
||||||
)
|
)
|
||||||
except HTTPException:
|
except HTTPException:
|
||||||
raise
|
raise
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import { NotebookResponse } from '@/lib/types/api'
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
import { MoreHorizontal, Archive, ArchiveRestore, Trash2 } from 'lucide-react'
|
import { MoreHorizontal, Archive, ArchiveRestore, Trash2, FileText, StickyNote } from 'lucide-react'
|
||||||
import { formatDistanceToNow } from 'date-fns'
|
import { formatDistanceToNow } from 'date-fns'
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
|
|
@ -108,10 +108,22 @@ export function NotebookCard({ notebook }: NotebookCardProps) {
|
||||||
<CardDescription className="line-clamp-2 text-sm">
|
<CardDescription className="line-clamp-2 text-sm">
|
||||||
{notebook.description || 'No description'}
|
{notebook.description || 'No description'}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
|
|
||||||
<div className="mt-3 text-xs text-muted-foreground">
|
<div className="mt-3 text-xs text-muted-foreground">
|
||||||
Updated {formatDistanceToNow(new Date(notebook.updated), { addSuffix: true })}
|
Updated {formatDistanceToNow(new Date(notebook.updated), { addSuffix: true })}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Item counts footer */}
|
||||||
|
<div className="mt-3 flex items-center gap-1.5 border-t pt-3">
|
||||||
|
<Badge variant="outline" className="text-xs flex items-center gap-1 px-1.5 py-0.5 text-primary border-primary/50">
|
||||||
|
<FileText className="h-3 w-3" />
|
||||||
|
<span>{notebook.source_count}</span>
|
||||||
|
</Badge>
|
||||||
|
<Badge variant="outline" className="text-xs flex items-center gap-1 px-1.5 py-0.5 text-primary border-primary/50">
|
||||||
|
<StickyNote className="h-3 w-3" />
|
||||||
|
<span>{notebook.note_count}</span>
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -229,7 +229,7 @@ export function EpisodeProfileFormDialog({
|
||||||
type="number"
|
type="number"
|
||||||
min={3}
|
min={3}
|
||||||
max={20}
|
max={20}
|
||||||
{...register('num_segments')}
|
{...register('num_segments', { valueAsNumber: true })}
|
||||||
/>
|
/>
|
||||||
{errors.num_segments ? (
|
{errors.num_segments ? (
|
||||||
<p className="text-xs text-red-600">{errors.num_segments.message}</p>
|
<p className="text-xs text-red-600">{errors.num_segments.message}</p>
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@ export interface NotebookResponse {
|
||||||
archived: boolean
|
archived: boolean
|
||||||
created: string
|
created: string
|
||||||
updated: string
|
updated: string
|
||||||
|
source_count: number
|
||||||
|
note_count: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface NoteResponse {
|
export interface NoteResponse {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue