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 { listNotes, readNote, writeNote, getVaultPath, setVaultPath, ensureVault, getOrCreateDaily, 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; } 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 navigate = useNavigate(); // 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) { // Default to the project's vault directory path = "/home/amir/code/notes/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(); }, [vaultPath, refreshNotes]); // Build backlinks for current note useEffect(() => { if (!vaultPath || !currentNote || !notes.length) { setBacklinks([]); return; } (async () => { const currentName = currentNote .replace(/\.md$/, "") .split("/") .pop() ?.toLowerCase(); if (!currentName) return; // Read all notes and find backlinks 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) { 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 { // Skip unreadable notes } } 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] ); if (loading) { return (
📝

Loading vault...

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

Failed to load vault

{error}

); } return (
} /> } /> } /> } />
); } /* ── Note View ──────────────────────────────────────────────── */ function NoteView() { const { path } = useParams<{ path: string }>(); const { vaultPath, setCurrentNote, noteContent, setNoteContent } = useVault(); const decodedPath = decodeURIComponent(path || ""); useEffect(() => { if (!decodedPath || !vaultPath) return; setCurrentNote(decodedPath); readNote(vaultPath, decodedPath).then(setNoteContent).catch(() => setNoteContent("")); }, [decodedPath, vaultPath, setCurrentNote, setNoteContent]); return (
); } /* ── 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; }