"use client"; import { useEffect, useState } from "react"; import Link from "next/link"; import { useRouter } from "next/navigation"; import { ActionButton } from "@/components/ui/action-button"; import ActionConfirmingDialog from "@/components/ui/action-confirming-dialog"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { FullPageSpinner } from "@/components/ui/full-page-spinner"; import { Progress } from "@/components/ui/progress"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { useDeleteImportSession, useImportSessionResults, useImportSessionStats, usePauseImportSession, useResumeImportSession, } from "@/lib/hooks/useImportSessions"; import { useTranslation } from "@/lib/i18n/client"; import { formatDistanceToNow } from "date-fns"; import { AlertCircle, ArrowLeft, CheckCircle2, Clock, ExternalLink, FileText, Globe, Loader2, Paperclip, Pause, Play, Trash2, Upload, } from "lucide-react"; import { useInView } from "react-intersection-observer"; import type { ZImportSessionStatus } from "@karakeep/shared/types/importSessions"; import { switchCase } from "@karakeep/shared/utils/switch"; type FilterType = | "all" | "accepted" | "rejected" | "skipped_duplicate" | "pending"; type SimpleTFunction = ( key: string, options?: Record, ) => string; interface ImportSessionResultItem { id: string; title: string | null; url: string | null; content: string | null; type: string; status: string; result: string | null; resultReason: string | null; resultBookmarkId: string | null; } function getStatusColor(status: string) { switch (status) { case "staging": return "bg-purple-500/10 text-purple-700 dark:text-purple-400"; case "pending": return "bg-muted text-muted-foreground"; case "running": return "bg-blue-500/10 text-blue-700 dark:text-blue-400"; case "paused": return "bg-yellow-500/10 text-yellow-700 dark:text-yellow-400"; case "completed": return "bg-green-500/10 text-green-700 dark:text-green-400"; case "failed": return "bg-destructive/10 text-destructive"; default: return "bg-muted text-muted-foreground"; } } function getStatusIcon(status: string) { switch (status) { case "staging": return ; case "pending": return ; case "running": return ; case "paused": return ; case "completed": return ; case "failed": return ; default: return ; } } function getResultBadge( status: string, result: string | null, t: (key: string) => string, ) { if (status === "pending") { return ( {t("settings.import_sessions.detail.result_pending")} ); } if (status === "processing") { return ( {t("settings.import_sessions.detail.result_processing")} ); } switch (result) { case "accepted": return ( {t("settings.import_sessions.detail.result_accepted")} ); case "rejected": return ( {t("settings.import_sessions.detail.result_rejected")} ); case "skipped_duplicate": return ( {t("settings.import_sessions.detail.result_skipped_duplicate")} ); default: return ( ); } } function getTypeIcon(type: string) { switch (type) { case "link": return ; case "text": return ; case "asset": return ; default: return null; } } function getTypeLabel(type: string, t: SimpleTFunction) { switch (type) { case "link": return t("common.bookmark_types.link"); case "text": return t("common.bookmark_types.text"); case "asset": return t("common.bookmark_types.media"); default: return type; } } function getTitleDisplay( item: { title: string | null; url: string | null; content: string | null; type: string; }, noTitleLabel: string, ) { if (item.title) { return item.title; } if (item.type === "text" && item.content) { return item.content.length > 80 ? item.content.substring(0, 80) + "…" : item.content; } if (item.url) { try { const url = new URL(item.url); const display = url.hostname + url.pathname; return display.length > 60 ? display.substring(0, 60) + "…" : display; } catch { return item.url.length > 60 ? item.url.substring(0, 60) + "…" : item.url; } } return noTitleLabel; } export default function ImportSessionDetail({ sessionId, }: { sessionId: string; }) { const { t: tRaw } = useTranslation(); const t = tRaw as SimpleTFunction; const router = useRouter(); const [filter, setFilter] = useState("all"); const { data: stats, isLoading: isStatsLoading } = useImportSessionStats(sessionId); const { data: resultsData, isLoading: isResultsLoading, fetchNextPage, hasNextPage, isFetchingNextPage, } = useImportSessionResults(sessionId, filter); const deleteSession = useDeleteImportSession(); const pauseSession = usePauseImportSession(); const resumeSession = useResumeImportSession(); const { ref: loadMoreRef, inView: loadMoreInView } = useInView(); useEffect(() => { if (loadMoreInView && hasNextPage && !isFetchingNextPage) { fetchNextPage(); } }, [fetchNextPage, hasNextPage, isFetchingNextPage, loadMoreInView]); if (isStatsLoading) { return ; } if (!stats) { return null; } const items: ImportSessionResultItem[] = resultsData?.pages.flatMap((page) => page.items) ?? []; const progress = stats.totalBookmarks > 0 ? ((stats.completedBookmarks + stats.failedBookmarks) / stats.totalBookmarks) * 100 : 0; const canDelete = stats.status === "completed" || stats.status === "failed" || stats.status === "paused"; const canPause = stats.status === "pending" || stats.status === "running"; const canResume = stats.status === "paused"; const statusLabels = (s: ZImportSessionStatus) => switchCase(s, { staging: t("settings.import_sessions.status.staging"), pending: t("settings.import_sessions.status.pending"), running: t("settings.import_sessions.status.running"), paused: t("settings.import_sessions.status.paused"), completed: t("settings.import_sessions.status.completed"), failed: t("settings.import_sessions.status.failed"), }); const handleDelete = () => { deleteSession.mutateAsync({ importSessionId: sessionId }).then(() => { router.push("/settings/import"); }); }; return (
{/* Back link */} {t("settings.import_sessions.detail.back_to_import")} {/* Header */}

{stats.name}

{t("settings.import_sessions.created_at", { time: formatDistanceToNow(stats.createdAt, { addSuffix: true, }), })}

{getStatusIcon(stats.status)} {statusLabels(stats.status)}
{/* Progress bar + stats */} {stats.totalBookmarks > 0 && (

{t("settings.import_sessions.progress")}

{stats.completedBookmarks + stats.failedBookmarks} /{" "} {stats.totalBookmarks} {Math.round(progress)}%
{stats.completedBookmarks > 0 && ( {t("settings.import_sessions.badges.completed", { count: stats.completedBookmarks, })} )} {stats.failedBookmarks > 0 && ( {t("settings.import_sessions.badges.failed", { count: stats.failedBookmarks, })} )} {stats.pendingBookmarks > 0 && ( {t("settings.import_sessions.badges.pending", { count: stats.pendingBookmarks, })} )} {stats.processingBookmarks > 0 && ( {t("settings.import_sessions.badges.processing", { count: stats.processingBookmarks, })} )}
)} {/* Message */} {stats.message && (
{stats.message}
)} {/* Action buttons */}
{canPause && ( )} {canResume && ( )} {canDelete && ( {t("settings.import_sessions.delete_dialog_description", { name: stats.name, })}
} actionButton={(setDialogOpen) => ( )} > )}
{/* Filter tabs + Results table */}
setFilter(v as FilterType)} className="w-full" > {t("settings.import_sessions.detail.filter_all")} {t("settings.import_sessions.detail.filter_accepted")} {t("settings.import_sessions.detail.filter_rejected")} {t("settings.import_sessions.detail.filter_duplicates")} {t("settings.import_sessions.detail.filter_pending")} {isResultsLoading ? ( ) : items.length === 0 ? (

{t("settings.import_sessions.detail.no_results")}

) : (
{t("settings.import_sessions.detail.table_title")} {t("settings.import_sessions.detail.table_type")} {t("settings.import_sessions.detail.table_result")} {t("settings.import_sessions.detail.table_reason")} {t("settings.import_sessions.detail.table_bookmark")} {items.map((item) => ( {getTitleDisplay( item, t("settings.import_sessions.detail.no_title"), )} {getTypeIcon(item.type)} {getTypeLabel(item.type, t)} {getResultBadge(item.status, item.result, t)} {item.resultReason || "—"} {item.resultBookmarkId ? ( {t("settings.import_sessions.detail.view_bookmark")} ) : ( )} ))}
{hasNextPage && (
fetchNextPage()} variant="ghost" > {t("settings.import_sessions.detail.load_more")}
)}
)}
); }