"use client"; import { useCallback, useMemo, useState, type ReactNode } from "react"; import { AlertTriangle, CheckCircle2, Eye, FolderInput, FolderOpen, Loader2, } from "lucide-react"; import { useTranslation } from "react-i18next"; import Modal from "@/components/common/Modal"; import Button from "@/components/ui/Button"; import ScopePicker from "@/components/space/ScopePicker"; import { importChatHistory } from "@/lib/imports-api"; import { newAgentId, saveAgent } from "@/lib/chat-import/agent-store"; import { buildSelectGroups, ImportScanError, isFileSystemAccessSupported, parseSessions, pickAndScan, selectionUnit, SOURCE_LABEL, type AgentScope, type ImportScanErrorCode, type ImportSource, type ScanResult, type SelectGroup, type SelectionUnit, } from "@/lib/chat-import"; type Phase = "intro" | "scanning" | "select" | "importing" | "done" | "error"; interface ImportWizardProps { onClose: () => void; onImported: () => void; } function buildScope(unit: SelectionUnit, keys: string[]): AgentScope { return unit === "dates" ? { kind: "dates", days: keys } : { kind: "projects", cwds: keys }; } export default function ImportWizard({ onClose, onImported, }: ImportWizardProps) { const { t, i18n } = useTranslation(); const [phase, setPhase] = useState("intro"); const [scan, setScan] = useState(null); const [name, setName] = useState(""); const [selectedKeys, setSelectedKeys] = useState>(new Set()); const [errorCode, setErrorCode] = useState( "generic", ); const [progress, setProgress] = useState({ stage: "parsing" as "parsing" | "saving", done: 0, total: 0, }); const [result, setResult] = useState<{ imported: number; skipped: number }>({ imported: 0, skipped: 0, }); const groups = useMemo( () => (scan ? buildSelectGroups(scan) : []), [scan], ); const unit = scan ? selectionUnit(scan.source) : "projects"; const selectedRefs = useMemo( () => groups.filter((g) => selectedKeys.has(g.key)).flatMap((g) => g.sessions), [groups, selectedKeys], ); const runScan = useCallback(async () => { setPhase("scanning"); try { const next = await pickAndScan(); const nextGroups = buildSelectGroups(next); setScan(next); // Default to "bring everything in" — one click for the common case, with // every unit pre-checked so narrowing is just deselecting. setSelectedKeys(new Set(nextGroups.map((g) => g.key))); setName(SOURCE_LABEL[next.source]); setPhase("select"); } catch (err) { if (err instanceof ImportScanError && err.code === "aborted") { setPhase("intro"); return; } setErrorCode(err instanceof ImportScanError ? err.code : "generic"); setPhase("error"); } }, []); const toggleKey = useCallback((key: string) => { setSelectedKeys((prev) => { const next = new Set(prev); if (next.has(key)) next.delete(key); else next.add(key); return next; }); }, []); const selectAll = useCallback( () => setSelectedKeys(new Set(groups.map((g) => g.key))), [groups], ); const clearAll = useCallback(() => setSelectedKeys(new Set()), []); const runImport = useCallback(async () => { if (!scan || selectedRefs.length === 0) return; setPhase("importing"); setProgress({ stage: "parsing", done: 0, total: selectedRefs.length }); try { const normalized = await parseSessions( scan.source, selectedRefs, (done, total) => setProgress({ stage: "parsing", done, total }), ); setProgress({ stage: "saving", done: 0, total: normalized.length }); const scope = buildScope(unit, [...selectedKeys]); const agentId = newAgentId(scan.source); const agentName = name.trim() || SOURCE_LABEL[scan.source]; const res = await importChatHistory(scan.source, normalized, { id: agentId, name: agentName, }); // Register the named, scoped agent so it can be re-synced later without // re-picking. Never block the success state on the registry write. try { await saveAgent({ id: agentId, name: agentName, source: scan.source, folderName: scan.handle.name, handle: scan.handle, scope, createdAt: Date.now(), lastSyncAt: Date.now(), }); } catch { // registry is best-effort } setResult({ imported: res.imported, skipped: res.skipped }); setPhase("done"); } catch { setErrorCode("generic"); setPhase("error"); } }, [scan, selectedRefs, selectedKeys, unit, name]); const titleIcon = ; return ( 0} onCancel={onClose} onPick={runScan} onImport={runImport} onDone={onImported} onRetry={() => setPhase("intro")} /> } >
{phase === "intro" && } {phase === "scanning" && ( } title={t("Reading your folder…")} subtitle={t("Finding projects and conversations.")} /> )} {phase === "select" && scan && (groups.length === 0 ? ( } title={t("No conversations found")} subtitle={t( "This folder has no readable chat history. Pick your .claude or .codex folder.", )} /> ) : (
setName(e.target.value)} placeholder={SOURCE_LABEL[scan.source]} className="w-full rounded-xl border border-[var(--border)] bg-[var(--background)] px-3 py-2 text-[13px] text-[var(--foreground)] outline-none transition focus:border-[var(--primary)]/50 focus:ring-2 focus:ring-[var(--primary)]/15" />

{unit === "dates" ? t( "Pick the days to bring in. Refreshing later re-syncs these days and pulls in new conversations on them.", ) : t( "Pick the projects to bring in. Refreshing later re-syncs these projects and pulls in their new conversations.", )}

))} {phase === "importing" && } {phase === "done" && } {phase === "error" && }
); } /* ----------------------------- sub-views ----------------------------- */ function IntroView() { const { t } = useTranslation(); const supported = isFileSystemAccessSupported(); return (

{t( "Select your local .claude or .codex folder. DeepTutor reads it right here in your browser — nothing leaves your machine until you choose what to import.", )}

{t( "It auto-detects the tool, groups conversations by project, and lets you pick exactly what to bring in.", )}

{!supported ? (
{t( "Your browser doesn't support folder access. Please use a Chromium-based browser (Chrome, Edge, Arc).", )}
) : (
{t( "Hidden folders like .claude won't show by default — press ⌘⇧. (Command-Shift-Period) in the picker to reveal them.", )}
)}
); } function ImportingView({ progress, }: { progress: { stage: "parsing" | "saving"; done: number; total: number }; }) { const { t } = useTranslation(); const pct = progress.total > 0 ? Math.round((progress.done / progress.total) * 100) : progress.stage === "saving" ? 90 : 0; return (
} title={ progress.stage === "parsing" ? t("Reading conversations…") : t("Saving to your space…") } subtitle={ progress.stage === "parsing" ? t("{{done}} / {{total}}", { done: progress.done, total: progress.total, }) : t("Almost there.") } />
); } function DoneView({ result, }: { result: { imported: number; skipped: number }; }) { const { t } = useTranslation(); return ( } title={t("Imported {{count}} conversations", { count: result.imported })} subtitle={ result.skipped > 0 ? t("{{count}} skipped (already imported or empty).", { count: result.skipped, }) : t("They're ready in your space — open one to keep chatting.") } /> ); } function ErrorView({ code }: { code: ImportScanErrorCode | "generic" }) { const { t } = useTranslation(); const message = code === "not_recognized" ? t( "This folder doesn't look like a .claude or .codex home. Please select the right folder.", ) : code === "unsupported_browser" ? t( "Your browser doesn't support folder access. Please use a Chromium-based browser.", ) : t("Something went wrong while importing. Please try again."); return ( } title={t("Couldn't import")} subtitle={message} /> ); } function CenteredStatus({ icon, title, subtitle, }: { icon: ReactNode; title: string; subtitle: string; }) { return (
{icon}

{title}

{subtitle}

); } /* ----------------------------- footer ----------------------------- */ function WizardFooter({ phase, source, selectedCount, canImport, onCancel, onPick, onImport, onDone, onRetry, }: { phase: Phase; source: ImportSource | null; selectedCount: number; canImport: boolean; onCancel: () => void; onPick: () => void; onImport: () => void; onDone: () => void; onRetry: () => void; }) { const { t } = useTranslation(); if (phase === "intro") { return (
); } if (phase === "select") { return (
{source ? SOURCE_LABEL[source] : ""}
); } if (phase === "done") { return (
); } if (phase === "error") { return (
); } // scanning / importing: no actions return (
{t("Working…")}
); }