'use client' import React, { useState, useEffect } from 'react' import { SourceListResponse } from '@/lib/types/api' import { Badge } from '@/components/ui/badge' import { Card, CardContent } from '@/components/ui/card' import { Button } from '@/components/ui/button' import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, DropdownMenuSeparator } from '@/components/ui/dropdown-menu' import { FileText, ExternalLink, Upload, MoreVertical, Trash2, RefreshCw, Clock, CheckCircle, AlertTriangle, Loader2, Unlink } from 'lucide-react' import { useSourceStatus } from '@/lib/hooks/use-sources' import { cn } from '@/lib/utils' import { ContextToggle } from '@/components/common/ContextToggle' import { ContextMode } from '@/app/(dashboard)/notebooks/[id]/page' interface SourceCardProps { source: SourceListResponse onDelete?: (sourceId: string) => void onRetry?: (sourceId: string) => void onRemoveFromNotebook?: (sourceId: string) => void onClick?: (sourceId: string) => void onRefresh?: () => void className?: string showRemoveFromNotebook?: boolean contextMode?: ContextMode onContextModeChange?: (mode: ContextMode) => void } const SOURCE_TYPE_ICONS = { link: ExternalLink, upload: Upload, text: FileText, } as const const STATUS_CONFIG = { new: { icon: Clock, color: 'text-blue-600', bgColor: 'bg-blue-50', borderColor: 'border-blue-200', label: 'Processing', description: 'Preparing to process' }, queued: { icon: Clock, color: 'text-blue-600', bgColor: 'bg-blue-50', borderColor: 'border-blue-200', label: 'Queued', description: 'Waiting to be processed' }, running: { icon: Loader2, color: 'text-blue-600', bgColor: 'bg-blue-50', borderColor: 'border-blue-200', label: 'Processing', description: 'Being processed' }, completed: { icon: CheckCircle, color: 'text-green-600', bgColor: 'bg-green-50', borderColor: 'border-green-200', label: 'Completed', description: 'Successfully processed' }, failed: { icon: AlertTriangle, color: 'text-red-600', bgColor: 'bg-red-50', borderColor: 'border-red-200', label: 'Failed', description: 'Processing failed' } } as const type SourceStatus = keyof typeof STATUS_CONFIG function isSourceStatus(status: unknown): status is SourceStatus { return typeof status === 'string' && status in STATUS_CONFIG } function getSourceType(source: SourceListResponse): 'link' | 'upload' | 'text' { // Determine type based on asset information if (source.asset?.url) return 'link' if (source.asset?.file_path) return 'upload' return 'text' } export function SourceCard({ source, onDelete, onRetry, onRemoveFromNotebook, onClick, onRefresh, className, showRemoveFromNotebook = false, contextMode, onContextModeChange }: SourceCardProps) { // Only fetch status for sources that might have async processing const sourceWithStatus = source as SourceListResponse & { command_id?: string; status?: string } // Track processing state to continue polling until we detect completion const [wasProcessing, setWasProcessing] = useState(false) const shouldFetchStatus = !!sourceWithStatus.command_id || sourceWithStatus.status === 'new' || sourceWithStatus.status === 'queued' || sourceWithStatus.status === 'running' || wasProcessing // Keep polling if we were processing to catch the completion const { data: statusData, isLoading: statusLoading } = useSourceStatus( source.id, shouldFetchStatus ) // Determine current status // If source has a command_id but no status, treat as "new" (just created) const rawStatus = statusData?.status || sourceWithStatus.status const currentStatus: SourceStatus = isSourceStatus(rawStatus) ? rawStatus : (sourceWithStatus.command_id ? 'new' : 'completed') // Track processing state and detect completion useEffect(() => { const currentStatusFromData = statusData?.status || sourceWithStatus.status // If we're currently processing, mark that we were processing if (currentStatusFromData === 'new' || currentStatusFromData === 'running' || currentStatusFromData === 'queued') { setWasProcessing(true) } // If we were processing and now completed/failed, trigger refresh and stop polling if (wasProcessing && (currentStatusFromData === 'completed' || currentStatusFromData === 'failed')) { setWasProcessing(false) // Stop polling if (onRefresh) { setTimeout(() => onRefresh(), 500) // Small delay to ensure API is updated } } }, [statusData, sourceWithStatus.status, wasProcessing, onRefresh, source.id]) const statusConfig = STATUS_CONFIG[currentStatus] || STATUS_CONFIG.completed const StatusIcon = statusConfig.icon const sourceType = getSourceType(source) const SourceTypeIcon = SOURCE_TYPE_ICONS[sourceType] const title = source.title || 'Untitled Source' const handleRetry = () => { if (onRetry) { onRetry(source.id) } } const handleDelete = () => { if (onDelete) { onDelete(source.id) } } const handleRemoveFromNotebook = () => { if (onRemoveFromNotebook) { onRemoveFromNotebook(source.id) } } const handleCardClick = () => { if (onClick) { onClick(source.id) } } const isProcessing: boolean = currentStatus === 'new' || currentStatus === 'running' || currentStatus === 'queued' const isFailed: boolean = currentStatus === 'failed' const isCompleted: boolean = currentStatus === 'completed' return ( {/* Header with status indicator */}
{/* Status badge - only show if not completed */} {!isCompleted && (
{statusLoading && shouldFetchStatus ? 'Checking...' : statusConfig.label}
{/* Source type indicator */}
{sourceType}
)} {/* Title */}

{title}

{/* Processing message for active statuses */} {statusData?.message && (isProcessing || isFailed) && (

{statusData.message}

)} {/* Metadata badges */}
{/* Source type badge */} {sourceType} {isCompleted && source.insights_count > 0 && ( {source.insights_count} insights )} {source.topics && source.topics.length > 0 && isCompleted && ( <> {source.topics.slice(0, 2).map((topic, index) => ( {topic} ))} {source.topics.length > 2 && ( +{source.topics.length - 2} )} )}
{/* Context toggle and actions */}
{/* Context toggle - only show if handler provided */} {onContextModeChange && contextMode && ( 0} onChange={onContextModeChange} /> )} {/* Actions dropdown */} {showRemoveFromNotebook && ( <> { e.stopPropagation() handleRemoveFromNotebook() }} disabled={!onRemoveFromNotebook} > Remove from Notebook )} {isFailed && ( <> { e.stopPropagation() handleRetry() }} disabled={!onRetry} > Retry Processing )} { e.stopPropagation() handleDelete() }} disabled={!onDelete} className="text-red-600 focus:text-red-600" > Delete Source
{/* eslint-disable-next-line @typescript-eslint/no-explicit-any */} {(isFailed as any) && (
)} {/* Processing progress indicator */} {isProcessing && statusData?.processing_info?.progress && (
Progress {Math.round(statusData.processing_info.progress as number)}%
)} ) }