import { useState, useEffect, useCallback, createContext, useContext } from "react"; import { Routes, Route, useNavigate, useParams } from "react-router-dom"; import { Sidebar } from "./components/Sidebar"; import { Editor } from "./components/Editor"; import { Backlinks } from "./components/Backlinks"; import { GraphView } from "./components/GraphView"; import { CommandPalette } from "./components/CommandPalette"; import { SplitView } from "./components/SplitView"; import { LinkPreview } from "./components/LinkPreview"; import { CalendarView } from "./components/CalendarView"; import { ThemePicker, useThemeInit } from "./components/ThemePicker"; import { KanbanView } from "./components/KanbanView"; import { SearchReplace } from "./components/SearchReplace"; import { FlashcardView } from "./components/FlashcardView"; import { CSSEditor, useCustomCssInit } from "./components/CSSEditor"; import { TabBar } from "./components/TabBar"; import { WhiteboardView } from "./components/WhiteboardView"; import { DatabaseView } from "./components/DatabaseView"; import { GitPanel } from "./components/GitPanel"; import { TimelineView } from "./components/TimelineView"; import { GraphAnalytics } from "./components/GraphAnalytics"; import { listNotes, readNote, writeNote, getVaultPath, setVaultPath, ensureVault, getOrCreateDaily, addVault, getFavorites, getBacklinkContext, setFavorites as setFavoritesCmd, type NoteEntry, } from "./lib/commands"; import { extractWikilinks, type BacklinkEntry } from "./lib/wikilinks"; /* ── Vault Context ──────────────────────────────────────────── */ interface VaultContextType { vaultPath: string; notes: NoteEntry[]; refreshNotes: () => Promise; currentNote: string | null; setCurrentNote: (path: string | null) => void; noteContent: string; setNoteContent: (content: string) => void; backlinks: BacklinkEntry[]; navigateToNote: (name: string) => void; sidebarOpen: boolean; toggleSidebar: () => void; editMode: boolean; toggleEditMode: () => void; splitNote: string | null; setSplitNote: (path: string | null) => void; favorites: string[]; toggleFavorite: (path: string) => void; recentNotes: string[]; switchVault: (path: string) => Promise; focusMode: boolean; toggleFocusMode: () => void; } const VaultContext = createContext(null!); export const useVault = () => useContext(VaultContext); /* ── Main App ───────────────────────────────────────────────── */ export default function App() { const [vaultPath, setVaultPathState] = useState(""); const [notes, setNotes] = useState([]); const [currentNote, setCurrentNote] = useState(null); const [noteContent, setNoteContent] = useState(""); const [backlinks, setBacklinks] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [sidebarOpen, setSidebarOpen] = useState(true); const [editMode, setEditMode] = useState(true); const [cmdPaletteOpen, setCmdPaletteOpen] = useState(false); const [splitNote, setSplitNote] = useState(null); const [favorites, setFavorites] = useState([]); const [recentNotes, setRecentNotes] = useState([]); const [themePickerOpen, setThemePickerOpen] = useState(false); const [focusMode, setFocusMode] = useState(false); const [searchReplaceOpen, setSearchReplaceOpen] = useState(false); const [cssEditorOpen, setCssEditorOpen] = useState(false); const navigate = useNavigate(); // Apply saved theme + custom CSS on mount useThemeInit(); useCustomCssInit(); const toggleSidebar = useCallback(() => setSidebarOpen(v => !v), []); const toggleEditMode = useCallback(() => setEditMode(v => !v), []); const toggleFocusMode = useCallback(() => setFocusMode(v => !v), []); // Initialize vault useEffect(() => { (async () => { try { console.log("[GraphNotes] Initializing vault..."); let path: string | null = null; try { path = await getVaultPath(); console.log("[GraphNotes] Stored vault path:", path); } catch (e) { console.warn("[GraphNotes] getVaultPath failed:", e); } if (!path) { // No stored vault path — use a sensible default path = "./vault"; console.log("[GraphNotes] Using default vault path:", path); try { await setVaultPath(path); } catch (e) { console.warn("[GraphNotes] setVaultPath failed:", e); } } try { await ensureVault(path); } catch (e) { console.warn("[GraphNotes] ensureVault failed:", e); } setVaultPathState(path); console.log("[GraphNotes] Vault ready at:", path); setLoading(false); } catch (e) { console.error("[GraphNotes] Init failed:", e); setError(String(e)); setLoading(false); } })(); }, []); // Load notes when vault is ready const refreshNotes = useCallback(async () => { if (!vaultPath) return; const entries = await listNotes(vaultPath); setNotes(entries); }, [vaultPath]); useEffect(() => { if (vaultPath) refreshNotes(); // Load favorites if (vaultPath) { getFavorites(vaultPath).then(setFavorites).catch(() => setFavorites([])); } }, [vaultPath, refreshNotes]); // Track recent notes const trackRecent = useCallback((notePath: string) => { setRecentNotes(prev => { const next = [notePath, ...prev.filter(p => p !== notePath)].slice(0, 10); return next; }); }, []); // Watch currentNote changes to track recents useEffect(() => { if (currentNote) trackRecent(currentNote); }, [currentNote, trackRecent]); // Toggle favorite const toggleFavorite = useCallback((path: string) => { setFavorites(prev => { const next = prev.includes(path) ? prev.filter(p => p !== path) : [...prev, path]; // Persist if (vaultPath) setFavoritesCmd(vaultPath, next).catch(() => { }); return next; }); }, [vaultPath]); // Switch vault const switchVault = useCallback(async (newPath: string) => { try { await ensureVault(newPath); await setVaultPath(newPath); await addVault(newPath); setVaultPathState(newPath); setCurrentNote(null); setNoteContent(""); setSplitNote(null); setRecentNotes([]); navigate("/"); } catch (e) { console.error("Switch vault failed:", e); } }, [navigate, setCurrentNote, setNoteContent]); // Build backlinks for current note using backend context useEffect(() => { if (!vaultPath || !currentNote || !notes.length) { setBacklinks([]); return; } (async () => { const currentName = currentNote .replace(/\.md$/, "") .split("/") .pop() || ""; if (!currentName) return; try { const contexts = await getBacklinkContext(vaultPath, currentName); const entries: BacklinkEntry[] = contexts.map(c => ({ sourcePath: c.source_path, sourceName: c.source_name, context: c.excerpt, })); setBacklinks(entries); } catch { // Fallback: simple wikilink scan const allPaths = flattenNotes(notes); const entries: BacklinkEntry[] = []; for (const notePath of allPaths) { if (notePath === currentNote) continue; try { const content = await readNote(vaultPath, notePath); const links = extractWikilinks(content); for (const link of links) { if (link.target.toLowerCase() === currentName.toLowerCase()) { const lines = content.split("\n"); const contextLine = lines.find((l) => l.includes(link.raw)) || ""; entries.push({ sourcePath: notePath, sourceName: notePath.replace(/\.md$/, "").split("/").pop() || notePath, context: contextLine.trim().substring(0, 200), }); } } } catch { } } setBacklinks(entries); } })(); }, [vaultPath, currentNote, notes]); const navigateToNote = useCallback( (name: string) => { // Find the note by name (case-insensitive) const allPaths = flattenNotes(notes); const match = allPaths.find( (p) => p .replace(/\.md$/, "") .split("/") .pop() ?.toLowerCase() === name.toLowerCase() ); if (match) { navigate(`/note/${encodeURIComponent(match)}`); } else { // Create new note const newPath = `${name}.md`; writeNote(vaultPath, newPath, `# ${name}\n\n`).then(() => { refreshNotes(); navigate(`/note/${encodeURIComponent(newPath)}`); }); } }, [notes, vaultPath, navigate, refreshNotes] ); // ── Global keyboard shortcuts ── useEffect(() => { const handler = (e: KeyboardEvent) => { const mod = e.metaKey || e.ctrlKey; if (!mod) return; // ⌘⇧F — Focus mode if (e.shiftKey && e.key.toLowerCase() === "f") { e.preventDefault(); setFocusMode(v => !v); return; } switch (e.key.toLowerCase()) { case "k": e.preventDefault(); setCmdPaletteOpen(v => !v); break; case "n": e.preventDefault(); { const name = prompt("Note name:"); if (name?.trim()) navigateToNote(name.trim()); } break; case "g": e.preventDefault(); navigate("/graph"); break; case "d": e.preventDefault(); navigate("/daily"); break; case "e": e.preventDefault(); toggleEditMode(); break; case "\\": e.preventDefault(); toggleSidebar(); break; case "t": e.preventDefault(); setThemePickerOpen(v => !v); break; case "h": e.preventDefault(); setSearchReplaceOpen(v => !v); break; case "u": e.preventDefault(); setCssEditorOpen(v => !v); break; } }; window.addEventListener("keydown", handler); return () => window.removeEventListener("keydown", handler); }, [navigate, navigateToNote, toggleEditMode, toggleSidebar]); if (loading) { return (
📝

Loading vault...

); } if (error) { return (
⚠️

Failed to load vault

{error}

); } return (
{sidebarOpen && !focusMode && } } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } />
setCmdPaletteOpen(false)} /> setThemePickerOpen(false)} /> setSearchReplaceOpen(false)} /> setCssEditorOpen(false)} />
); } /* ── Daily View ─────────────────────────────────────────────── */ function DailyView() { const { vaultPath, refreshNotes } = useVault(); const navigate = useNavigate(); useEffect(() => { if (!vaultPath) return; getOrCreateDaily(vaultPath).then((dailyPath) => { refreshNotes(); navigate(`/note/${encodeURIComponent(dailyPath)}`, { replace: true }); }); }, [vaultPath, navigate, refreshNotes]); return (

Creating daily note...

); } /* ── Welcome Screen ─────────────────────────────────────────── */ function WelcomeScreen() { const navigate = useNavigate(); return (
📝

Graph Notes

A local-first knowledge base with bidirectional linking and graph visualization. Your notes, your data.

); } /* ── Helpers ────────────────────────────────────────────────── */ function flattenNotes(entries: NoteEntry[]): string[] { const paths: string[] = []; for (const entry of entries) { if (entry.is_dir && entry.children) { paths.push(...flattenNotes(entry.children)); } else if (!entry.is_dir) { paths.push(entry.path); } } return paths; }