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."""
|
(und via Auftragsbuch-Entscheidungen); hier wird nur gelesen und navigiert."""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import time
|
import time
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
@@ -90,3 +91,60 @@ def read_file(pfad: str) -> dict:
|
|||||||
raise
|
raise
|
||||||
except (ValueError, OSError):
|
except (ValueError, OSError):
|
||||||
raise HTTPException(404, "Notiz nicht lesbar.")
|
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) =>
|
export const useMemoryGraph = (enabled = true) =>
|
||||||
useQuery({
|
useQuery({
|
||||||
queryKey: qk.memoryGraph,
|
queryKey: qk.memoryGraph,
|
||||||
queryFn: () => api<MemoryGraph>("/api/memory/graph"),
|
queryFn: () => api<MemoryGraph>("/api/wissen/graph"),
|
||||||
enabled,
|
enabled,
|
||||||
|
staleTime: 60 * 1000,
|
||||||
})
|
})
|
||||||
|
|
||||||
export const useHealth = () =>
|
export const useHealth = () =>
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { useEffect, useMemo, useState } from "react"
|
import { useEffect, useMemo, useState } from "react"
|
||||||
import { Library, FileText, Sparkles, Loader2, FolderOpen, Search } from "lucide-react"
|
import { Library, FileText, Sparkles, Loader2, FolderOpen, Search } from "lucide-react"
|
||||||
import { api, type WissenDatei, type WissenFile } from "@/lib/api"
|
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"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
// Wissens-Vault: die nächtlichen Traum-Notizen der Box als klickbares Wiki.
|
// 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 [doc, setDoc] = useState<WissenDatei | null>(null)
|
||||||
const [docLoading, setDocLoading] = useState(false)
|
const [docLoading, setDocLoading] = useState(false)
|
||||||
const [q, setQ] = useState("")
|
const [q, setQ] = useState("")
|
||||||
|
const [showGraph, setShowGraph] = useState(false)
|
||||||
|
|
||||||
|
const { data: graphData, isLoading: graphLoading } = useMemoryGraph(showGraph)
|
||||||
|
|
||||||
const files = useMemo(() => data?.files ?? [], [data])
|
const files = useMemo(() => data?.files ?? [], [data])
|
||||||
|
|
||||||
@@ -74,13 +79,31 @@ export function WissenView() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-5">
|
<div className="space-y-5">
|
||||||
<div>
|
<div className="flex items-center justify-between">
|
||||||
<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">
|
<div>
|
||||||
Wissen
|
<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">
|
||||||
</h1>
|
Wissen
|
||||||
<p className="text-sm text-muted-foreground">
|
</h1>
|
||||||
Was sich die Box nachts erschließt — Traum-Notizen, Muster und Skill-Ideen aus dem Wissens-Vault, verlinkt wie ein Wiki.
|
<p className="text-sm text-muted-foreground">
|
||||||
</p>
|
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>
|
</div>
|
||||||
|
|
||||||
{data && !data.available && (
|
{data && !data.available && (
|
||||||
@@ -101,7 +124,31 @@ export function WissenView() {
|
|||||||
</div>
|
</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">
|
<div className="flex flex-col gap-4 lg:flex-row">
|
||||||
{/* Datei-Liste */}
|
{/* Datei-Liste */}
|
||||||
<aside className="w-full shrink-0 space-y-3 lg:w-72">
|
<aside className="w-full shrink-0 space-y-3 lg:w-72">
|
||||||
|
|||||||
Reference in New Issue
Block a user