open-notebook/frontend/src/app/(dashboard)/notebooks/[id]/page.tsx
Antonio Gatta 47db332d17 feat(notebooks): improve layout responsiveness
- Adjusted `div` class styles to add horizontal scrolling in smaller viewports (`overflow-x-auto`).
- Updated chat column element with additional padding and margin adjustments for better desktop layout.
2025-12-08 00:24:37 +01:00

224 lines
8.1 KiB
TypeScript

'use client'
import { useState, useEffect } from 'react'
import { useParams } from 'next/navigation'
import { AppShell } from '@/components/layout/AppShell'
import { NotebookHeader } from '../components/NotebookHeader'
import { SourcesColumn } from '../components/SourcesColumn'
import { NotesColumn } from '../components/NotesColumn'
import { ChatColumn } from '../components/ChatColumn'
import { useNotebook } from '@/lib/hooks/use-notebooks'
import { useSources } from '@/lib/hooks/use-sources'
import { useNotes } from '@/lib/hooks/use-notes'
import { LoadingSpinner } from '@/components/common/LoadingSpinner'
import { useNotebookColumnsStore } from '@/lib/stores/notebook-columns-store'
import { useIsDesktop } from '@/lib/hooks/use-media-query'
import { cn } from '@/lib/utils'
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { FileText, StickyNote, MessageSquare } from 'lucide-react'
export type ContextMode = 'off' | 'insights' | 'full'
export interface ContextSelections {
sources: Record<string, ContextMode>
notes: Record<string, ContextMode>
}
export default function NotebookPage() {
const params = useParams()
// Ensure the notebook ID is properly decoded from URL
const notebookId = decodeURIComponent(params.id as string)
const { data: notebook, isLoading: notebookLoading } = useNotebook(notebookId)
const { data: sources, isLoading: sourcesLoading, refetch: refetchSources } = useSources(notebookId)
const { data: notes, isLoading: notesLoading } = useNotes(notebookId)
// Get collapse states for dynamic layout
const { sourcesCollapsed, notesCollapsed } = useNotebookColumnsStore()
// Detect desktop to avoid double-mounting ChatColumn
const isDesktop = useIsDesktop()
// Mobile tab state (Sources, Notes, or Chat)
const [mobileActiveTab, setMobileActiveTab] = useState<'sources' | 'notes' | 'chat'>('chat')
// Context selection state
const [contextSelections, setContextSelections] = useState<ContextSelections>({
sources: {},
notes: {}
})
// Initialize default selections when sources/notes load
useEffect(() => {
if (sources && sources.length > 0) {
setContextSelections(prev => {
const newSourceSelections = { ...prev.sources }
sources.forEach(source => {
// Only set default if not already set
if (!(source.id in newSourceSelections)) {
// Default to 'insights' if has insights, otherwise 'full'
newSourceSelections[source.id] = source.insights_count > 0 ? 'insights' : 'full'
}
})
return { ...prev, sources: newSourceSelections }
})
}
}, [sources])
useEffect(() => {
if (notes && notes.length > 0) {
setContextSelections(prev => {
const newNoteSelections = { ...prev.notes }
notes.forEach(note => {
// Only set default if not already set
if (!(note.id in newNoteSelections)) {
// Notes default to 'full'
newNoteSelections[note.id] = 'full'
}
})
return { ...prev, notes: newNoteSelections }
})
}
}, [notes])
// Handler to update context selection
const handleContextModeChange = (itemId: string, mode: ContextMode, type: 'source' | 'note') => {
setContextSelections(prev => ({
...prev,
[type === 'source' ? 'sources' : 'notes']: {
...(type === 'source' ? prev.sources : prev.notes),
[itemId]: mode
}
}))
}
if (notebookLoading) {
return (
<div className="min-h-screen flex items-center justify-center">
<LoadingSpinner size="lg" />
</div>
)
}
if (!notebook) {
return (
<AppShell>
<div className="p-6">
<h1 className="text-2xl font-bold mb-4">Notebook Not Found</h1>
<p className="text-muted-foreground">The requested notebook could not be found.</p>
</div>
</AppShell>
)
}
return (
<AppShell>
<div className="flex flex-col flex-1 min-h-0">
<div className="flex-shrink-0 p-6 pb-0">
<NotebookHeader notebook={notebook} />
</div>
<div className="flex-1 p-6 pt-6 overflow-x-auto flex flex-col">
{/* Mobile: Tabbed interface - only render on mobile to avoid double-mounting */}
{!isDesktop && (
<>
<div className="lg:hidden mb-4">
<Tabs value={mobileActiveTab} onValueChange={(value) => setMobileActiveTab(value as 'sources' | 'notes' | 'chat')}>
<TabsList className="grid w-full grid-cols-3">
<TabsTrigger value="sources" className="gap-2">
<FileText className="h-4 w-4" />
Sources
</TabsTrigger>
<TabsTrigger value="notes" className="gap-2">
<StickyNote className="h-4 w-4" />
Notes
</TabsTrigger>
<TabsTrigger value="chat" className="gap-2">
<MessageSquare className="h-4 w-4" />
Chat
</TabsTrigger>
</TabsList>
</Tabs>
</div>
{/* Mobile: Show only active tab */}
<div className="flex-1 overflow-hidden lg:hidden">
{mobileActiveTab === 'sources' && (
<SourcesColumn
sources={sources}
isLoading={sourcesLoading}
notebookId={notebookId}
notebookName={notebook?.name}
onRefresh={refetchSources}
contextSelections={contextSelections.sources}
onContextModeChange={(sourceId, mode) => handleContextModeChange(sourceId, mode, 'source')}
/>
)}
{mobileActiveTab === 'notes' && (
<NotesColumn
notes={notes}
isLoading={notesLoading}
notebookId={notebookId}
contextSelections={contextSelections.notes}
onContextModeChange={(noteId, mode) => handleContextModeChange(noteId, mode, 'note')}
/>
)}
{mobileActiveTab === 'chat' && (
<ChatColumn
notebookId={notebookId}
contextSelections={contextSelections}
/>
)}
</div>
</>
)}
{/* Desktop: Collapsible columns layout */}
<div className={cn(
'hidden lg:flex h-full min-h-0 gap-6 transition-all duration-150',
'flex-row'
)}>
{/* Sources Column */}
<div className={cn(
'transition-all duration-150',
sourcesCollapsed ? 'w-12 flex-shrink-0' : 'flex-none basis-1/3'
)}>
<SourcesColumn
sources={sources}
isLoading={sourcesLoading}
notebookId={notebookId}
notebookName={notebook?.name}
onRefresh={refetchSources}
contextSelections={contextSelections.sources}
onContextModeChange={(sourceId, mode) => handleContextModeChange(sourceId, mode, 'source')}
/>
</div>
{/* Notes Column */}
<div className={cn(
'transition-all duration-150',
notesCollapsed ? 'w-12 flex-shrink-0' : 'flex-none basis-1/3'
)}>
<NotesColumn
notes={notes}
isLoading={notesLoading}
notebookId={notebookId}
contextSelections={contextSelections.notes}
onContextModeChange={(noteId, mode) => handleContextModeChange(noteId, mode, 'note')}
/>
</div>
{/* Chat Column - always expanded, takes remaining space */}
<div className="transition-all duration-150 flex-1 lg:pr-6 lg:-mr-6">
<ChatColumn
notebookId={notebookId}
contextSelections={contextSelections}
/>
</div>
</div>
</div>
</div>
</AppShell>
)
}