'use client' import { useState, useEffect, useMemo, useCallback } from 'react' import { useDebounce } from 'use-debounce' import { Search, Link2, LoaderIcon, FileText, Link as LinkIcon, Upload } from 'lucide-react' import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogFooter, } from '@/components/ui/dialog' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Checkbox } from '@/components/ui/checkbox' import { Badge } from '@/components/ui/badge' import { ScrollArea } from '@/components/ui/scroll-area' import { searchApi } from '@/lib/api/search' import { sourcesApi } from '@/lib/api/sources' import { useSources, useAddSourcesToNotebook } from '@/lib/hooks/use-sources' import { SourceListResponse } from '@/lib/types/api' interface AddExistingSourceDialogProps { open: boolean onOpenChange: (open: boolean) => void notebookId: string onSuccess?: () => void } export function AddExistingSourceDialog({ open, onOpenChange, notebookId, onSuccess, }: AddExistingSourceDialogProps) { const [searchQuery, setSearchQuery] = useState('') const [debouncedSearchQuery] = useDebounce(searchQuery, 300) const [selectedSourceIds, setSelectedSourceIds] = useState([]) const [allSources, setAllSources] = useState([]) const [filteredSources, setFilteredSources] = useState([]) const [isSearching, setIsSearching] = useState(false) // Get sources already in this notebook const { data: currentNotebookSources } = useSources(notebookId) const currentSourceIds = useMemo( () => new Set(currentNotebookSources?.map(s => s.id) || []), [currentNotebookSources] ) const addSources = useAddSourcesToNotebook() const loadAllSources = useCallback(async () => { try { setIsSearching(true) // Use sources API directly to get all sources (max 100 per API limit) const sources = await sourcesApi.list({ limit: 100, offset: 0, sort_by: 'created', sort_order: 'desc', }) setAllSources(sources) setFilteredSources(sources) } catch (error) { console.error('Error loading sources:', error) } finally { setIsSearching(false) } }, []) const performSearch = useCallback(async () => { if (!debouncedSearchQuery.trim()) { // Empty query - show all sources setFilteredSources(allSources) setIsSearching(false) return } try { setIsSearching(true) const response = await searchApi.search({ query: debouncedSearchQuery, type: 'text', search_sources: true, search_notes: false, limit: 100, minimum_score: 0.01, }) // Since we set search_sources=true and search_notes=false, // the API only returns sources, no need to filter const sources = response.results.map(r => ({ id: r.parent_id, title: r.title || 'Untitled', topics: [], asset: null, embedded: false, embedded_chunks: 0, insights_count: 0, created: r.created, updated: r.updated, })) as SourceListResponse[] setFilteredSources(sources) } catch (error) { console.error('Error searching sources:', error) // On error, fall back to showing all sources setFilteredSources(allSources) } finally { setIsSearching(false) } }, [debouncedSearchQuery, allSources]) // Load all sources initially useEffect(() => { if (open) { loadAllSources() } }, [open, loadAllSources]) // Filter sources when search query changes useEffect(() => { if (!debouncedSearchQuery) { setFilteredSources(allSources) setIsSearching(false) return } performSearch() }, [debouncedSearchQuery, allSources, performSearch]) const handleToggleSource = (sourceId: string) => { setSelectedSourceIds(prev => prev.includes(sourceId) ? prev.filter(id => id !== sourceId) : [...prev, sourceId] ) } const handleAddSelected = async () => { if (selectedSourceIds.length === 0) return try { await addSources.mutateAsync({ notebookId, sourceIds: selectedSourceIds, }) // Reset state setSelectedSourceIds([]) setSearchQuery('') onOpenChange(false) onSuccess?.() } catch (error) { // Error handled by the hook's onError console.error('Error adding sources:', error) } } const getSourceIcon = (source: SourceListResponse) => { // Derive type from asset if (source.asset?.url) { return } if (source.asset?.file_path) { return } return } const formatDate = (dateString: string) => { try { return new Date(dateString).toLocaleDateString() } catch { return '' } } return ( Add Existing Sources Search and select existing sources to add to this notebook
{/* Search Input */}
setSearchQuery(e.target.value)} className="pl-10" /> {isSearching && ( )}
{/* Source List */} {isSearching && filteredSources.length === 0 ? (

Loading sources...

) : filteredSources.length === 0 ? (

No sources found

) : (
{filteredSources.map((source) => { const isAlreadyLinked = currentSourceIds.has(source.id) const isSelected = selectedSourceIds.includes(source.id) return (
handleToggleSource(source.id)} disabled={isAlreadyLinked} className="mt-1" />
{getSourceIcon(source)}

{source.title}

{isAlreadyLinked && ( Linked )}

Added {formatDate(source.created)}

) })}
)}
{/* Truncation Warning */} {allSources.length >= 100 && !debouncedSearchQuery && (
Showing first 100 sources. Use the Search feature to find specific sources.
)} {/* Selection Summary */} {selectedSourceIds.length > 0 && (
{selectedSourceIds.length} source{selectedSourceIds.length > 1 ? 's' : ''} selected
)}
) }