feat: obsidian-style graph view for wissens-vault
This commit is contained in:
@@ -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}
|
||||
|
||||
@@ -172,8 +172,9 @@ export const useGovernor = (refetchInterval: number = TAKT.normal) =>
|
||||
export const useMemoryGraph = (enabled = true) =>
|
||||
useQuery({
|
||||
queryKey: qk.memoryGraph,
|
||||
queryFn: () => api<MemoryGraph>("/api/memory/graph"),
|
||||
queryFn: () => api<MemoryGraph>("/api/wissen/graph"),
|
||||
enabled,
|
||||
staleTime: 60 * 1000,
|
||||
})
|
||||
|
||||
export const useHealth = () =>
|
||||
|
||||
@@ -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<WissenDatei | null>(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,6 +79,7 @@ export function WissenView() {
|
||||
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="bg-gradient-to-r from-foreground via-foreground to-primary bg-clip-text font-space text-2xl font-bold tracking-tight text-transparent">
|
||||
Wissen
|
||||
@@ -82,6 +88,23 @@ export function WissenView() {
|
||||
Was sich die Box nachts erschließt — Traum-Notizen, Muster und Skill-Ideen aus dem Wissens-Vault, verlinkt wie ein Wiki.
|
||||
</p>
|
||||
</div>
|
||||
{files.length > 0 && (
|
||||
<div className="flex rounded-lg border border-border/60 bg-card p-1">
|
||||
<button
|
||||
onClick={() => setShowGraph(false)}
|
||||
className={cn("rounded-md px-3 py-1.5 text-xs font-medium transition-colors", !showGraph ? "bg-primary text-primary-foreground shadow" : "text-muted-foreground hover:bg-muted")}
|
||||
>
|
||||
Liste
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowGraph(true)}
|
||||
className={cn("rounded-md px-3 py-1.5 text-xs font-medium transition-colors", showGraph ? "bg-primary text-primary-foreground shadow" : "text-muted-foreground hover:bg-muted")}
|
||||
>
|
||||
Graph
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{data && !data.available && (
|
||||
<div className="rounded-2xl border border-amber-500/30 bg-amber-500/5 p-4 text-xs text-amber-300">
|
||||
@@ -101,7 +124,31 @@ export function WissenView() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{files.length > 0 && (
|
||||
{showGraph ? (
|
||||
<div className="mc-card flex h-[70vh] flex-col overflow-hidden p-0 relative">
|
||||
{graphLoading && (
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-background/50 z-10">
|
||||
<div className="flex items-center gap-2 rounded-2xl border border-border/60 bg-card p-6 text-xs text-muted-foreground">
|
||||
<Loader2 className="h-4 w-4 animate-spin" /> Graph wird berechnet …
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<GraphErrorBoundary>
|
||||
{graphData && (
|
||||
<GraphView
|
||||
data={graphData}
|
||||
selectedNodeId={active}
|
||||
onNodeSelect={(id) => {
|
||||
if (id) {
|
||||
setActive(id)
|
||||
setShowGraph(false)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</GraphErrorBoundary>
|
||||
</div>
|
||||
) : files.length > 0 && (
|
||||
<div className="flex flex-col gap-4 lg:flex-row">
|
||||
{/* Datei-Liste */}
|
||||
<aside className="w-full shrink-0 space-y-3 lg:w-72">
|
||||
|
||||
Reference in New Issue
Block a user