open-notebook/frontend/src/app/(dashboard)/notebooks/components/NotesColumn.tsx
Luis Novo 45a99831a9
Hide sources notes (#273)
* fix: add missing overflow wrapper to notebooks list page

Adds flex-1 overflow-y-auto wrapper to enable proper scrolling
when notebook list exceeds viewport height. Matches the layout
pattern used by all other dashboard pages.

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

* fix: reorder transformation routes to prevent dynamic route interception

Moved static routes (/transformations/execute and /transformations/default-prompt)
before dynamic routes (/transformations/{transformation_id}) to ensure FastAPI
matches them correctly. Previously, requests to static routes were incorrectly
captured by the dynamic route handler.

Fixes #250

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

* chore: bump to 1.2.1

* hide source and notes panel - fixes #193

* feat: improve layout for mobile views

* bump version to 1.2.2

* fix: address PR review feedback for collapsible columns

- Remove unused CollapseButton component from CollapsibleColumn.tsx
- Rename useCollapseButton to createCollapseButton (not a React hook)
- Move dialogs outside Card in SourcesColumn.tsx for consistency
- Add useMemo for collapseButton in both columns to prevent re-renders

* feat: support multiple sources

* fix: prevent ChatColumn double mounting on desktop

Add useIsDesktop hook to conditionally render mobile view only on
mobile screens. Previously, the mobile ChatColumn was hidden via CSS
on desktop but still mounted, causing duplicate hooks initialization
and redundant network requests.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-11-25 16:59:26 -03:00

218 lines
8 KiB
TypeScript

'use client'
import { useState, useMemo } from 'react'
import { NoteResponse } from '@/lib/types/api'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { Plus, StickyNote, Bot, User, MoreVertical, Trash2 } from 'lucide-react'
import { LoadingSpinner } from '@/components/common/LoadingSpinner'
import { EmptyState } from '@/components/common/EmptyState'
import { Badge } from '@/components/ui/badge'
import { NoteEditorDialog } from './NoteEditorDialog'
import { formatDistanceToNow } from 'date-fns'
import { ContextToggle } from '@/components/common/ContextToggle'
import { ContextMode } from '../[id]/page'
import { useDeleteNote } from '@/lib/hooks/use-notes'
import { ConfirmDialog } from '@/components/common/ConfirmDialog'
import { CollapsibleColumn, createCollapseButton } from '@/components/notebooks/CollapsibleColumn'
import { useNotebookColumnsStore } from '@/lib/stores/notebook-columns-store'
interface NotesColumnProps {
notes?: NoteResponse[]
isLoading: boolean
notebookId: string
contextSelections?: Record<string, ContextMode>
onContextModeChange?: (noteId: string, mode: ContextMode) => void
}
export function NotesColumn({
notes,
isLoading,
notebookId,
contextSelections,
onContextModeChange
}: NotesColumnProps) {
const [showAddDialog, setShowAddDialog] = useState(false)
const [editingNote, setEditingNote] = useState<NoteResponse | null>(null)
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false)
const [noteToDelete, setNoteToDelete] = useState<string | null>(null)
const deleteNote = useDeleteNote()
// Collapsible column state
const { notesCollapsed, toggleNotes } = useNotebookColumnsStore()
const collapseButton = useMemo(
() => createCollapseButton(toggleNotes, 'Notes'),
[toggleNotes]
)
const handleDeleteClick = (noteId: string) => {
setNoteToDelete(noteId)
setDeleteDialogOpen(true)
}
const handleDeleteConfirm = async () => {
if (!noteToDelete) return
try {
await deleteNote.mutateAsync(noteToDelete)
setDeleteDialogOpen(false)
setNoteToDelete(null)
} catch (error) {
console.error('Failed to delete note:', error)
}
}
return (
<>
<CollapsibleColumn
isCollapsed={notesCollapsed}
onToggle={toggleNotes}
collapsedIcon={StickyNote}
collapsedLabel="Notes"
>
<Card className="h-full flex flex-col flex-1 overflow-hidden">
<CardHeader className="pb-3 flex-shrink-0">
<div className="flex items-center justify-between gap-2">
<CardTitle className="text-lg">Notes</CardTitle>
<div className="flex items-center gap-2">
<Button
size="sm"
onClick={() => {
setEditingNote(null)
setShowAddDialog(true)
}}
>
<Plus className="h-4 w-4 mr-2" />
Write Note
</Button>
{collapseButton}
</div>
</div>
</CardHeader>
<CardContent className="flex-1 overflow-y-auto min-h-0">
{isLoading ? (
<div className="flex items-center justify-center py-8">
<LoadingSpinner />
</div>
) : !notes || notes.length === 0 ? (
<EmptyState
icon={StickyNote}
title="No notes yet"
description="Create your first note to capture insights and observations."
/>
) : (
<div className="space-y-3">
{notes.map((note) => (
<div
key={note.id}
className="p-3 border rounded-lg card-hover group relative cursor-pointer"
onClick={() => setEditingNote(note)}
>
<div className="flex items-start justify-between mb-2">
<div className="flex items-center gap-2">
{note.note_type === 'ai' ? (
<Bot className="h-4 w-4 text-primary" />
) : (
<User className="h-4 w-4 text-muted-foreground" />
)}
<Badge variant="secondary" className="text-xs">
{note.note_type === 'ai' ? 'AI Generated' : 'Human'}
</Badge>
</div>
<div className="flex items-center gap-2">
<span className="text-xs text-muted-foreground">
{formatDistanceToNow(new Date(note.updated), { addSuffix: true })}
</span>
{/* Context toggle - only show if handler provided */}
{onContextModeChange && contextSelections?.[note.id] && (
<div onClick={(event) => event.stopPropagation()}>
<ContextToggle
mode={contextSelections[note.id]}
hasInsights={false}
onChange={(mode) => onContextModeChange(note.id, mode)}
/>
</div>
)}
{/* Ellipsis menu for delete action */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="sm"
className="h-8 w-8 p-0 opacity-0 group-hover:opacity-100 transition-opacity"
onClick={(e) => e.stopPropagation()}
>
<MoreVertical className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-48">
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation()
handleDeleteClick(note.id)
}}
className="text-red-600 focus:text-red-600"
>
<Trash2 className="h-4 w-4 mr-2" />
Delete Note
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
{note.title && (
<h4 className="text-sm font-medium mb-2">{note.title}</h4>
)}
{note.content && (
<p className="text-sm text-muted-foreground line-clamp-3">
{note.content}
</p>
)}
</div>
))}
</div>
)}
</CardContent>
</Card>
</CollapsibleColumn>
<NoteEditorDialog
open={showAddDialog || Boolean(editingNote)}
onOpenChange={(open) => {
if (!open) {
setShowAddDialog(false)
setEditingNote(null)
} else {
setShowAddDialog(true)
}
}}
notebookId={notebookId}
note={editingNote ?? undefined}
/>
<ConfirmDialog
open={deleteDialogOpen}
onOpenChange={setDeleteDialogOpen}
title="Delete Note"
description="Are you sure you want to delete this note? This action cannot be undone."
confirmText="Delete"
onConfirm={handleDeleteConfirm}
isLoading={deleteNote.isPending}
confirmVariant="destructive"
/>
</>
)
}