From 8f359ef4c9272d42818e789e47a1e985b177ef4c Mon Sep 17 00:00:00 2001 From: Hitonabi Date: Fri, 7 Aug 2026 12:16:01 +0200 Subject: [PATCH] feat: obsidian-style graph view for wissens-vault --- backend/routers/wissen.py | 58 +++++++++++++++++++++++++++ frontend/src/lib/queries.ts | 3 +- frontend/src/views/WissenView.tsx | 65 ++++++++++++++++++++++++++----- 3 files changed, 116 insertions(+), 10 deletions(-) diff --git a/backend/routers/wissen.py b/backend/routers/wissen.py index 2f45495..336fdd3 100644 --- a/backend/routers/wissen.py +++ b/backend/routers/wissen.py @@ -3,6 +3,7 @@ Wiki in der Zentrale. Bewusst READ-ONLY — geschrieben wird der Vault nur vom T (und via Auftragsbuch-Entscheidungen); hier wird nur gelesen und navigiert.""" import os +import re import time from pathlib import Path @@ -90,3 +91,60 @@ def read_file(pfad: str) -> dict: raise except (ValueError, OSError): raise HTTPException(404, "Notiz nicht lesbar.") + + +@router.get("/wissen/graph") +def graph_vault() -> dict: + """Obsidian-artiger Graph des Vaults (Nodes = Dateien, Edges = Wiki-Links).""" + if not _available(): + return {"nodes": [], "edges": []} + + nodes = [] + edges = [] + # Für schnelle Link-Auflösung (case-insensitive Name -> relativer Pfad als ID) + name_to_id = {} + + # 1. Alle Nodes sammeln + for p in VAULT.rglob("*.md"): + if ".git" in p.parts: + continue + rel = p.relative_to(VAULT).as_posix() + name_to_id[p.stem.lower()] = rel + + # Kategorie aus dem Ordner ableiten + cat = "knowledge" + parent = p.parent.name if p.parent != VAULT else "" + if parent == "traeume": + cat = "events" + elif parent == "muster": + cat = "rules" + elif parent == "skill-kandidaten": + cat = "identity" + + nodes.append({ + "id": rel, + "label": p.stem, # stem ist oft kürzer/prägnanter als der Titel + "category": cat + }) + + # 2. Edges extrahieren (Wiki-Links [[Name]]) + link_rx = re.compile(r"\[\[([^\]]+)\]\]") + for n in nodes: + try: + target = VAULT / n["id"] + content = target.read_text(encoding="utf-8", errors="replace") + # Set, um mehrfache Links auf dieselbe Datei zu entduplizieren + found_links = set(m.group(1).strip().lower() for m in link_rx.finditer(content)) + + for link in found_links: + target_id = name_to_id.get(link) + if target_id and target_id != n["id"]: + edges.append({ + "source": n["id"], + "target": target_id, + "weight": 1.0 + }) + except OSError: + pass + + return {"nodes": nodes, "edges": edges} diff --git a/frontend/src/lib/queries.ts b/frontend/src/lib/queries.ts index b0beed3..169a85f 100644 --- a/frontend/src/lib/queries.ts +++ b/frontend/src/lib/queries.ts @@ -172,8 +172,9 @@ export const useGovernor = (refetchInterval: number = TAKT.normal) => export const useMemoryGraph = (enabled = true) => useQuery({ queryKey: qk.memoryGraph, - queryFn: () => api("/api/memory/graph"), + queryFn: () => api("/api/wissen/graph"), enabled, + staleTime: 60 * 1000, }) export const useHealth = () => diff --git a/frontend/src/views/WissenView.tsx b/frontend/src/views/WissenView.tsx index 01d9742..fcbf697 100644 --- a/frontend/src/views/WissenView.tsx +++ b/frontend/src/views/WissenView.tsx @@ -1,7 +1,9 @@ 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 } from "@/lib/queries" +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. @@ -23,6 +25,9 @@ export function WissenView() { 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]) @@ -74,13 +79,31 @@ export function WissenView() { return (
-
-

- Wissen -

-

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

+
+
+

+ 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 && ( @@ -101,7 +124,31 @@ export function WissenView() {
)} - {files.length > 0 && ( + {showGraph ? ( +
+ {graphLoading && ( +
+
+ Graph wird berechnet … +
+
+ )} + + {graphData && ( + { + if (id) { + setActive(id) + setShowGraph(false) + } + }} + /> + )} + +
+ ) : files.length > 0 && (
{/* Datei-Liste */}