* 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>
218 lines
8 KiB
TypeScript
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"
|
|
/>
|
|
</>
|
|
)
|
|
}
|