/** * T080, T083-T084: Main application layout with two-pane design * Loads directory tree on mount and note + backlinks when path changes */ import { useState, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; import { Plus, Settings as SettingsIcon, FolderPlus } from 'lucide-react'; import { ResizablePanelGroup, ResizablePanel, ResizableHandle } from '@/components/ui/resizable'; import { Button } from '@/components/ui/button'; import { Separator } from '@/components/ui/separator'; import { Alert, AlertDescription } from '@/components/ui/alert'; import { DirectoryTree } from '@/components/DirectoryTree'; import { DirectoryTreeSkeleton } from '@/components/DirectoryTreeSkeleton'; import { SearchBar } from '@/components/SearchBar'; import { NoteViewer } from '@/components/NoteViewer'; import { NoteViewerSkeleton } from '@/components/NoteViewerSkeleton'; import { NoteEditor } from '@/components/NoteEditor'; import { useToast } from '@/hooks/useToast'; import { GraphView } from '@/components/GraphView'; import { listNotes, getNote, getBacklinks, getIndexHealth, createNote, moveNote, type BacklinkResult, APIException, } from '@/services/api'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } from '@/components/ui/dialog'; import { Input } from '@/components/ui/input'; import type { IndexHealth } from '@/types/search'; import type { Note, NoteSummary } from '@/types/note'; import { normalizeSlug } from '@/lib/wikilink'; import { Network } from 'lucide-react'; import { AUTH_TOKEN_CHANGED_EVENT, isDemoSession, login } from '@/services/auth'; export function MainApp() { const navigate = useNavigate(); const toast = useToast(); const [notes, setNotes] = useState([]); const [selectedPath, setSelectedPath] = useState(null); const [currentNote, setCurrentNote] = useState(null); const [backlinks, setBacklinks] = useState([]); const [isLoadingNotes, setIsLoadingNotes] = useState(true); const [isLoadingNote, setIsLoadingNote] = useState(false); const [error, setError] = useState(null); const [isEditMode, setIsEditMode] = useState(false); const [isGraphView, setIsGraphView] = useState(false); const [indexHealth, setIndexHealth] = useState(null); const [isNewNoteDialogOpen, setIsNewNoteDialogOpen] = useState(false); const [newNoteName, setNewNoteName] = useState(''); const [isCreatingNote, setIsCreatingNote] = useState(false); const [isNewFolderDialogOpen, setIsNewFolderDialogOpen] = useState(false); const [newFolderName, setNewFolderName] = useState(''); const [isCreatingFolder, setIsCreatingFolder] = useState(false); const [isDemoMode, setIsDemoMode] = useState(isDemoSession()); useEffect(() => { const handleAuthChange = () => { const demo = isDemoSession(); setIsDemoMode(demo); if (demo) { setIsEditMode(false); } }; window.addEventListener(AUTH_TOKEN_CHANGED_EVENT, handleAuthChange); return () => { window.removeEventListener(AUTH_TOKEN_CHANGED_EVENT, handleAuthChange); }; }, []); // T083: Load directory tree on mount // T119: Load index health useEffect(() => { const loadData = async () => { setIsLoadingNotes(true); setError(null); try { // Load notes and index health in parallel const [notesList, health] = await Promise.all([ listNotes(), getIndexHealth().catch(() => null), // Don't fail if health unavailable ]); setNotes(notesList); setIndexHealth(health); // Auto-select first note if available if (notesList.length > 0 && !selectedPath) { setSelectedPath(notesList[0].note_path); } } catch (err) { if (err instanceof APIException) { setError(err.error); } else { setError('Failed to load notes'); } console.error('Error loading notes:', err); } finally { setIsLoadingNotes(false); } }; loadData(); }, []); // T084: Load note and backlinks when path changes useEffect(() => { if (!selectedPath) { setCurrentNote(null); setBacklinks([]); return; } const loadNote = async () => { setIsLoadingNote(true); setError(null); try { const [note, noteBacklinks] = await Promise.all([ getNote(selectedPath), getBacklinks(selectedPath), ]); setCurrentNote(note); setBacklinks(noteBacklinks); } catch (err) { if (err instanceof APIException) { setError(err.error); } else { setError('Failed to load note'); } console.error('Error loading note:', err); setCurrentNote(null); setBacklinks([]); } finally { setIsLoadingNote(false); } }; loadNote(); }, [selectedPath]); // Handle wikilink clicks const handleWikilinkClick = async (linkText: string) => { const slug = normalizeSlug(linkText); console.log(`[Wikilink] Clicked: "${linkText}", Slug: "${slug}"`); // Try to find exact match first let targetNote = notes.find( (note) => normalizeSlug(note.title) === slug ); // If not found, try path-based matching if (!targetNote) { targetNote = notes.find((note) => { const pathSlug = normalizeSlug(note.note_path.replace(/\.md$/, '')); // console.log(`Checking path: ${note.note_path}, Slug: ${pathSlug}`); return pathSlug.endsWith(slug); }); } if (targetNote) { console.log(`[Wikilink] Found target: ${targetNote.note_path}`); setSelectedPath(targetNote.note_path); } else { // TODO: Show "Create note" dialog console.log('Note not found for wikilink:', linkText); setError(`Note not found: ${linkText}`); } }; const handleSelectNote = (path: string) => { setSelectedPath(path); setError(null); setIsEditMode(false); // Exit edit mode when switching notes }; // T093: Handle edit button click const handleEdit = () => { if (isDemoMode) { toast.error('Demo mode is read-only. Sign in with Hugging Face to edit notes.'); return; } setIsEditMode(true); }; // Handle note save from editor const handleNoteSave = (updatedNote: Note) => { setCurrentNote(updatedNote); setIsEditMode(false); setError(null); // Reload notes list to update modified timestamp listNotes().then(setNotes).catch(console.error); }; // Handle editor cancel const handleEditCancel = () => { setIsEditMode(false); }; // Handle note dialog open change const handleDialogOpenChange = (open: boolean) => { if (open && isDemoMode) { toast.error('Demo mode is read-only. Sign in with Hugging Face to create notes.'); return; } setIsNewNoteDialogOpen(open); if (!open) { // Clear input when dialog closes setNewNoteName(''); } }; // Handle folder dialog open change const handleFolderDialogOpenChange = (open: boolean) => { if (open && isDemoMode) { toast.error('Demo mode is read-only. Sign in with Hugging Face to create folders.'); return; } setIsNewFolderDialogOpen(open); if (!open) { // Clear input when dialog closes setNewFolderName(''); } }; // Handle create new note const handleCreateNote = async () => { if (isDemoMode) { toast.error('Demo mode is read-only. Sign in to create notes.'); return; } if (!newNoteName.trim() || isCreatingNote) return; setIsCreatingNote(true); setError(null); try { const baseName = newNoteName.replace(/\.md$/, ''); let notePath = newNoteName.endsWith('.md') ? newNoteName : `${newNoteName}.md`; let attempt = 1; const maxAttempts = 100; // Retry with number suffix if note already exists while (attempt <= maxAttempts) { try { const note = await createNote({ note_path: notePath, title: baseName, body: `# ${baseName}\n\nStart writing your note here...`, }); // Refresh notes list const notesList = await listNotes(); setNotes(notesList); // Select the new note setSelectedPath(note.note_path); setIsEditMode(true); const displayName = notePath.replace(/\.md$/, ''); toast.success(`Note "${displayName}" created successfully`); break; } catch (err) { if (err instanceof APIException && err.status === 409) { // Note already exists, try with number suffix attempt++; if (attempt <= maxAttempts) { notePath = `${baseName} ${attempt}.md`; continue; } else { throw err; } } else { throw err; } } } } catch (err) { let errorMessage = 'Failed to create note'; if (err instanceof APIException) { // Use the message field which contains the actual error description errorMessage = err.message || err.error; } else if (err instanceof Error) { errorMessage = err.message; } toast.error(errorMessage); console.error('Error creating note:', err); } finally { setIsCreatingNote(false); // Always close dialog, regardless of success or failure handleDialogOpenChange(false); } }; // Handle create new folder const handleCreateFolder = async () => { if (isDemoMode) { toast.error('Demo mode is read-only. Sign in to create folders.'); return; } if (!newFolderName.trim() || isCreatingFolder) return; setIsCreatingFolder(true); setError(null); try { // Create a placeholder note in the folder const folderPath = newFolderName.replace(/\/$/, ''); // Remove trailing slash if present const placeholderPath = `${folderPath}/.placeholder.md`; await createNote({ note_path: placeholderPath, title: 'Folder', body: `# ${folderPath}\n\nThis folder was created.`, }); // Refresh notes list const notesList = await listNotes(); setNotes(notesList); toast.success(`Folder "${folderPath}" created successfully`); } catch (err) { let errorMessage = 'Failed to create folder'; if (err instanceof APIException) { errorMessage = err.message || err.error; } else if (err instanceof Error) { errorMessage = err.message; } toast.error(errorMessage); console.error('Error creating folder:', err); } finally { setIsCreatingFolder(false); // Always close dialog, regardless of success or failure handleFolderDialogOpenChange(false); } }; // Handle dragging file to folder const handleMoveNoteToFolder = async (oldPath: string, targetFolderPath: string) => { if (isDemoMode) { toast.error('Demo mode is read-only. Sign in to move notes.'); return; } try { // Get the filename from the old path const fileName = oldPath.split('/').pop(); if (!fileName) { toast.error('Invalid file path'); return; } // Construct new path: targetFolder/fileName const newPath = targetFolderPath ? `${targetFolderPath}/${fileName}` : fileName; // Don't move if source and destination are the same if (newPath === oldPath) { return; } await moveNote(oldPath, newPath); // Refresh notes list const notesList = await listNotes(); setNotes(notesList); // If moving currently selected note, update selection if (selectedPath === oldPath) { setSelectedPath(newPath); } toast.success(`Note moved successfully`); } catch (err) { let errorMessage = 'Failed to move note'; if (err instanceof APIException) { errorMessage = err.message || err.error; } else if (err instanceof Error) { errorMessage = err.message; } toast.error(errorMessage); console.error('Error moving note:', err); } }; return (
{/* Demo warning banner */} DEMO ONLY - ALL DATA IS TEMPORARY AND MAY BE DELETED AT ANY TIME {/* Top bar */}

📚 Document Viewer

{isDemoMode && ( )}
{/* Main content */}
{isDemoMode && (
You are browsing the shared demo vault in read-only mode. Sign in with your Hugging Face account to create and edit notes.
)} {/* Left sidebar */}
Create New Note Enter a name for your new note. The .md extension will be added automatically if not provided.
setNewNoteName(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') { handleCreateNote(); } }} />
Create New Folder Enter a name for your new folder. You can use forward slashes for nested folders (e.g., "Projects/Work").
setNewFolderName(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') { handleCreateFolder(); } }} />
{isLoadingNotes ? ( ) : ( )}
{/* Main content pane */}
{error && (
{error}
)} {isGraphView ? ( { handleSelectNote(path); setIsGraphView(false); }} /> ) : ( isLoadingNote ? ( ) : currentNote ? ( isEditMode ? ( ) : ( ) ) : (

Select a note to view

{notes.length === 0 ? 'No notes available. Create your first note to get started.' : 'Choose a note from the sidebar'}

) )}
{/* Footer with Index Health */}
{indexHealth ? ( <> {indexHealth.note_count} note{indexHealth.note_count !== 1 ? 's' : ''} indexed ) : ( <> {notes.length} note{notes.length !== 1 ? 's' : ''} indexed )}
{indexHealth && indexHealth.last_incremental_update && (
Last updated: {new Date(indexHealth.last_incremental_update).toLocaleString('en-US', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', })}
)}
); }