"use client"; import { useState, useCallback, useEffect, useRef } from "react"; import { useRouter } from "next/navigation"; import { getLocalDateString } from "@/lib/utils"; import { Button } from "@/components/ui/button"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog"; import { SpinnerGap, CheckCircle, X } from "@phosphor-icons/react"; import { useTranslation } from "@/hooks/useTranslation"; import type { WorkspaceInspectResult } from "@/types"; interface FileStatus { exists: boolean; chars: number; preview: string; } interface WorkspaceState { onboardingComplete: boolean; lastCheckInDate: string | null; schemaVersion: number; } interface TaxonomyCategoryInfo { id: string; label: string; role: string; confidence: number; source: string; paths: string[]; } interface IndexStats { fileCount: number; chunkCount: number; lastIndexed: number; staleCount: number; } interface WorkspaceInfo { path: string | null; valid?: boolean; reason?: string; exists?: boolean; files: Record; state: WorkspaceState | null; } const FILE_LABELS: Record = { claude: "claude.md", soul: "soul.md", user: "user.md", memory: "memory.md", }; type TabId = 'files' | 'taxonomy' | 'index' | 'organize'; type PathValidationStatus = 'idle' | 'checking' | 'valid' | 'invalid'; type ConfirmDialogType = | { kind: 'not_found' } | { kind: 'empty' } | { kind: 'normal_directory' } | { kind: 'existing_workspace'; summary: NonNullable } | { kind: 'partial_workspace' }; export function AssistantWorkspaceSection() { const { t } = useTranslation(); const router = useRouter(); const [workspace, setWorkspace] = useState(null); const [loading, setLoading] = useState(true); const [initializing, setInitializing] = useState(false); const [refreshingDocs, setRefreshingDocs] = useState(false); const [pathInput, setPathInput] = useState(""); const [creatingSession, setCreatingSession] = useState(false); const [activeTab, setActiveTab] = useState('files'); const [taxonomy, setTaxonomy] = useState([]); const [indexStats, setIndexStats] = useState(null); const [reindexing, setReindexing] = useState(false); const [archiving, setArchiving] = useState(false); // Path validation state const [pathValidation, setPathValidation] = useState('idle'); const [pathError, setPathError] = useState(null); const debounceRef = useRef | null>(null); // Confirmation dialog state const [confirmDialog, setConfirmDialog] = useState(null); const [inspecting, setInspecting] = useState(false); const fetchWorkspace = useCallback(async () => { try { const res = await fetch("/api/settings/workspace"); if (res.ok) { const data = await res.json(); setWorkspace(data); if (data.path) setPathInput(data.path); } } catch (e) { console.error("Failed to fetch workspace:", e); } finally { setLoading(false); } }, []); const fetchTaxonomy = useCallback(async () => { try { const res = await fetch("/api/settings/workspace"); if (res.ok) { const data = await res.json(); if (data.taxonomy) setTaxonomy(data.taxonomy); } } catch { /* ignore */ } }, []); const fetchIndexStats = useCallback(async () => { try { const res = await fetch("/api/workspace/index"); if (res.ok) { const data = await res.json(); setIndexStats(data); } } catch { /* ignore */ } }, []); useEffect(() => { fetchWorkspace(); }, [fetchWorkspace]); useEffect(() => { if (workspace?.path && activeTab === 'taxonomy') fetchTaxonomy(); if (workspace?.path && activeTab === 'index') fetchIndexStats(); }, [workspace?.path, activeTab, fetchTaxonomy, fetchIndexStats]); // Debounced path validation const validatePath = useCallback((path: string) => { if (debounceRef.current) clearTimeout(debounceRef.current); setPathError(null); if (!path.trim()) { setPathValidation('idle'); return; } setPathValidation('checking'); debounceRef.current = setTimeout(async () => { try { const res = await fetch(`/api/workspace/inspect?path=${encodeURIComponent(path.trim())}`); if (!res.ok) { setPathValidation('invalid'); setPathError(t('assistant.inspectFailed')); return; } const data: WorkspaceInspectResult = await res.json(); if (!data.exists) { // Non-existent path is allowed — user can create it via initialize setPathValidation('valid'); } else if (!data.isDirectory) { setPathValidation('invalid'); setPathError(t('assistant.pathNotDirectory')); } else if (!data.readable) { setPathValidation('invalid'); setPathError(t('assistant.pathNotReadable')); } else if (!data.writable) { setPathValidation('invalid'); setPathError(t('assistant.pathNotWritable')); } else { setPathValidation('valid'); } } catch { setPathValidation('invalid'); setPathError(t('assistant.inspectFailed')); } }, 500); }, [t]); // Clean up debounce on unmount useEffect(() => { return () => { if (debounceRef.current) clearTimeout(debounceRef.current); }; }, []); const handlePathInputChange = useCallback((value: string) => { setPathInput(value); validatePath(value); }, [validatePath]); // Execute the actual save + optional auto-navigate // navigateMode: 'new' = always create new session, 'reuse' = try to reuse latest session const executeSave = useCallback(async (initialize: boolean, resetOnboarding?: boolean, navigateMode: 'new' | 'reuse' = 'new') => { if (!pathInput.trim()) return; const oldPath = workspace?.path || null; const newPath = pathInput.trim(); if (initialize) setInitializing(true); try { const body: Record = { path: newPath, initialize }; if (resetOnboarding) body.resetOnboarding = true; const res = await fetch("/api/settings/workspace", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), }); if (res.ok) { await fetchWorkspace(); // If path actually changed, dispatch event if (oldPath && oldPath !== newPath) { window.dispatchEvent(new CustomEvent('assistant-workspace-switched', { detail: { oldPath, newPath }, })); } // Navigate to assistant session try { const model = typeof window !== 'undefined' ? localStorage.getItem('codepilot:last-model') || '' : ''; const provider_id = typeof window !== 'undefined' ? localStorage.getItem('codepilot:last-provider-id') || '' : ''; const sessionMode = navigateMode === 'reuse' ? 'checkin' : 'onboarding'; const sessionRes = await fetch("/api/workspace/session", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ mode: sessionMode, model, provider_id }), }); if (sessionRes.ok) { const sessionData = await sessionRes.json(); window.dispatchEvent(new CustomEvent("session-created")); router.push(`/chat/${sessionData.session.id}`); } } catch (navErr) { console.error("Failed to auto-navigate to session:", navErr); } } } catch (e) { console.error("Failed to save workspace path:", e); } finally { setInitializing(false); setConfirmDialog(null); } }, [pathInput, fetchWorkspace, workspace?.path, router]); // Inspect path and show confirmation dialog (or save directly if path unchanged) const handleSaveClick = useCallback(async () => { if (!pathInput.trim()) return; // If path hasn't changed, no-op if (pathInput.trim() === workspace?.path) { return; } setInspecting(true); try { const res = await fetch(`/api/workspace/inspect?path=${encodeURIComponent(pathInput.trim())}`); if (!res.ok) { setPathValidation('invalid'); setPathError(t('assistant.inspectFailed')); return; } const data: WorkspaceInspectResult = await res.json(); // Non-existent path — offer to create it if (!data.exists) { setConfirmDialog({ kind: 'not_found' }); return; } // Check for invalid states — show error, block save if (!data.isDirectory) { setPathValidation('invalid'); setPathError(t('assistant.pathNotDirectory')); return; } if (!data.readable) { setPathValidation('invalid'); setPathError(t('assistant.pathNotReadable')); return; } if (!data.writable) { setPathValidation('invalid'); setPathError(t('assistant.pathNotWritable')); return; } // Show confirmation dialog based on workspace status switch (data.workspaceStatus) { case 'empty': setConfirmDialog({ kind: 'empty' }); break; case 'normal_directory': setConfirmDialog({ kind: 'normal_directory' }); break; case 'existing_workspace': setConfirmDialog({ kind: 'existing_workspace', summary: data.summary || { onboardingComplete: false, lastCheckInDate: null, fileCount: 0 }, }); break; case 'partial_workspace': setConfirmDialog({ kind: 'partial_workspace' }); break; default: setPathValidation('invalid'); setPathError(t('assistant.pathInvalid')); } } catch (e) { console.error("Failed to inspect workspace:", e); setPathValidation('invalid'); setPathError(t('assistant.inspectFailed')); } finally { setInspecting(false); } }, [pathInput, workspace?.path, executeSave, t]); const handleSelectFolder = useCallback(async () => { try { if (window.electronAPI?.dialog?.openFolder) { const result = await window.electronAPI.dialog.openFolder({ title: t('assistant.selectFolder') }); if (!result.canceled && result.filePaths[0]) { setPathInput(result.filePaths[0]); validatePath(result.filePaths[0]); } } else { const input = prompt("Enter workspace directory path:"); if (input) { setPathInput(input); validatePath(input); } } } catch (e) { console.error("Failed to select folder:", e); } }, [validatePath, t]); const handleRefreshDocs = useCallback(async () => { setRefreshingDocs(true); try { await fetch("/api/workspace/docs", { method: "POST" }); } catch (e) { console.error("Failed to refresh docs:", e); } finally { setRefreshingDocs(false); } }, []); const handleStartOnboarding = useCallback(async () => { if (!workspace?.path) return; setCreatingSession(true); try { const model = typeof window !== 'undefined' ? localStorage.getItem('codepilot:last-model') || '' : ''; const provider_id = typeof window !== 'undefined' ? localStorage.getItem('codepilot:last-provider-id') || '' : ''; const res = await fetch("/api/workspace/session", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ mode: 'onboarding', model, provider_id }), }); if (res.ok) { const data = await res.json(); window.dispatchEvent(new CustomEvent("session-created")); router.push(`/chat/${data.session.id}`); } } catch (e) { console.error("Failed to create onboarding session:", e); } finally { setCreatingSession(false); } }, [workspace?.path, router]); const handleStartCheckIn = useCallback(async () => { if (!workspace?.path) return; setCreatingSession(true); try { const model = typeof window !== 'undefined' ? localStorage.getItem('codepilot:last-model') || '' : ''; const provider_id = typeof window !== 'undefined' ? localStorage.getItem('codepilot:last-provider-id') || '' : ''; const res = await fetch("/api/workspace/session", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ mode: 'checkin', model, provider_id }), }); if (res.ok) { const data = await res.json(); window.dispatchEvent(new CustomEvent("session-created")); router.push(`/chat/${data.session.id}`); } } catch (e) { console.error("Failed to create check-in session:", e); } finally { setCreatingSession(false); } }, [workspace?.path, router]); const handleReindex = useCallback(async () => { setReindexing(true); try { await fetch("/api/workspace/index", { method: "POST" }); await fetchIndexStats(); } catch (e) { console.error("Failed to reindex:", e); } finally { setReindexing(false); } }, [fetchIndexStats]); const handleArchive = useCallback(async () => { setArchiving(true); try { await fetch("/api/workspace/organize", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ action: 'archive' }), }); } catch (e) { console.error("Failed to archive:", e); } finally { setArchiving(false); } }, []); if (loading) { return (
); } const today = getLocalDateString(); const checkInDoneToday = workspace?.state?.lastCheckInDate === today; const tabs: Array<{ id: TabId; label: string }> = [ { id: 'files', label: t('assistant.fileStatus') }, { id: 'taxonomy', label: t('assistant.taxonomyTitle') }, { id: 'index', label: t('assistant.indexTitle') }, { id: 'organize', label: t('assistant.organizeTitle') }, ]; // Render path validation indicator const renderValidationIcon = () => { switch (pathValidation) { case 'checking': return ; case 'valid': return ; case 'invalid': return ; default: return null; } }; return (
{/* Workspace Path Card */}

{t('assistant.workspacePath')}

{t('assistant.workspacePathHint')}

handlePathInputChange(e.target.value)} placeholder="/path/to/workspace" className="w-full rounded-md border border-border/50 bg-background px-3 py-1.5 text-sm pr-8" />
{renderValidationIcon()}
{pathError && (

{pathError}

)}
{/* Invalid workspace path warning */} {workspace?.path && workspace.valid === false && (

{t('assistant.workspaceInvalid')}: {workspace.reason === 'path_not_found' ? t('assistant.pathNotExist') : workspace.reason === 'not_a_directory' ? t('assistant.pathNotDirectory') : workspace.reason === 'not_readable' ? t('assistant.pathNotReadable') : workspace.reason === 'not_writable' ? t('assistant.pathNotWritable') : t('assistant.pathInvalid') }

)} {/* Onboarding Status Card */} {workspace?.path && workspace.valid !== false && (

{t('assistant.onboardingTitle')}

{t('assistant.onboardingDesc')}

{workspace.state?.onboardingComplete ? {t('assistant.onboardingComplete')} : {t('assistant.onboardingNotStarted')} }

)} {/* Daily Check-in Card — only shown when onboarding is complete */} {workspace?.path && workspace.valid !== false && workspace.state?.onboardingComplete && (

{t('assistant.checkInTitle')}

{t('assistant.checkInDesc')}

{workspace.state?.lastCheckInDate && ( {t('assistant.lastCheckIn')}: {workspace.state.lastCheckInDate} )} {" "} {checkInDoneToday ? {t('assistant.checkInToday')} : {t('assistant.checkInNeeded')} }

)} {/* Tabbed Section: Files / Taxonomy / Index / Organize */} {workspace?.path && workspace.valid !== false && (
{tabs.map(tab => ( ))}
{/* Files Tab */} {activeTab === 'files' && (
{Object.entries(FILE_LABELS).map(([key, label]) => { const file = workspace.files[key]; return (
{label}
{file?.exists ? ( <> {t('assistant.fileChars', { count: String(file.chars) })} {t('assistant.fileExists')} ) : ( <> {t('assistant.fileMissing')} )}
); })}
)} {/* Taxonomy Tab */} {activeTab === 'taxonomy' && (

{t('assistant.taxonomyDesc')}

{taxonomy.length === 0 ? (

{t('assistant.taxonomyEmpty')}

) : (
{taxonomy.map(cat => (
{cat.label} {t('assistant.taxonomyRole')}: {cat.role}
{t('assistant.taxonomySource')}: {cat.source} 0.7 ? 'bg-green-500/10 text-green-600' : cat.confidence > 0.4 ? 'bg-yellow-500/10 text-yellow-600' : 'bg-red-500/10 text-red-600' }`}> {Math.round(cat.confidence * 100)}%
))}
)}
)} {/* Index Tab */} {activeTab === 'index' && (

{t('assistant.indexDesc')}

{indexStats ? (
{t('assistant.indexFiles', { count: String(indexStats.fileCount) })}
{t('assistant.indexChunks', { count: String(indexStats.chunkCount) })}
{t('assistant.indexStale', { count: String(indexStats.staleCount) })}
{t('assistant.indexLastIndexed')}: {indexStats.lastIndexed ? new Date(indexStats.lastIndexed).toLocaleString() : 'never'}
) : (

{t('common.loading')}

)}
)} {/* Organize Tab */} {activeTab === 'organize' && (

{t('assistant.organizeDesc')}

)}
)} {/* ── Confirmation Dialogs ── */} {/* Non-existent path — offer to create */} { if (!open) setConfirmDialog(null); }}> {t('assistant.confirmNotFoundTitle')} {t('assistant.confirmNotFoundDesc')} {t('common.cancel')} executeSave(true)} disabled={initializing}> {initializing ? ( <> {t('assistant.initializing')} ) : ( t('assistant.confirmCreate') )} {/* Empty directory confirmation */} { if (!open) setConfirmDialog(null); }}> {t('assistant.confirmEmptyTitle')} {t('assistant.confirmEmptyDesc')} {t('common.cancel')} executeSave(true)} disabled={initializing}> {initializing ? ( <> {t('assistant.initializing')} ) : ( t('assistant.confirmInitialize') )} {/* Normal directory confirmation */} { if (!open) setConfirmDialog(null); }}> {t('assistant.confirmNormalTitle')}

{t('assistant.confirmNormalDesc')}

{t('assistant.confirmNormalHint')}

{t('common.cancel')} executeSave(true)} disabled={initializing}> {initializing ? ( <> {t('assistant.initializing')} ) : ( t('assistant.confirmInitialize') )}
{/* Existing workspace confirmation */} { if (!open) setConfirmDialog(null); }}> {t('assistant.confirmExistingTitle')}

{t('assistant.confirmExistingDesc')}

{confirmDialog?.kind === 'existing_workspace' && (
{t('assistant.summaryOnboarding')}: {confirmDialog.summary.onboardingComplete ? t('assistant.onboardingComplete') : t('assistant.onboardingNotStarted') }
{t('assistant.summaryLastCheckIn')}: {confirmDialog.summary.lastCheckInDate || t('assistant.summaryNever')}
{t('assistant.summaryFileCount')}: {confirmDialog.summary.fileCount}
)}
{t('common.cancel')}
{/* Partial workspace confirmation */} { if (!open) setConfirmDialog(null); }}> {t('assistant.confirmPartialTitle')} {t('assistant.confirmPartialDesc')} {t('common.cancel')} executeSave(true)} disabled={initializing}> {initializing ? ( <> {t('assistant.initializing')} ) : ( t('assistant.confirmRepair') )}
); }