'use client' import { useState, useEffect, useCallback, useMemo } from 'react' import { isAxiosError } from 'axios' import ReactMarkdown from 'react-markdown' import remarkGfm from 'remark-gfm' import { sourcesApi } from '@/lib/api/sources' import { insightsApi, SourceInsightResponse } from '@/lib/api/insights' import { transformationsApi } from '@/lib/api/transformations' import { embeddingApi } from '@/lib/api/embedding' import { SourceDetailResponse } from '@/lib/types/api' import { Transformation } from '@/lib/types/transformations' import { LoadingSpinner } from '@/components/common/LoadingSpinner' import { InlineEdit } from '@/components/common/InlineEdit' import { Button } from '@/components/ui/button' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' import { Badge } from '@/components/ui/badge' import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu' import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from '@/components/ui/alert-dialog' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select' import { Link as LinkIcon, Upload, AlignLeft, ExternalLink, Download, Copy, CheckCircle, Youtube, MoreVertical, Trash2, Sparkles, Plus, Lightbulb, Database, AlertCircle, MessageSquare, } from 'lucide-react' import { formatDistanceToNow } from 'date-fns' import { toast } from 'sonner' import { SourceInsightDialog } from '@/components/source/SourceInsightDialog' import { NotebookAssociations } from '@/components/source/NotebookAssociations' interface SourceDetailContentProps { sourceId: string showChatButton?: boolean onChatClick?: () => void onClose?: () => void } export function SourceDetailContent({ sourceId, showChatButton = false, onChatClick, onClose }: SourceDetailContentProps) { const [source, setSource] = useState(null) const [insights, setInsights] = useState([]) const [transformations, setTransformations] = useState([]) const [selectedTransformation, setSelectedTransformation] = useState('') const [loading, setLoading] = useState(true) const [loadingInsights, setLoadingInsights] = useState(false) const [creatingInsight, setCreatingInsight] = useState(false) const [error, setError] = useState(null) const [copied, setCopied] = useState(false) const [isEmbedding, setIsEmbedding] = useState(false) const [isDownloadingFile, setIsDownloadingFile] = useState(false) const [fileAvailable, setFileAvailable] = useState(null) const [selectedInsight, setSelectedInsight] = useState(null) const [insightToDelete, setInsightToDelete] = useState(null) const [deletingInsight, setDeletingInsight] = useState(false) const fetchSource = useCallback(async () => { try { setLoading(true) const data = await sourcesApi.get(sourceId) setSource(data) if (typeof data.file_available === 'boolean') { setFileAvailable(data.file_available) } else if (!data.asset?.file_path) { setFileAvailable(null) } else { setFileAvailable(null) } } catch (err) { console.error('Failed to fetch source:', err) setError('Failed to load source details') } finally { setLoading(false) } }, [sourceId]) const fetchInsights = useCallback(async () => { try { setLoadingInsights(true) const data = await insightsApi.listForSource(sourceId) setInsights(data) } catch (err) { console.error('Failed to fetch insights:', err) } finally { setLoadingInsights(false) } }, [sourceId]) const fetchTransformations = useCallback(async () => { try { const data = await transformationsApi.list() setTransformations(data) } catch (err) { console.error('Failed to fetch transformations:', err) } }, []) useEffect(() => { if (sourceId) { void fetchSource() void fetchInsights() void fetchTransformations() } }, [fetchInsights, fetchSource, fetchTransformations, sourceId]) const createInsight = async () => { if (!selectedTransformation) { toast.error('Please select a transformation') return } try { setCreatingInsight(true) await insightsApi.create(sourceId, { transformation_id: selectedTransformation }) toast.success('Insight created successfully') await fetchInsights() setSelectedTransformation('') } catch (err) { console.error('Failed to create insight:', err) toast.error('Failed to create insight') } finally { setCreatingInsight(false) } } const handleDeleteInsight = async (e?: React.MouseEvent) => { e?.preventDefault() if (!insightToDelete) return try { setDeletingInsight(true) await insightsApi.delete(insightToDelete) toast.success('Insight deleted successfully') setInsightToDelete(null) await fetchInsights() } catch (err) { console.error('Failed to delete insight:', err) toast.error('Failed to delete insight') } finally { setDeletingInsight(false) } } const handleUpdateTitle = async (title: string) => { if (!source || title === source.title) return try { await sourcesApi.update(sourceId, { title }) toast.success('Source title updated') setSource({ ...source, title }) } catch (err) { console.error('Failed to update source title:', err) toast.error('Failed to update source title') await fetchSource() } } const handleEmbedContent = async () => { if (!source) return try { setIsEmbedding(true) const response = await embeddingApi.embedContent(sourceId, 'source') toast.success(response.message) await fetchSource() } catch (err) { console.error('Failed to embed content:', err) toast.error('Failed to embed content') } finally { setIsEmbedding(false) } } const extractFilename = (pathOrUrl: string | undefined, fallback: string) => { if (!pathOrUrl) { return fallback } const segments = pathOrUrl.split(/[/\\]/) return segments.pop() || fallback } const parseContentDisposition = (header?: string | null) => { if (!header) { return null } const match = header.match(/filename\*?=([^;]+)/i) if (!match) { return null } const value = match[1].trim() if (value.toLowerCase().startsWith("utf-8''")) { return decodeURIComponent(value.slice(7)) } return value.replace(/^["']|["']$/g, '') } const handleDownloadFile = async () => { if (!source?.asset?.file_path || isDownloadingFile || fileAvailable === false) { return } try { setIsDownloadingFile(true) const response = await sourcesApi.downloadFile(source.id) const filenameFromHeader = parseContentDisposition( response.headers?.['content-disposition'] as string | undefined ) const fallbackName = extractFilename(source.asset.file_path, `source-${source.id}`) const filename = filenameFromHeader || fallbackName const blobUrl = window.URL.createObjectURL(response.data) const link = document.createElement('a') link.href = blobUrl link.download = filename document.body.appendChild(link) link.click() document.body.removeChild(link) window.URL.revokeObjectURL(blobUrl) setFileAvailable(true) toast.success('Download started') } catch (err) { console.error('Failed to download file:', err) if (isAxiosError(err) && err.response?.status === 404) { setFileAvailable(false) toast.error('Original file is no longer available on the server') } else { toast.error('Failed to download file') } } finally { setIsDownloadingFile(false) } } const getSourceIcon = () => { if (!source) return null if (source.asset?.url) return if (source.asset?.file_path) return return } const getSourceType = () => { if (!source) return 'unknown' if (source.asset?.url) return 'link' if (source.asset?.file_path) return 'file' return 'text' } const handleCopyUrl = useCallback(() => { if (source?.asset?.url) { navigator.clipboard.writeText(source.asset.url) setCopied(true) toast.success('URL copied to clipboard') setTimeout(() => setCopied(false), 2000) } }, [source]) const handleOpenExternal = useCallback(() => { if (source?.asset?.url) { window.open(source.asset.url, '_blank') } }, [source]) const getYouTubeVideoId = (url: string): string | null => { const patterns = [ /(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/)([^&\n?#]+)/, /youtube\.com\/watch\?.*v=([^&\n?#]+)/ ] for (const pattern of patterns) { const match = url.match(pattern) if (match) return match[1] } return null } const isYouTubeUrl = useMemo(() => { if (!source?.asset?.url) return false return !!(getYouTubeVideoId(source.asset.url)) }, [source?.asset?.url]) const youTubeVideoId = useMemo(() => { if (!source?.asset?.url) return null return getYouTubeVideoId(source.asset.url) }, [source?.asset?.url]) const handleDelete = async () => { if (!source) return if (confirm('Are you sure you want to delete this source?')) { try { await sourcesApi.delete(source.id) toast.success('Source deleted successfully') onClose?.() } catch (error) { console.error('Failed to delete source:', error) toast.error('Failed to delete source') } } } if (loading) { return (
) } if (error || !source) { return (

{error || 'Source not found'}

) } return (
{/* Header */}

Source ID: {source.id}

{getSourceIcon()} {getSourceType()} {/* Chat with source button - only in modal */} {showChatButton && onChatClick && ( )} {source.asset?.file_path && ( <> {fileAvailable === false ? 'File unavailable' : isDownloadingFile ? 'Preparing download…' : 'Download File'} )} {isEmbedding ? 'Embedding...' : source.embedded ? 'Already Embedded' : 'Embed Content'} Delete Source
{/* Tabs Content */}
Content Insights {insights.length > 0 && `(${insights.length})`} Details {isYouTubeUrl && } Content {source.asset?.url && !isYouTubeUrl && ( {source.asset.url} )} {isYouTubeUrl && youTubeVideoId && (