'use client' import { useState, useMemo, useRef, useCallback, useEffect } from 'react' import { SourceListResponse } 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, FileText, Link2, ChevronDown, Loader2 } from 'lucide-react' import { LoadingSpinner } from '@/components/common/LoadingSpinner' import { EmptyState } from '@/components/common/EmptyState' import { AddSourceDialog } from '@/components/sources/AddSourceDialog' import { AddExistingSourceDialog } from '@/components/sources/AddExistingSourceDialog' import { SourceCard } from '@/components/sources/SourceCard' import { useDeleteSource, useRetrySource, useRemoveSourceFromNotebook } from '@/lib/hooks/use-sources' import { ConfirmDialog } from '@/components/common/ConfirmDialog' import { useModalManager } from '@/lib/hooks/use-modal-manager' import { ContextMode } from '../[id]/page' import { CollapsibleColumn, createCollapseButton } from '@/components/notebooks/CollapsibleColumn' import { useNotebookColumnsStore } from '@/lib/stores/notebook-columns-store' interface SourcesColumnProps { sources?: SourceListResponse[] isLoading: boolean notebookId: string notebookName?: string onRefresh?: () => void contextSelections?: Record onContextModeChange?: (sourceId: string, mode: ContextMode) => void // Pagination props hasNextPage?: boolean isFetchingNextPage?: boolean fetchNextPage?: () => void } export function SourcesColumn({ sources, isLoading, notebookId, onRefresh, contextSelections, onContextModeChange, hasNextPage, isFetchingNextPage, fetchNextPage, }: SourcesColumnProps) { const [dropdownOpen, setDropdownOpen] = useState(false) const [addDialogOpen, setAddDialogOpen] = useState(false) const [addExistingDialogOpen, setAddExistingDialogOpen] = useState(false) const [deleteDialogOpen, setDeleteDialogOpen] = useState(false) const [sourceToDelete, setSourceToDelete] = useState(null) const [removeDialogOpen, setRemoveDialogOpen] = useState(false) const [sourceToRemove, setSourceToRemove] = useState(null) const { openModal } = useModalManager() const deleteSource = useDeleteSource() const retrySource = useRetrySource() const removeFromNotebook = useRemoveSourceFromNotebook() // Collapsible column state const { sourcesCollapsed, toggleSources } = useNotebookColumnsStore() const collapseButton = useMemo( () => createCollapseButton(toggleSources, 'Sources'), [toggleSources] ) // Scroll container ref for infinite scroll const scrollContainerRef = useRef(null) // Handle scroll for infinite loading const handleScroll = useCallback(() => { const container = scrollContainerRef.current if (!container || !hasNextPage || isFetchingNextPage || !fetchNextPage) return const { scrollTop, scrollHeight, clientHeight } = container // Load more when user scrolls within 200px of the bottom if (scrollHeight - scrollTop - clientHeight < 200) { fetchNextPage() } }, [hasNextPage, isFetchingNextPage, fetchNextPage]) // Attach scroll listener useEffect(() => { const container = scrollContainerRef.current if (!container) return container.addEventListener('scroll', handleScroll) return () => container.removeEventListener('scroll', handleScroll) }, [handleScroll]) const handleDeleteClick = (sourceId: string) => { setSourceToDelete(sourceId) setDeleteDialogOpen(true) } const handleDeleteConfirm = async () => { if (!sourceToDelete) return try { await deleteSource.mutateAsync(sourceToDelete) setDeleteDialogOpen(false) setSourceToDelete(null) onRefresh?.() } catch (error) { console.error('Failed to delete source:', error) } } const handleRemoveFromNotebook = (sourceId: string) => { setSourceToRemove(sourceId) setRemoveDialogOpen(true) } const handleRemoveConfirm = async () => { if (!sourceToRemove) return try { await removeFromNotebook.mutateAsync({ notebookId, sourceId: sourceToRemove }) setRemoveDialogOpen(false) setSourceToRemove(null) } catch (error) { console.error('Failed to remove source from notebook:', error) // Error toast is handled by the hook } } const handleRetry = async (sourceId: string) => { try { await retrySource.mutateAsync(sourceId) } catch (error) { console.error('Failed to retry source:', error) } } const handleSourceClick = (sourceId: string) => { openModal('source', sourceId) } return ( <>
Sources
{ setDropdownOpen(false); setAddDialogOpen(true); }}> Add New Source { setDropdownOpen(false); setAddExistingDialogOpen(true); }}> Add Existing Source {collapseButton}
{isLoading ? (
) : !sources || sources.length === 0 ? ( ) : (
{sources.map((source) => ( onContextModeChange(source.id, mode) : undefined } /> ))} {/* Loading indicator for infinite scroll */} {isFetchingNextPage && (
)}
)}
) }