import { useEffect, useMemo, useState } from "react" import { Library, FileText, Sparkles, Loader2, FolderOpen, Search } from "lucide-react" import { api, type WissenDatei, type WissenFile } from "@/lib/api" import { useWissen, useMemoryGraph } from "@/lib/queries" import { GraphView } from "./GraphView" import { GraphErrorBoundary } from "./GraphErrorBoundary" import { cn } from "@/lib/utils" // Wissens-Vault: die nächtlichen Traum-Notizen der Box als klickbares Wiki. // [[wiki-links]] springen zur verlinkten Notiz; Markdown wird leichtgewichtig gerendert // (bewusst ohne externe Markdown-Lib — Überschriften, Listen, fett/kursiv, Code reichen hier). const DIR_LABEL: Record = { "": "Allgemein", traeume: "Träume", muster: "Muster", "skill-kandidaten": "Skill-Kandidaten", "skill-kandidaten/beauftragt": "Skill-Kandidaten · beauftragt", "skill-kandidaten/verworfen": "Skill-Kandidaten · verworfen", } export function WissenView() { const { data, isLoading } = useWissen() const [active, setActive] = useState(null) const [doc, setDoc] = useState(null) const [docLoading, setDocLoading] = useState(false) const [q, setQ] = useState("") const [showGraph, setShowGraph] = useState(false) const { data: graphData, isLoading: graphLoading } = useMemoryGraph(showGraph) const files = useMemo(() => data?.files ?? [], [data]) // Name → Pfad für [[wiki-links]] (Dateiname ohne .md, case-insensitive). const byName = useMemo(() => { const m = new Map() for (const f of files) m.set(f.name.toLowerCase(), f.path) return m }, [files]) const filtered = useMemo(() => { if (!q.trim()) return files const ql = q.toLowerCase() return files.filter((f) => f.title.toLowerCase().includes(ql) || f.path.toLowerCase().includes(ql)) }, [files, q]) const groups = useMemo(() => { const g: { dir: string; files: WissenFile[] }[] = [] for (const f of filtered) { const found = g.find((x) => x.dir === f.dir) if (found) found.files.push(f) else g.push({ dir: f.dir, files: [f] }) } // INDEX zuerst, dann Träume, dann Rest alphabetisch return g.sort((a, b) => (a.dir === "" ? -1 : b.dir === "" ? 1 : a.dir.localeCompare(b.dir))) }, [filtered]) // Beim ersten Laden: INDEX.md öffnen, falls vorhanden. useEffect(() => { if (!active && files.length) { const index = files.find((f) => f.path.toLowerCase() === "index.md") setActive(index?.path ?? files[0].path) } }, [files, active]) useEffect(() => { if (!active) return setDocLoading(true) api(`/api/wissen/datei?pfad=${encodeURIComponent(active)}`) .then(setDoc) .catch(() => setDoc({ path: active, content: "*(Notiz nicht ladbar)*", mtime: 0 })) .finally(() => setDocLoading(false)) }, [active]) const openWikiLink = (name: string) => { const path = byName.get(name.toLowerCase()) if (path) setActive(path) } return (

Wissen

Was sich die Box nachts erschließt — Traum-Notizen, Muster und Skill-Ideen aus dem Wissens-Vault, verlinkt wie ein Wiki.

{files.length > 0 && (
)}
{data && !data.available && (
Der Wissens-Vault liegt auf der Box (~/wissens-vault) — im lokalen Dev-Modus nicht verfügbar.
)} {isLoading && (
Vault wird geladen …
)} {data?.available && files.length === 0 && (
Noch keine Notizen. Der nächtliche Traum (03:15) legt sie an — morgen früh steht hier die erste.
)} {showGraph ? (
{graphLoading && (
Graph wird berechnet …
)} {graphData && ( { if (id) { setActive(id) setShowGraph(false) } }} /> )}
) : files.length > 0 && (
{/* Datei-Liste */} {/* Inhalt */}
{docLoading ? (
Notiz wird geladen …
) : doc ? ( <>
{doc.path} {doc.mtime > 0 && ( Stand {new Date(doc.mtime * 1000).toLocaleString("de-DE", { day: "2-digit", month: "2-digit", hour: "2-digit", minute: "2-digit" })} )}
) : null}
)}
) } // ── Leichtgewichtiger Markdown-Renderer (Zeilen-basiert, XSS-frei da reines JSX) ── function Markdown({ text, onWikiLink, known }: { text: string onWikiLink: (name: string) => void known: Map }) { const lines = text.split("\n") const out: React.ReactNode[] = [] let list: React.ReactNode[] = [] let code: string[] | null = null const flushList = (key: string) => { if (list.length) { out.push(
    {list}
) list = [] } } lines.forEach((raw, i) => { const key = `l${i}` if (code !== null) { if (raw.trimEnd() === "```") { out.push(
{code.join("\n")}
) code = null } else code.push(raw) return } if (raw.trimStart().startsWith("```")) { flushList(key); code = []; return } const line = raw.trimEnd() if (!line.trim()) { flushList(key); return } const h = line.match(/^(#{1,4})\s+(.*)$/) if (h) { flushList(key) const level = h[1].length const cls = level === 1 ? "text-lg font-bold mt-1 mb-3" : level === 2 ? "text-base font-bold mt-4 mb-2" : "text-sm font-bold mt-3 mb-1.5" out.push(

{inline(h[2], onWikiLink, known, key)}

) return } const li = line.match(/^\s*[-*•]\s+(.*)$/) if (li) { list.push(
  • {inline(li[1], onWikiLink, known, key)}
  • ) return } flushList(key) out.push(

    {inline(line, onWikiLink, known, key)}

    ) }) flushList("end") if (code !== null) out.push(
    {(code as string[]).join("\n")}
    ) return
    {out}
    } // Inline: [[wiki-links]], **fett**, *kursiv*, `code` — per Regex-Split, reines JSX (kein HTML-Inject). function inline(text: string, onWikiLink: (n: string) => void, known: Map, keyBase: string): React.ReactNode[] { const parts = text.split(/(\[\[[^\]]+\]\]|\*\*[^*]+\*\*|\*[^*]+\*|`[^`]+`)/g) return parts.map((p, i) => { const key = `${keyBase}-${i}` const wiki = p.match(/^\[\[([^\]|]+)(?:\|([^\]]+))?\]\]$/) if (wiki) { const target = wiki[1].trim() const label = (wiki[2] ?? wiki[1]).trim() const exists = known.has(target.toLowerCase()) return exists ? ( ) : ( {label} ) } if (p.startsWith("**") && p.endsWith("**")) return {p.slice(2, -2)} if (p.startsWith("*") && p.endsWith("*") && p.length > 2) return {p.slice(1, -1)} if (p.startsWith("`") && p.endsWith("`")) return {p.slice(1, -1)} return {p} }) }