Feat: Gedächtnis-Tab überarbeitet — Sigma.js-Graph (skaliert), Liste als Default + gruppiert

Graph: reagraph (three.js, schwer, hing Renderer) → Sigma.js v3 + graphology (graph-optimiertes WebGL,
skaliert auf tausende Knoten), im App-Look: Kategorie-Farben, Knotengröße nach Verknüpfungen,
Hover-Highlight (Nachbarn hervor, Rest dimmt), Legende. Liste ist jetzt Default + nach Kategorie
gruppierte Sektionen (statt flacher Wand). reagraph deinstalliert → leichteres Bundle.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-06-28 11:09:33 +02:00
parent 7bc20a6302
commit d832f90ad6
16 changed files with 958 additions and 4808 deletions
+9 -1
View File
@@ -24,7 +24,7 @@ def updates() -> dict:
@router.get("/maintenance/update-details") @router.get("/maintenance/update-details")
def update_details(kind: str) -> dict: def update_details(kind: str) -> dict:
if kind not in ("os", "engine", "hermes"): if kind not in ("os", "engine", "swap", "hermes"):
raise HTTPException(400, "Unbekannte Update-Art.") raise HTTPException(400, "Unbekannte Update-Art.")
return maintenance.update_details(kind) return maintenance.update_details(kind)
@@ -52,6 +52,14 @@ def engine_update(body: SudoReq) -> dict:
return res return res
@router.post("/maintenance/swap-update")
def swap_update(body: SudoReq) -> dict:
res = maintenance.swap_update_job(body.sudo_password)
if not res:
raise HTTPException(400, "Kein Router-Update-Befehl gesetzt (MC_SWAP_UPDATE_CMD).")
return res
@router.post("/maintenance/hermes-update") @router.post("/maintenance/hermes-update")
def hermes_update() -> dict: def hermes_update() -> dict:
return maintenance.hermes_update_job() return maintenance.hermes_update_job()
+79
View File
@@ -30,6 +30,14 @@ ENGINE_PATH = os.environ.get("MC_ENGINE_PATH", "/opt/llamacpp-vulkan")
ENGINE_REPO = os.environ.get("MC_ENGINE_REPO", "ggml-org/llama.cpp") ENGINE_REPO = os.environ.get("MC_ENGINE_REPO", "ggml-org/llama.cpp")
_engine_cache = {"ts": 0.0, "avail": False} _engine_cache = {"ts": 0.0, "avail": False}
# Router = llama-swap (mostlygeek): proxyt Anfragen und wechselt die Modelle heiß. Eigenes
# Upstream-Projekt mit eigenem Release-Zyklus → getrennt von der Engine geführt.
SWAP_UPDATE_CMD = os.environ.get(
"MC_SWAP_UPDATE_CMD", f"sudo bash {_REPO_ROOT}/deploy/update-swap.sh")
SWAP_BIN = os.environ.get("MC_SWAP_BIN", "/usr/local/bin/llama-swap")
SWAP_REPO = os.environ.get("MC_SWAP_REPO", "mostlygeek/llama-swap")
_swap_cache = {"ts": 0.0, "avail": False}
def _installed_engine_build() -> int | None: def _installed_engine_build() -> int | None:
"""Build-Nummer der installierten llama-server-Binary (z.B. 9821), oder None. """Build-Nummer der installierten llama-server-Binary (z.B. 9821), oder None.
@@ -88,6 +96,39 @@ def _engine_update_available() -> bool:
return avail return avail
def _installed_swap_version() -> int | None:
"""Versions-Nummer der installierten llama-swap-Binary (z.B. 228), oder None."""
if not os.path.exists(SWAP_BIN):
return None
try:
out = subprocess.run([SWAP_BIN, "--version"], capture_output=True, text=True, timeout=15)
txt = (out.stdout or "") + (out.stderr or "")
if (m := re.search(r"version:\s*(\d+)", txt)) or (m := re.search(r"\bv?(\d{2,})\b", txt)):
return int(m.group(1))
except Exception:
return None
return None
def _swap_update_available() -> bool:
now = time.time()
if now - _swap_cache["ts"] < 3600:
return _swap_cache["avail"]
avail = False
try:
rel = httpx.get(f"https://api.github.com/repos/{SWAP_REPO}/releases/latest",
timeout=6, headers={"User-Agent": "MissionControl2"}).json()
tag = str(rel.get("tag_name", ""))
latest = int(m.group(1)) if (m := re.search(r"(\d{2,})", tag)) else None
installed = _installed_swap_version()
if latest is not None and installed is not None:
avail = latest > installed
except Exception:
avail = False
_swap_cache.update(ts=now, avail=avail)
return avail
_comp_cache = {"ts": 0.0, "data": []} _comp_cache = {"ts": 0.0, "data": []}
@@ -244,6 +285,7 @@ def _last_apt_update() -> float | None:
def updates() -> dict: def updates() -> dict:
ups = model_upgrades() ups = model_upgrades()
return {"os": _os_upgradable(), "engine": 1 if _engine_update_available() else 0, return {"os": _os_upgradable(), "engine": 1 if _engine_update_available() else 0,
"swap": 1 if _swap_update_available() else 0,
"models": len(ups), "model_list": ups, "last_check": _last_apt_update(), "models": len(ups), "model_list": ups, "last_check": _last_apt_update(),
"components": _components_cached()} "components": _components_cached()}
@@ -289,6 +331,26 @@ def engine_update_details() -> dict:
return info return info
def swap_update_details() -> dict:
"""Installierte vs. neueste llama-swap-Version + Release-Name/-Notizen/-Link."""
info: dict = {"kind": "swap", "installed_build": _installed_swap_version(),
"latest_build": None, "latest_tag": None, "name": None,
"url": None, "body": None}
try:
rel = httpx.get(f"https://api.github.com/repos/{SWAP_REPO}/releases/latest",
timeout=8, headers={"User-Agent": "MissionControl2"}).json()
tag = str(rel.get("tag_name", ""))
info["latest_tag"] = tag
info["latest_build"] = int(m.group(1)) if (m := re.search(r"(\d{2,})", tag)) else None
info["name"] = rel.get("name") or tag
info["url"] = rel.get("html_url")
body = (rel.get("body") or "").strip()
info["body"] = body[:2000] if body else None
except Exception as exc: # noqa: BLE001
info["error"] = str(exc)
return info
def hermes_update_details() -> dict: def hermes_update_details() -> dict:
"""Commits, die ein Hermes-Update einspielen würde (HEAD..origin/<branch>).""" """Commits, die ein Hermes-Update einspielen würde (HEAD..origin/<branch>)."""
info: dict = {"kind": "hermes", "branch": None, "behind": 0, "commits": []} info: dict = {"kind": "hermes", "branch": None, "behind": 0, "commits": []}
@@ -319,6 +381,7 @@ def hermes_update_details() -> dict:
def update_details(kind: str) -> dict: def update_details(kind: str) -> dict:
return {"os": os_update_details, "engine": engine_update_details, return {"os": os_update_details, "engine": engine_update_details,
"swap": swap_update_details,
"hermes": hermes_update_details}.get(kind, lambda: {"error": "unbekannt"})() "hermes": hermes_update_details}.get(kind, lambda: {"error": "unbekannt"})()
@@ -429,6 +492,22 @@ def engine_update_job(sudo_password: str | None = None) -> dict | None:
return {"ok": True, "job_id": job_id} return {"ok": True, "job_id": job_id}
def swap_update_job(sudo_password: str | None = None) -> dict | None:
if not SWAP_UPDATE_CMD:
return None
if err := check_sudo_needs_password(sudo_password):
return err
def on_done():
_swap_cache.update(ts=0.0, avail=False) # Cache leeren → frischer Versions-Vergleich
# update-swap.sh läuft via sudo als root, ersetzt die Binary und startet llama-swap neu.
job_id = jobengine.start_job(["bash", "-c", SWAP_UPDATE_CMD],
"Router-Update (llama-swap)",
on_done=on_done, sudo_password=sudo_password)
return {"ok": True, "job_id": job_id}
def hermes_update_job() -> dict: def hermes_update_job() -> dict:
"""Hermes-Agent aktualisieren wie die CLI (`hermes update` = git pull + Deps), danach """Hermes-Agent aktualisieren wie die CLI (`hermes update` = git pull + Deps), danach
den Gateway neu starten. Davor ein Sicherheits-Backup (unser deploy/backup.sh). Kein sudo den Gateway neu starten. Davor ein Sicherheits-Backup (unser deploy/backup.sh). Kein sudo
+33
View File
@@ -0,0 +1,33 @@
#!/usr/bin/env bash
# Aktualisiert den llama-swap-Router (mostlygeek/llama-swap) auf den neuesten Release.
# Wird vom MC2-„Router Update"-Button via `sudo bash …` als ROOT aufgerufen
# (deshalb KEIN internes sudo). Ersetzt die Binary und startet llama-swap neu.
set -euo pipefail
SWAP_BIN="${MC_SWAP_BIN:-/usr/local/bin/llama-swap}"
PIN_VER="${MC_SWAP_VERSION:-}" # leer = neuester Release
if [ -n "$PIN_VER" ]; then
TAG="$PIN_VER"
else
TAG="$(curl -s https://api.github.com/repos/mostlygeek/llama-swap/releases/latest | jq -r .tag_name)"
fi
[ -n "$TAG" ] && [ "$TAG" != "null" ] || { echo "Konnte neuesten Release-Tag nicht ermitteln"; exit 1; }
# Asset-Name nutzt die nackte Nummer (z.B. v230 -> 230): llama-swap_230_linux_amd64.tar.gz
NUM="${TAG#v}"
ASSET="llama-swap_${NUM}_linux_amd64.tar.gz"
URL="https://github.com/mostlygeek/llama-swap/releases/download/${TAG}/${ASSET}"
TMP="$(mktemp -d)"
trap 'rm -rf "$TMP"' EXIT
echo "Lade llama-swap ${TAG}"
curl -fsSL -o "$TMP/s.tgz" "$URL"
tar xzf "$TMP/s.tgz" -C "$TMP"
NEW="$(find "$TMP" -type f -name llama-swap | head -1)"
[ -n "$NEW" ] || { echo "llama-swap-Binary im Archiv nicht gefunden"; exit 1; }
install -m 0755 "$NEW" "$SWAP_BIN"
test -x "$SWAP_BIN"
systemctl restart llama-swap
echo "Router aktualisiert auf ${TAG} und llama-swap neugestartet."
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -7,8 +7,8 @@
<link rel="manifest" href="/manifest.webmanifest" /> <link rel="manifest" href="/manifest.webmanifest" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" /> <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<title>Mission Control 2.0</title> <title>Mission Control 2.0</title>
<script type="module" crossorigin src="/assets/index-Qs-v42ar.js"></script> <script type="module" crossorigin src="/assets/index-BhNoAezr.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index--0Qg2tjI.css"> <link rel="stylesheet" crossorigin href="/assets/index-BQ6s_c3E.css">
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
+17 -477
View File
@@ -14,11 +14,13 @@
"@tanstack/react-query": "^5.101.1", "@tanstack/react-query": "^5.101.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cmdk": "^1.1.1", "cmdk": "^1.1.1",
"graphology": "^0.26.0",
"graphology-layout-forceatlas2": "^0.10.1",
"lucide-react": "^0.460.0", "lucide-react": "^0.460.0",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"reagraph": "^4.22.0",
"recharts": "^3.9.0", "recharts": "^3.9.0",
"sigma": "^3.0.3",
"tailwind-merge": "^2.5.5", "tailwind-merge": "^2.5.5",
"three": "^0.169.0" "three": "^0.169.0"
}, },
@@ -817,12 +819,6 @@
"@jridgewell/sourcemap-codec": "^1.4.14" "@jridgewell/sourcemap-codec": "^1.4.14"
} }
}, },
"node_modules/@mediapipe/tasks-vision": {
"version": "0.10.8",
"resolved": "https://registry.npmjs.org/@mediapipe/tasks-vision/-/tasks-vision-0.10.8.tgz",
"integrity": "sha512-Rp7ll8BHrKB3wXaRFKhrltwZl1CiXGdibPxuWXvqGnKTnv8fqa/nvftYNuSbf+pbJWKYCXdBtYTITdAUTGGh0Q==",
"license": "Apache-2.0"
},
"node_modules/@monogrid/gainmap-js": { "node_modules/@monogrid/gainmap-js": {
"version": "3.4.0", "version": "3.4.0",
"resolved": "https://registry.npmjs.org/@monogrid/gainmap-js/-/gainmap-js-3.4.0.tgz", "resolved": "https://registry.npmjs.org/@monogrid/gainmap-js/-/gainmap-js-3.4.0.tgz",
@@ -1301,57 +1297,6 @@
} }
} }
}, },
"node_modules/@react-spring/animated": {
"version": "9.6.1",
"resolved": "https://registry.npmjs.org/@react-spring/animated/-/animated-9.6.1.tgz",
"integrity": "sha512-ls/rJBrAqiAYozjLo5EPPLLOb1LM0lNVQcXODTC1SMtS6DbuBCPaKco5svFUQFMP2dso3O+qcC4k9FsKc0KxMQ==",
"license": "MIT",
"dependencies": {
"@react-spring/shared": "~9.6.1",
"@react-spring/types": "~9.6.1"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/@react-spring/core": {
"version": "9.6.1",
"resolved": "https://registry.npmjs.org/@react-spring/core/-/core-9.6.1.tgz",
"integrity": "sha512-3HAAinAyCPessyQNNXe5W0OHzRfa8Yo5P748paPcmMowZ/4sMfaZ2ZB6e5x5khQI8NusOHj8nquoutd6FRY5WQ==",
"license": "MIT",
"dependencies": {
"@react-spring/animated": "~9.6.1",
"@react-spring/rafz": "~9.6.1",
"@react-spring/shared": "~9.6.1",
"@react-spring/types": "~9.6.1"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/react-spring/donate"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/@react-spring/rafz": {
"version": "9.6.1",
"resolved": "https://registry.npmjs.org/@react-spring/rafz/-/rafz-9.6.1.tgz",
"integrity": "sha512-v6qbgNRpztJFFfSE3e2W1Uz+g8KnIBs6SmzCzcVVF61GdGfGOuBrbjIcp+nUz301awVmREKi4eMQb2Ab2gGgyQ==",
"license": "MIT"
},
"node_modules/@react-spring/shared": {
"version": "9.6.1",
"resolved": "https://registry.npmjs.org/@react-spring/shared/-/shared-9.6.1.tgz",
"integrity": "sha512-PBFBXabxFEuF8enNLkVqMC9h5uLRBo6GQhRMQT/nRTnemVENimgRd+0ZT4yFnAQ0AxWNiJfX3qux+bW2LbG6Bw==",
"license": "MIT",
"dependencies": {
"@react-spring/rafz": "~9.6.1",
"@react-spring/types": "~9.6.1"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/@react-spring/three": { "node_modules/@react-spring/three": {
"version": "9.7.5", "version": "9.7.5",
"resolved": "https://registry.npmjs.org/@react-spring/three/-/three-9.7.5.tgz", "resolved": "https://registry.npmjs.org/@react-spring/three/-/three-9.7.5.tgz",
@@ -1425,12 +1370,6 @@
"integrity": "sha512-HVj7LrZ4ReHWBimBvu2SKND3cDVUPWKLqRTmWe/fNY6o1owGOX0cAHbdPDTMelgBlVbrTKrre6lFkhqGZErK/g==", "integrity": "sha512-HVj7LrZ4ReHWBimBvu2SKND3cDVUPWKLqRTmWe/fNY6o1owGOX0cAHbdPDTMelgBlVbrTKrre6lFkhqGZErK/g==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@react-spring/types": {
"version": "9.6.1",
"resolved": "https://registry.npmjs.org/@react-spring/types/-/types-9.6.1.tgz",
"integrity": "sha512-POu8Mk0hIU3lRXB3bGIGe4VHIwwDsQyoD1F394OK7STTiX9w4dG3cTLljjYswkQN+hDSHRrj4O36kuVa7KPU8Q==",
"license": "MIT"
},
"node_modules/@react-three/drei": { "node_modules/@react-three/drei": {
"version": "9.122.0", "version": "9.122.0",
"resolved": "https://registry.npmjs.org/@react-three/drei/-/drei-9.122.0.tgz", "resolved": "https://registry.npmjs.org/@react-three/drei/-/drei-9.122.0.tgz",
@@ -2597,12 +2536,6 @@
"integrity": "sha512-mMy8/ODcKhab808co15eW+yN+HgXoQxRQHTiBV9Mrvl1r0ufnid7YOcI+gi4eUWSWl9ezD6TW2KXccrL8HCh2A==", "integrity": "sha512-mMy8/ODcKhab808co15eW+yN+HgXoQxRQHTiBV9Mrvl1r0ufnid7YOcI+gi4eUWSWl9ezD6TW2KXccrL8HCh2A==",
"license": "BSD-3-Clause" "license": "BSD-3-Clause"
}, },
"node_modules/@yomguithereal/helpers": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@yomguithereal/helpers/-/helpers-1.1.1.tgz",
"integrity": "sha512-UYvAq/XCA7xoh1juWDYsq3W0WywOB+pz8cgVnE1b45ZfdMhBvHDrgmSFG3jXeZSr2tMTYLGHFHON+ekG05Jebg==",
"license": "MIT"
},
"node_modules/aria-hidden": { "node_modules/aria-hidden": {
"version": "1.2.6", "version": "1.2.6",
"resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz",
@@ -2745,12 +2678,6 @@
], ],
"license": "CC-BY-4.0" "license": "CC-BY-4.0"
}, },
"node_modules/classnames": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz",
"integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==",
"license": "MIT"
},
"node_modules/clsx": { "node_modules/clsx": {
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
@@ -2833,12 +2760,6 @@
"node": ">=12" "node": ">=12"
} }
}, },
"node_modules/d3-binarytree": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/d3-binarytree/-/d3-binarytree-1.0.2.tgz",
"integrity": "sha512-cElUNH+sHu95L04m92pG73t2MEJXKu+GeKUN1TJkFsu93E5W8E9Sc3kHEGJKgenGvj19m6upSn2EunvMgMD2Yw==",
"license": "MIT"
},
"node_modules/d3-color": { "node_modules/d3-color": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
@@ -2848,15 +2769,6 @@
"node": ">=12" "node": ">=12"
} }
}, },
"node_modules/d3-dispatch": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz",
"integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-ease": { "node_modules/d3-ease": {
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
@@ -2866,22 +2778,6 @@
"node": ">=12" "node": ">=12"
} }
}, },
"node_modules/d3-force-3d": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/d3-force-3d/-/d3-force-3d-3.0.6.tgz",
"integrity": "sha512-4tsKHUPLOVkyfEffZo1v6sFHvGFwAIIjt/W8IThbp08DYAsXZck+2pSHEG5W1+gQgEvFLdZkYvmJAbRM2EzMnA==",
"license": "MIT",
"dependencies": {
"d3-binarytree": "1",
"d3-dispatch": "1 - 3",
"d3-octree": "1",
"d3-quadtree": "1 - 3",
"d3-timer": "1 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-format": { "node_modules/d3-format": {
"version": "3.1.2", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz",
@@ -2891,15 +2787,6 @@
"node": ">=12" "node": ">=12"
} }
}, },
"node_modules/d3-hierarchy": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz",
"integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-interpolate": { "node_modules/d3-interpolate": {
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
@@ -2912,12 +2799,6 @@
"node": ">=12" "node": ">=12"
} }
}, },
"node_modules/d3-octree": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/d3-octree/-/d3-octree-1.1.0.tgz",
"integrity": "sha512-F8gPlqpP+HwRPMO/8uOu5wjH110+6q4cgJvgJT6vlpy3BEaDIKlTZrgHKZSp/i1InRpVfh4puY/kvL6MxK930A==",
"license": "MIT"
},
"node_modules/d3-path": { "node_modules/d3-path": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz",
@@ -2927,15 +2808,6 @@
"node": ">=12" "node": ">=12"
} }
}, },
"node_modules/d3-quadtree": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz",
"integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-scale": { "node_modules/d3-scale": {
"version": "4.0.2", "version": "4.0.2",
"resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
@@ -3059,12 +2931,6 @@
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/ellipsize": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/ellipsize/-/ellipsize-0.5.1.tgz",
"integrity": "sha512-0jEAyuIRU6U8MN0S5yUqIrkK/AQWkChh642N3zQuGV57s9bsUWYLc0jJOoDIUkZ2sbEL3ySq8xfq71BvG4q3hw==",
"license": "MIT"
},
"node_modules/enhanced-resolve": { "node_modules/enhanced-resolve": {
"version": "5.21.6", "version": "5.21.6",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.6.tgz", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.6.tgz",
@@ -3228,44 +3094,17 @@
"license": "ISC" "license": "ISC"
}, },
"node_modules/graphology": { "node_modules/graphology": {
"version": "0.25.4", "version": "0.26.0",
"resolved": "https://registry.npmjs.org/graphology/-/graphology-0.25.4.tgz", "resolved": "https://registry.npmjs.org/graphology/-/graphology-0.26.0.tgz",
"integrity": "sha512-33g0Ol9nkWdD6ulw687viS8YJQBxqG5LWII6FI6nul0pq6iM2t5EKquOTFDbyTblRB3O9I+7KX4xI8u5ffekAQ==", "integrity": "sha512-8SSImzgUUYC89Z042s+0r/vMibY7GX/Emz4LDO5e7jYXhuoWfHISPFJYjpRLUSJGq6UQ6xlenvX1p/hJdfXuXg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"events": "^3.3.0", "events": "^3.3.0"
"obliterator": "^2.0.2"
}, },
"peerDependencies": { "peerDependencies": {
"graphology-types": ">=0.24.0" "graphology-types": ">=0.24.0"
} }
}, },
"node_modules/graphology-indices": {
"version": "0.17.0",
"resolved": "https://registry.npmjs.org/graphology-indices/-/graphology-indices-0.17.0.tgz",
"integrity": "sha512-A7RXuKQvdqSWOpn7ZVQo4S33O0vCfPBnUSf7FwE0zNCasqwZVUaCXePuWo5HBpWw68KJcwObZDHpFk6HKH6MYQ==",
"license": "MIT",
"dependencies": {
"graphology-utils": "^2.4.2",
"mnemonist": "^0.39.0"
},
"peerDependencies": {
"graphology-types": ">=0.20.0"
}
},
"node_modules/graphology-layout": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/graphology-layout/-/graphology-layout-0.6.1.tgz",
"integrity": "sha512-m9aMvbd0uDPffUCFPng5ibRkb2pmfNvdKjQWeZrf71RS1aOoat5874+DcyNfMeCT4aQguKC7Lj9eCbqZj/h8Ag==",
"license": "MIT",
"dependencies": {
"graphology-utils": "^2.3.0",
"pandemonium": "^2.4.0"
},
"peerDependencies": {
"graphology-types": ">=0.19.0"
}
},
"node_modules/graphology-layout-forceatlas2": { "node_modules/graphology-layout-forceatlas2": {
"version": "0.10.1", "version": "0.10.1",
"resolved": "https://registry.npmjs.org/graphology-layout-forceatlas2/-/graphology-layout-forceatlas2-0.10.1.tgz", "resolved": "https://registry.npmjs.org/graphology-layout-forceatlas2/-/graphology-layout-forceatlas2-0.10.1.tgz",
@@ -3278,49 +3117,6 @@
"graphology-types": ">=0.19.0" "graphology-types": ">=0.19.0"
} }
}, },
"node_modules/graphology-layout-noverlap": {
"version": "0.4.2",
"resolved": "https://registry.npmjs.org/graphology-layout-noverlap/-/graphology-layout-noverlap-0.4.2.tgz",
"integrity": "sha512-13WwZSx96zim6l1dfZONcqLh3oqyRcjIBsqz2c2iJ3ohgs3605IDWjldH41Gnhh462xGB1j6VGmuGhZ2FKISXA==",
"license": "MIT",
"dependencies": {
"graphology-utils": "^2.3.0"
},
"peerDependencies": {
"graphology-types": ">=0.19.0"
}
},
"node_modules/graphology-metrics": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/graphology-metrics/-/graphology-metrics-2.4.0.tgz",
"integrity": "sha512-7WOfOP+mFLCaTJx55Qg4eY+211vr1/b3D/R3biz3SXGhAaCVcWYkfabnmO4O4WBNWANEHtVnFrGgJ0kj6MM6xw==",
"license": "MIT",
"dependencies": {
"graphology-indices": "^0.17.0",
"graphology-shortest-path": "^2.0.0",
"graphology-utils": "^2.4.4",
"mnemonist": "^0.39.0",
"pandemonium": "2.4.1"
},
"peerDependencies": {
"graphology-types": ">=0.20.0"
}
},
"node_modules/graphology-shortest-path": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/graphology-shortest-path/-/graphology-shortest-path-2.1.0.tgz",
"integrity": "sha512-KbT9CTkP/u72vGEJzyRr24xFC7usI9Es3LMmCPHGwQ1KTsoZjxwA9lMKxfU0syvT/w+7fZUdB/Hu2wWYcJBm6Q==",
"license": "MIT",
"dependencies": {
"@yomguithereal/helpers": "^1.1.1",
"graphology-indices": "^0.17.0",
"graphology-utils": "^2.4.3",
"mnemonist": "^0.39.0"
},
"peerDependencies": {
"graphology-types": ">=0.20.0"
}
},
"node_modules/graphology-types": { "node_modules/graphology-types": {
"version": "0.24.8", "version": "0.24.8",
"resolved": "https://registry.npmjs.org/graphology-types/-/graphology-types-0.24.8.tgz", "resolved": "https://registry.npmjs.org/graphology-types/-/graphology-types-0.24.8.tgz",
@@ -3343,12 +3139,6 @@
"integrity": "sha512-VSIRpLfRwlAAdGL4wiTucx2ScRipo0ed1FBatWkyt832jC4CReKstga6yIhYVwGu9LOBjuX9wzmRMeQdBJtzEA==", "integrity": "sha512-VSIRpLfRwlAAdGL4wiTucx2ScRipo0ed1FBatWkyt832jC4CReKstga6yIhYVwGu9LOBjuX9wzmRMeQdBJtzEA==",
"license": "Apache-2.0" "license": "Apache-2.0"
}, },
"node_modules/hold-event": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/hold-event/-/hold-event-0.2.0.tgz",
"integrity": "sha512-rko5P1XgHzy4B0NR0xVHEpWPgj0i23f8Mf8qsOugd1CHvfLR0PyIyy+8TAQQA9v8qAa1OZ4XuCKk04rxmPGHNQ==",
"license": "MIT"
},
"node_modules/ieee754": { "node_modules/ieee754": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
@@ -3817,15 +3607,6 @@
"integrity": "sha512-ZhoIoL7TNV4s5B6+rx5mC//fw8/POGyNxS/DZyCJeiZ12ScLfVwRE/GfsxwiTkMYYD5DmK2/JXnEVXqL4rF+Sw==", "integrity": "sha512-ZhoIoL7TNV4s5B6+rx5mC//fw8/POGyNxS/DZyCJeiZ12ScLfVwRE/GfsxwiTkMYYD5DmK2/JXnEVXqL4rF+Sw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/mnemonist": {
"version": "0.39.8",
"resolved": "https://registry.npmjs.org/mnemonist/-/mnemonist-0.39.8.tgz",
"integrity": "sha512-vyWo2K3fjrUw8YeeZ1zF0fy6Mu59RHokURlld8ymdUPjMlD9EC9ov1/YPqTgqRvUN9nTr3Gqfz29LYAmu0PHPQ==",
"license": "MIT",
"dependencies": {
"obliterator": "^2.0.1"
}
},
"node_modules/ms": { "node_modules/ms": {
"version": "2.1.3", "version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@@ -3871,21 +3652,6 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/obliterator": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/obliterator/-/obliterator-2.0.5.tgz",
"integrity": "sha512-42CPE9AhahZRsMNslczq0ctAEtqk8Eka26QofnqC346BZdHDySk3LWka23LI7ULIw11NmltpiLagIq8gBozxTw==",
"license": "MIT"
},
"node_modules/pandemonium": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/pandemonium/-/pandemonium-2.4.1.tgz",
"integrity": "sha512-wRqjisUyiUfXowgm7MFH2rwJzKIr20rca5FsHXCMNm1W5YPP1hCtrZfgmQ62kP7OZ7Xt+cR858aB28lu5NX55g==",
"license": "MIT",
"dependencies": {
"mnemonist": "^0.39.2"
}
},
"node_modules/path-key": { "node_modules/path-key": {
"version": "3.1.1", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
@@ -4021,16 +3787,6 @@
"license": "MIT", "license": "MIT",
"peer": true "peer": true
}, },
"node_modules/react-merge-refs": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/react-merge-refs/-/react-merge-refs-1.1.0.tgz",
"integrity": "sha512-alTKsjEL0dKH/ru1Iyn7vliS2QRcBp9zZPGoWxUOvRGWPUYgjo+V01is7p04It6KhgrzhJGnIj9GgX8W4bZoCQ==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/gregberge"
}
},
"node_modules/react-reconciler": { "node_modules/react-reconciler": {
"version": "0.27.0", "version": "0.27.0",
"resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.27.0.tgz", "resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.27.0.tgz",
@@ -4173,212 +3929,6 @@
} }
} }
}, },
"node_modules/reagraph": {
"version": "4.22.0",
"resolved": "https://registry.npmjs.org/reagraph/-/reagraph-4.22.0.tgz",
"integrity": "sha512-opEQdl/xkaKJFIywkQTamNFzByd78szB8dZa57F3Yj69MwVgh7q7AGzv9S5sYlorqYrKA8O+Xm9rTpWXsFiQKg==",
"license": "Apache-2.0",
"dependencies": {
"@react-spring/three": "9.6.1",
"@react-three/fiber": "8.13.5",
"@use-gesture/react": "^10.3.1",
"camera-controls": "^2.8.3",
"classnames": "^2.5.1",
"d3-array": "^3.2.4",
"d3-force-3d": "^3.0.3",
"d3-hierarchy": "^3.1.2",
"d3-scale": "^4.0.2",
"ellipsize": "^0.5.1",
"glodrei": "^0.0.1",
"graphology": "^0.25.4",
"graphology-layout": "^0.6.1",
"graphology-layout-forceatlas2": "^0.10.1",
"graphology-layout-noverlap": "^0.4.2",
"graphology-metrics": "^2.1.0",
"graphology-shortest-path": "^2.0.2",
"hold-event": "^0.2.0",
"three": "^0.154.0",
"three-stdlib": "^2.23.13",
"zustand": "4.3.9"
},
"peerDependencies": {
"react": ">=16",
"react-dom": ">=16"
}
},
"node_modules/reagraph/node_modules/@react-spring/three": {
"version": "9.6.1",
"resolved": "https://registry.npmjs.org/@react-spring/three/-/three-9.6.1.tgz",
"integrity": "sha512-Tyw2YhZPKJAX3t2FcqvpLRb71CyTe1GvT3V+i+xJzfALgpk10uPGdGaQQ5Xrzmok1340DAeg2pR/MCfaW7b8AA==",
"license": "MIT",
"dependencies": {
"@react-spring/animated": "~9.6.1",
"@react-spring/core": "~9.6.1",
"@react-spring/shared": "~9.6.1",
"@react-spring/types": "~9.6.1"
},
"peerDependencies": {
"@react-three/fiber": ">=6.0",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0",
"three": ">=0.126"
}
},
"node_modules/reagraph/node_modules/@react-three/fiber": {
"version": "8.13.5",
"resolved": "https://registry.npmjs.org/@react-three/fiber/-/fiber-8.13.5.tgz",
"integrity": "sha512-x9QdsaB/Wm/6NGvRXQahPPWfn2dQce7Fg3C2r00NNzyDdqRKw32YavL+WEqjZOOd0nvFpzv7FtaKc+VCOTR59w==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.17.8",
"@types/react-reconciler": "^0.26.7",
"its-fine": "^1.0.6",
"react-reconciler": "^0.27.0",
"react-use-measure": "^2.1.1",
"scheduler": "^0.21.0",
"suspend-react": "^0.1.3",
"zustand": "^3.7.1"
},
"peerDependencies": {
"expo": ">=43.0",
"expo-asset": ">=8.4",
"expo-gl": ">=11.0",
"react": ">=18.0",
"react-dom": ">=18.0",
"react-native": ">=0.64",
"three": ">=0.133"
},
"peerDependenciesMeta": {
"expo": {
"optional": true
},
"expo-asset": {
"optional": true
},
"expo-gl": {
"optional": true
},
"react-dom": {
"optional": true
},
"react-native": {
"optional": true
}
}
},
"node_modules/reagraph/node_modules/@react-three/fiber/node_modules/zustand": {
"version": "3.7.2",
"resolved": "https://registry.npmjs.org/zustand/-/zustand-3.7.2.tgz",
"integrity": "sha512-PIJDIZKtokhof+9+60cpockVOq05sJzHCriyvaLBmEJixseQ1a5Kdov6fWZfWOu5SK9c+FhH1jU0tntLxRJYMA==",
"license": "MIT",
"engines": {
"node": ">=12.7.0"
},
"peerDependencies": {
"react": ">=16.8"
},
"peerDependenciesMeta": {
"react": {
"optional": true
}
}
},
"node_modules/reagraph/node_modules/glodrei": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/glodrei/-/glodrei-0.0.1.tgz",
"integrity": "sha512-DMx6ElCSwh1pR4IyDS3LvyFwZHSCCKCqdqo8P1G7klQtqH6PcOjleduCDsHehDtyYQ1E4dzVeoEzHIL1DIxjag==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.11.2",
"@mediapipe/tasks-vision": "0.10.8",
"@react-spring/three": "~9.6.1",
"@use-gesture/react": "^10.2.24",
"camera-controls": "^2.4.2",
"cross-env": "^7.0.3",
"detect-gpu": "^5.0.28",
"glsl-noise": "^0.0.0",
"maath": "^0.10.7",
"meshline": "^3.1.6",
"react-composer": "^5.0.3",
"react-merge-refs": "^1.1.0",
"stats-gl": "^2.0.0",
"stats.js": "^0.17.0",
"suspend-react": "^0.1.3",
"three-mesh-bvh": "^0.7.0",
"three-stdlib": "^2.29.4",
"troika-three-text": "^0.47.2",
"tunnel-rat": "^0.1.2",
"utility-types": "^3.10.0",
"uuid": "^9.0.1",
"zustand": "^3.7.1"
},
"peerDependencies": {
"@react-three/fiber": ">=8.0",
"react": ">=18.0",
"react-dom": ">=18.0",
"three": ">=0.137"
},
"peerDependenciesMeta": {
"react-dom": {
"optional": true
}
}
},
"node_modules/reagraph/node_modules/glodrei/node_modules/troika-three-text": {
"version": "0.47.2",
"resolved": "https://registry.npmjs.org/troika-three-text/-/troika-three-text-0.47.2.tgz",
"integrity": "sha512-qylT0F+U7xGs+/PEf3ujBdJMYWbn0Qci0kLqI5BJG2kW1wdg4T1XSxneypnF05DxFqJhEzuaOR9S2SjiyknMng==",
"license": "MIT",
"dependencies": {
"bidi-js": "^1.0.2",
"troika-three-utils": "^0.47.2",
"troika-worker-utils": "^0.47.2",
"webgl-sdf-generator": "1.1.1"
},
"peerDependencies": {
"three": ">=0.125.0"
}
},
"node_modules/reagraph/node_modules/glodrei/node_modules/troika-three-text/node_modules/troika-three-utils": {
"version": "0.47.2",
"resolved": "https://registry.npmjs.org/troika-three-utils/-/troika-three-utils-0.47.2.tgz",
"integrity": "sha512-/28plhCxfKtH7MSxEGx8e3b/OXU5A0xlwl+Sbdp0H8FXUHKZDoksduEKmjQayXYtxAyuUiCRunYIv/8Vi7aiyg==",
"license": "MIT",
"peerDependencies": {
"three": ">=0.125.0"
}
},
"node_modules/reagraph/node_modules/glodrei/node_modules/zustand": {
"version": "3.7.2",
"resolved": "https://registry.npmjs.org/zustand/-/zustand-3.7.2.tgz",
"integrity": "sha512-PIJDIZKtokhof+9+60cpockVOq05sJzHCriyvaLBmEJixseQ1a5Kdov6fWZfWOu5SK9c+FhH1jU0tntLxRJYMA==",
"license": "MIT",
"engines": {
"node": ">=12.7.0"
},
"peerDependencies": {
"react": ">=16.8"
},
"peerDependenciesMeta": {
"react": {
"optional": true
}
}
},
"node_modules/reagraph/node_modules/scheduler": {
"version": "0.21.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.21.0.tgz",
"integrity": "sha512-1r87x5fz9MXqswA2ERLo0EbOAU74DpIUO090gIasYTqlVoJeMcl+Z1Rg7WHz+qtPujhS/hGIt9kxZOYBV3faRQ==",
"license": "MIT",
"dependencies": {
"loose-envify": "^1.1.0"
}
},
"node_modules/reagraph/node_modules/three": {
"version": "0.154.0",
"resolved": "https://registry.npmjs.org/three/-/three-0.154.0.tgz",
"integrity": "sha512-Uzz8C/5GesJzv8i+Y2prEMYUwodwZySPcNhuJUdsVMH2Yn4Nm8qlbQe6qRN5fOhg55XB0WiLfTPBxVHxpE60ug==",
"license": "MIT"
},
"node_modules/recharts": { "node_modules/recharts": {
"version": "3.9.0", "version": "3.9.0",
"resolved": "https://registry.npmjs.org/recharts/-/recharts-3.9.0.tgz", "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.9.0.tgz",
@@ -4524,6 +4074,16 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/sigma": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/sigma/-/sigma-3.0.3.tgz",
"integrity": "sha512-5H0zFlx6/NTQpqBg4Rm569ZOpnBOXMaS25UQThIWMU3XyzI5AhmorK/gnl87BvJBLhQd0tW4C0LIp3enWzMoNw==",
"license": "MIT",
"dependencies": {
"events": "^3.3.0",
"graphology-utils": "^2.5.2"
}
},
"node_modules/source-map-js": { "node_modules/source-map-js": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
@@ -4686,12 +4246,6 @@
"three": ">=0.125.0" "three": ">=0.125.0"
} }
}, },
"node_modules/troika-worker-utils": {
"version": "0.47.2",
"resolved": "https://registry.npmjs.org/troika-worker-utils/-/troika-worker-utils-0.47.2.tgz",
"integrity": "sha512-mzss4MeyzUkYBppn4x5cdAqrhBHFEuVmMMgLMTyFV23x6GvQMyo+/R5E5Lsbrt7WSt5RfvewjcwD1DChRTA9lA==",
"license": "MIT"
},
"node_modules/tslib": { "node_modules/tslib": {
"version": "2.8.1", "version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
@@ -4820,20 +4374,6 @@
"node": ">= 4" "node": ">= 4"
} }
}, },
"node_modules/uuid": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
"integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
"deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"license": "MIT",
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/victory-vendor": { "node_modules/victory-vendor": {
"version": "37.3.6", "version": "37.3.6",
"resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz", "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz",
+3 -1
View File
@@ -15,11 +15,13 @@
"@tanstack/react-query": "^5.101.1", "@tanstack/react-query": "^5.101.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cmdk": "^1.1.1", "cmdk": "^1.1.1",
"graphology": "^0.26.0",
"graphology-layout-forceatlas2": "^0.10.1",
"lucide-react": "^0.460.0", "lucide-react": "^0.460.0",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"reagraph": "^4.22.0",
"recharts": "^3.9.0", "recharts": "^3.9.0",
"sigma": "^3.0.3",
"tailwind-merge": "^2.5.5", "tailwind-merge": "^2.5.5",
"three": "^0.169.0" "three": "^0.169.0"
}, },
+19 -6
View File
@@ -1,5 +1,5 @@
import { useEffect, useState, useRef } from "react" import { useEffect, useState, useRef } from "react"
import { X, RefreshCw, Terminal, Cpu, Server, Shield, Power, Eye, EyeOff, Key, AlertTriangle, Camera, Bot, Box, FileText, Package, GitCommit, ExternalLink, ArrowRight } from "lucide-react" import { X, RefreshCw, Terminal, Cpu, Server, Shield, Power, Eye, EyeOff, Key, AlertTriangle, Camera, Bot, Box, FileText, Package, GitCommit, ExternalLink, ArrowRight, Shuffle } from "lucide-react"
import { api, type Job, type UpdatesResp, type ServicesResp, type UpdateDetails } from "@/lib/api" import { api, type Job, type UpdatesResp, type ServicesResp, type UpdateDetails } from "@/lib/api"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import { CustomDialog } from "./CustomDialog" import { CustomDialog } from "./CustomDialog"
@@ -81,7 +81,7 @@ export function SystemDrawer({ open, onClose, defaultTab = "maintenance" }: Syst
const [hermesUpdating, setHermesUpdating] = useState(false) const [hermesUpdating, setHermesUpdating] = useState(false)
// Update-Detail-Fenster: zeigt VOR dem Anwenden, was genau aktualisiert wird. // Update-Detail-Fenster: zeigt VOR dem Anwenden, was genau aktualisiert wird.
const [detail, setDetail] = useState<{ kind: "os" | "engine" | "hermes"; loading: boolean; data: UpdateDetails | null } | null>(null) const [detail, setDetail] = useState<{ kind: "os" | "engine" | "swap" | "hermes"; loading: boolean; data: UpdateDetails | null } | null>(null)
// Custom Dialog State // Custom Dialog State
const [dialog, setDialog] = useState<{ const [dialog, setDialog] = useState<{
@@ -237,6 +237,16 @@ export function SystemDrawer({ open, onClose, defaultTab = "maintenance" }: Syst
} }
} }
async function triggerSwapUpdate() {
try {
await api<{ job_id: string }>("/api/maintenance/swap-update", { method: "POST" })
loadJobs()
setActiveTab("maintenance")
} catch (e: any) {
showAlert("Fehler", `Fehler beim Router-Update: ${e.message}`)
}
}
async function doHermesUpdate() { async function doHermesUpdate() {
setHermesUpdating(true) setHermesUpdating(true)
try { try {
@@ -250,7 +260,7 @@ export function SystemDrawer({ open, onClose, defaultTab = "maintenance" }: Syst
} }
// Öffnet das Detail-Fenster und lädt, was genau aktualisiert würde. // Öffnet das Detail-Fenster und lädt, was genau aktualisiert würde.
async function openUpdateDetails(kind: "os" | "engine" | "hermes") { async function openUpdateDetails(kind: "os" | "engine" | "swap" | "hermes") {
setDetail({ kind, loading: true, data: null }) setDetail({ kind, loading: true, data: null })
try { try {
const d = await api<UpdateDetails>(`/api/maintenance/update-details?kind=${kind}`) const d = await api<UpdateDetails>(`/api/maintenance/update-details?kind=${kind}`)
@@ -266,6 +276,7 @@ export function SystemDrawer({ open, onClose, defaultTab = "maintenance" }: Syst
setDetail(null) setDetail(null)
if (kind === "os") triggerOsUpdate() if (kind === "os") triggerOsUpdate()
else if (kind === "engine") triggerEngineUpdate() else if (kind === "engine") triggerEngineUpdate()
else if (kind === "swap") triggerSwapUpdate()
else if (kind === "hermes") doHermesUpdate() else if (kind === "hermes") doHermesUpdate()
} }
@@ -446,7 +457,8 @@ export function SystemDrawer({ open, onClose, defaultTab = "maintenance" }: Syst
)} )}
<div className="space-y-1.5"> <div className="space-y-1.5">
<UpdateRow icon={Shield} iconClass="text-cyan-400" name="OS-Pakete (apt)" available={!!updates?.os} status={updates?.os ? `${updates.os} verfügbar` : "aktuell"} actionLabel="Anzeigen" onAction={() => openUpdateDetails("os")} /> <UpdateRow icon={Shield} iconClass="text-cyan-400" name="OS-Pakete (apt)" available={!!updates?.os} status={updates?.os ? `${updates.os} verfügbar` : "aktuell"} actionLabel="Anzeigen" onAction={() => openUpdateDetails("os")} />
<UpdateRow icon={Server} iconClass="text-violet-400" name="Engine (llama.cpp)" available={!!updates?.engine} status={updates?.engine ? "Update verfügbar" : "aktuell"} actionLabel="Anzeigen" onAction={() => openUpdateDetails("engine")} /> <UpdateRow icon={Server} iconClass="text-violet-400" name="Inferenz-Engine (llama.cpp)" available={!!updates?.engine} status={updates?.engine ? "Update verfügbar" : "aktuell"} actionLabel="Anzeigen" onAction={() => openUpdateDetails("engine")} />
<UpdateRow icon={Shuffle} iconClass="text-fuchsia-400" name="Router (llama-swap)" available={!!updates?.swap} status={updates?.swap ? "Update verfügbar" : "aktuell"} actionLabel="Anzeigen" onAction={() => openUpdateDetails("swap")} />
{(() => { {(() => {
const h = updates?.components?.find((c) => c.key === "hermes_agent") const h = updates?.components?.find((c) => c.key === "hermes_agent")
return ( return (
@@ -764,7 +776,8 @@ export function SystemDrawer({ open, onClose, defaultTab = "maintenance" }: Syst
const d = detail.data const d = detail.data
const meta = { const meta = {
os: { icon: Shield, cls: "text-cyan-400", title: "OS-Pakete (apt)" }, os: { icon: Shield, cls: "text-cyan-400", title: "OS-Pakete (apt)" },
engine: { icon: Server, cls: "text-violet-400", title: "Engine (llama.cpp)" }, engine: { icon: Server, cls: "text-violet-400", title: "Inferenz-Engine (llama.cpp)" },
swap: { icon: Shuffle, cls: "text-fuchsia-400", title: "Router (llama-swap)" },
hermes: { icon: Bot, cls: "text-amber-400", title: "Hermes-Agent" }, hermes: { icon: Bot, cls: "text-amber-400", title: "Hermes-Agent" },
}[detail.kind] }[detail.kind]
const Icon = meta.icon const Icon = meta.icon
@@ -815,7 +828,7 @@ export function SystemDrawer({ open, onClose, defaultTab = "maintenance" }: Syst
</div> </div>
</> </>
) )
) : detail.kind === "engine" ? ( ) : detail.kind === "engine" || detail.kind === "swap" ? (
<> <>
<div className="flex items-center gap-2 font-mono text-[11px]"> <div className="flex items-center gap-2 font-mono text-[11px]">
<span className="rounded-md border border-border/40 bg-background/30 px-2 py-1">Build {d?.installed_build ?? "?"}</span> <span className="rounded-md border border-border/40 bg-background/30 px-2 py-1">Build {d?.installed_build ?? "?"}</span>
@@ -41,8 +41,10 @@ export function UpdatesCard() {
<div className="space-y-1.5"> <div className="space-y-1.5">
<StatusRow label="OS-Pakete" tone={updates.os > 0 ? "alert" : "muted"} <StatusRow label="OS-Pakete" tone={updates.os > 0 ? "alert" : "muted"}
value={updates.os > 0 ? `${updates.os} verfügbar` : "aktuell"} /> value={updates.os > 0 ? `${updates.os} verfügbar` : "aktuell"} />
<StatusRow label="Engine (llama.cpp)" tone={updates.engine > 0 ? "alert" : "muted"} <StatusRow label="Inferenz-Engine (llama.cpp)" tone={updates.engine > 0 ? "alert" : "muted"}
value={updates.engine > 0 ? "Update verfügbar" : "aktuell"} /> value={updates.engine > 0 ? "Update verfügbar" : "aktuell"} />
<StatusRow label="Router (llama-swap)" tone={updates.swap > 0 ? "alert" : "muted"}
value={updates.swap > 0 ? "Update verfügbar" : "aktuell"} />
<StatusRow label="Modell-Upgrades" tone={updates.models > 0 ? "accent" : "muted"} <StatusRow label="Modell-Upgrades" tone={updates.models > 0 ? "accent" : "muted"}
value={updates.models > 0 ? `${updates.models} verfügbar` : "aktuell"} /> value={updates.models > 0 ? `${updates.models} verfügbar` : "aktuell"} />
{updates.components?.map((c) => ( {updates.components?.map((c) => (
+2 -1
View File
@@ -242,6 +242,7 @@ export interface ComponentUpdate {
export interface UpdatesResp { export interface UpdatesResp {
os: number os: number
engine: number engine: number
swap: number
models: number models: number
model_list: { role: string; title: string; repo: string }[] model_list: { role: string; title: string; repo: string }[]
last_check?: number | null last_check?: number | null
@@ -249,7 +250,7 @@ export interface UpdatesResp {
} }
export interface UpdateDetails { export interface UpdateDetails {
kind: "os" | "engine" | "hermes" kind: "os" | "engine" | "swap" | "hermes"
error?: string error?: string
// os // os
count?: number count?: number
+90 -35
View File
@@ -1,8 +1,13 @@
import { useMemo, useState } from "react" import { useEffect, useMemo, useRef, useState } from "react"
import { GraphCanvas, darkTheme } from "reagraph" import Graph from "graphology"
import forceAtlas2 from "graphology-layout-forceatlas2"
import Sigma from "sigma"
import { Trash2, Sparkles, Share2 } from "lucide-react" import { Trash2, Sparkles, Share2 } from "lucide-react"
import { type MemoryGraph } from "@/lib/api" import { type MemoryGraph } from "@/lib/api"
// Sigma.js v3 + graphology: graph-optimiertes WebGL (kein three.js-Ballast) → skaliert auf tausende
// Fakten flüssig. Im App-Look gestylt; Hover hebt den Knoten + seine Nachbarn hervor, der Rest dimmt.
const CAT_COLOR: Record<string, string> = { const CAT_COLOR: Record<string, string> = {
identity: "#22d3ee", knowledge: "#6366f1", rules: "#a78bfa", events: "#fbbf24", identity: "#22d3ee", knowledge: "#6366f1", rules: "#a78bfa", events: "#fbbf24",
} }
@@ -10,9 +15,18 @@ const CAT_LABEL: Record<string, string> = {
identity: "Identität", knowledge: "Wissen", rules: "Regeln", events: "Ereignisse", identity: "Identität", knowledge: "Wissen", rules: "Regeln", events: "Ereignisse",
} }
const AUTO = new Set(["auto", "agent", "hermes"]) const AUTO = new Set(["auto", "agent", "hermes"])
const DIM_NODE = "#1f2937"
const DIM_EDGE = "#0f172a"
const EDGE = "#243044"
const EDGE_HI = "#4b5563"
export function GraphView({ data, onDelete }: { data: MemoryGraph; onDelete: (id: string) => void }) { export function GraphView({ data, onDelete }: { data: MemoryGraph; onDelete: (id: string) => void }) {
const [selected, setSelected] = useState<string | null>(null) const [selected, setSelected] = useState<string | null>(null)
const containerRef = useRef<HTMLDivElement>(null)
const sigmaRef = useRef<Sigma | null>(null)
const graphRef = useRef<Graph | null>(null)
const active = useRef<string | null>(null) // hervorgehobener Knoten (hover oder Auswahl)
const selectedRef = useRef<string | null>(null) // aktuelle Auswahl (für leaveNode ohne Effekt-Neulauf)
const degree = useMemo(() => { const degree = useMemo(() => {
const d: Record<string, number> = {} const d: Record<string, number> = {}
@@ -20,19 +34,67 @@ export function GraphView({ data, onDelete }: { data: MemoryGraph; onDelete: (id
return d return d
}, [data]) }, [data])
const nodes = useMemo(() => data.nodes.map((n) => ({ // Graph (neu)bauen + Layout + Sigma rendern, wenn sich die Daten ändern.
id: n.id, useEffect(() => {
label: n.content.length > 26 ? n.content.slice(0, 25) + "…" : n.content, if (!containerRef.current || !data.nodes.length) return
fill: CAT_COLOR[n.category] || "#64748b", const graph = new Graph()
size: 6 + Math.min(degree[n.id] || 0, 6) * 2, const N = data.nodes.length
})), [data, degree]) data.nodes.forEach((n, i) => {
const a = (2 * Math.PI * i) / N
graph.addNode(n.id, {
x: Math.cos(a), y: Math.sin(a), // Startposition (Kreis) für ForceAtlas2
size: 4 + Math.min(degree[n.id] || 0, 12) * 1.4,
color: CAT_COLOR[n.category] || "#64748b",
label: n.content.length > 48 ? n.content.slice(0, 47) + "…" : n.content,
})
})
data.edges.forEach((e) => {
if (graph.hasNode(e.source) && graph.hasNode(e.target) && !graph.hasEdge(e.source, e.target))
graph.addEdge(e.source, e.target, { size: 0.6 + (e.weight || 0), color: EDGE })
})
forceAtlas2.assign(graph, { iterations: 220, settings: forceAtlas2.inferSettings(graph) })
graphRef.current = graph
const edges = useMemo(() => data.edges.map((e) => ({ const renderer = new Sigma(graph, containerRef.current, {
id: `${e.source}->${e.target}`, renderLabels: true,
source: e.source, labelColor: { color: "#cbd5e1" },
target: e.target, labelSize: 11,
size: 0.4 + e.weight, labelWeight: "500",
})), [data]) labelDensity: 0.6,
labelRenderedSizeThreshold: N > 120 ? 12 : 0, // bei vielen Knoten nur große labeln (entklumpen)
defaultEdgeColor: EDGE,
minCameraRatio: 0.1,
maxCameraRatio: 4,
nodeReducer: (id, attrs) => {
const a = active.current
if (!a) return attrs
if (id === a || graph.areNeighbors(a, id)) return attrs
return { ...attrs, color: DIM_NODE, label: "" }
},
edgeReducer: (edge, attrs) => {
const a = active.current
if (!a) return attrs
const ext = graph.extremities(edge)
if (ext[0] === a || ext[1] === a) return { ...attrs, color: EDGE_HI, size: (attrs.size || 1) * 1.6 }
return { ...attrs, color: DIM_EDGE }
},
})
sigmaRef.current = renderer
renderer.on("clickNode", ({ node }) => setSelected(node))
renderer.on("clickStage", () => setSelected(null))
renderer.on("enterNode", ({ node }) => { active.current = node; renderer.refresh(); containerRef.current!.style.cursor = "pointer" })
renderer.on("leaveNode", () => { active.current = selectedRef.current; renderer.refresh(); containerRef.current!.style.cursor = "default" })
return () => { renderer.kill(); sigmaRef.current = null; graphRef.current = null }
}, [data, degree])
// Auswahl spiegelt die Hervorhebung (auch ohne Hover).
useEffect(() => {
selectedRef.current = selected
active.current = selected
sigmaRef.current?.refresh()
}, [selected])
const sel = selected ? data.nodes.find((n) => n.id === selected) ?? null : null const sel = selected ? data.nodes.find((n) => n.id === selected) ?? null : null
const neighbors = useMemo(() => { const neighbors = useMemo(() => {
@@ -56,17 +118,15 @@ export function GraphView({ data, onDelete }: { data: MemoryGraph; onDelete: (id
return ( return (
<div className="grid grid-cols-1 lg:grid-cols-[1fr_280px] gap-3"> <div className="grid grid-cols-1 lg:grid-cols-[1fr_280px] gap-3">
<div className="relative h-[480px] rounded-2xl border border-border/60 bg-[#070a0f] overflow-hidden"> <div className="relative h-[480px] rounded-2xl border border-border/60 bg-[#070a0f] overflow-hidden">
<GraphCanvas <div ref={containerRef} className="absolute inset-0" />
nodes={nodes} {/* Legende */}
edges={edges} <div className="absolute left-3 bottom-3 flex flex-wrap gap-2 rounded-lg bg-background/40 px-2.5 py-1.5 backdrop-blur-sm">
theme={darkTheme} {Object.entries(CAT_LABEL).map(([k, l]) => (
layoutType="forceDirected2d" <span key={k} className="flex items-center gap-1 text-[10px] text-muted-foreground">
labelType="nodes" <span className="h-2 w-2 rounded-full" style={{ background: CAT_COLOR[k] }} />{l}
edgeArrowPosition="none" </span>
draggable ))}
onNodeClick={(n: any) => setSelected(n.id)} </div>
onCanvasClick={() => setSelected(null)}
/>
</div> </div>
<div className="rounded-2xl border border-border/60 bg-card/45 backdrop-blur-md p-4 min-h-[480px]"> <div className="rounded-2xl border border-border/60 bg-card/45 backdrop-blur-md p-4 min-h-[480px]">
@@ -87,20 +147,15 @@ export function GraphView({ data, onDelete }: { data: MemoryGraph; onDelete: (id
</div> </div>
<div className="flex flex-col gap-1.5 mb-4"> <div className="flex flex-col gap-1.5 mb-4">
{neighbors.length ? neighbors.map((n) => ( {neighbors.length ? neighbors.map((n) => (
<button <button key={n.id} onClick={() => setSelected(n.id)}
key={n.id} className="flex items-center gap-2 text-[11px] text-muted-foreground hover:text-foreground text-left transition-colors">
onClick={() => setSelected(n.id)}
className="flex items-center gap-2 text-[11px] text-muted-foreground hover:text-foreground text-left transition-colors"
>
<span className="w-1.5 h-1.5 rounded-full shrink-0" style={{ background: CAT_COLOR[n.category] }} /> <span className="w-1.5 h-1.5 rounded-full shrink-0" style={{ background: CAT_COLOR[n.category] }} />
<span className="truncate">{n.content}</span> <span className="truncate">{n.content}</span>
</button> </button>
)) : <span className="text-[11px] text-muted-foreground/60"></span>} )) : <span className="text-[11px] text-muted-foreground/60"></span>}
</div> </div>
<button <button onClick={() => { onDelete(sel.id); setSelected(null) }}
onClick={() => { onDelete(sel.id); setSelected(null) }} className="flex items-center gap-1.5 text-[11px] text-red-400 border border-red-500/20 rounded-lg px-2.5 py-1.5 hover:bg-red-500/5 transition-all">
className="flex items-center gap-1.5 text-[11px] text-red-400 border border-red-500/20 rounded-lg px-2.5 py-1.5 hover:bg-red-500/5 transition-all"
>
<Trash2 className="h-3.5 w-3.5" /> vergessen <Trash2 className="h-3.5 w-3.5" /> vergessen
</button> </button>
</> </>
@@ -108,7 +163,7 @@ export function GraphView({ data, onDelete }: { data: MemoryGraph; onDelete: (id
<div className="h-full flex flex-col items-center justify-center text-center text-muted-foreground/70 gap-2 pt-24"> <div className="h-full flex flex-col items-center justify-center text-center text-muted-foreground/70 gap-2 pt-24">
<Share2 className="h-6 w-6" /> <Share2 className="h-6 w-6" />
<div className="text-xs max-w-[180px] leading-relaxed"> <div className="text-xs max-w-[180px] leading-relaxed">
Einen Knoten wählen, um den Fakt und seine semantischen Nachbarn zu sehen. Auf einen Knoten klicken, um den Fakt und seine semantischen Nachbarn zu sehen. Hover hebt das Netz hervor.
</div> </div>
</div> </div>
)} )}
+37 -15
View File
@@ -9,7 +9,7 @@ import { useDialog } from "@/lib/useDialog"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import { GraphErrorBoundary } from "./GraphErrorBoundary" import { GraphErrorBoundary } from "./GraphErrorBoundary"
// reagraph + three.js sind schwer → erst laden, wenn der Graph-Tab geöffnet wird. // Sigma/graphology nur laden, wenn der Graph-Tab geöffnet wird (Code-Splitting).
const GraphView = lazy(() => import("./GraphView").then((m) => ({ default: m.GraphView }))) const GraphView = lazy(() => import("./GraphView").then((m) => ({ default: m.GraphView })))
const CATEGORIES = ["identity", "knowledge", "rules", "events"] const CATEGORIES = ["identity", "knowledge", "rules", "events"]
@@ -48,7 +48,7 @@ export function MemoryView() {
const [category, setCategory] = useState("knowledge") const [category, setCategory] = useState("knowledge")
const [deduping, setDeduping] = useState(false) const [deduping, setDeduping] = useState(false)
const [copied, setCopied] = useState(false) const [copied, setCopied] = useState(false)
const [view, setView] = useState<"liste" | "graph">("graph") const [view, setView] = useState<"liste" | "graph">("liste")
const [showAdd, setShowAdd] = useState(false) const [showAdd, setShowAdd] = useState(false)
const qc = useQueryClient() const qc = useQueryClient()
@@ -80,6 +80,14 @@ export function MemoryView() {
return { nodes: ns, edges: g.edges.filter((e) => ids.has(e.source) && ids.has(e.target)) } return { nodes: ns, edges: g.edges.filter((e) => ids.has(e.source) && ids.has(e.target)) }
}, [graph, q]) }, [graph, q])
// Liste nach Kategorie gruppieren (skaliert: klare Sektionen statt einer flachen Wand).
const grouped = useMemo(() => {
const g: Record<string, (typeof items)[number][]> = {}
items.forEach((m) => { (g[m.category] ??= []).push(m) })
return g
}, [items])
const otherItems = useMemo(() => items.filter((m) => !CATEGORIES.includes(m.category)), [items])
async function add() { async function add() {
if (!content.trim()) return if (!content.trim()) return
await api("/api/memory", { method: "POST", body: JSON.stringify({ content, category, source: "ui" }) }) await api("/api/memory", { method: "POST", body: JSON.stringify({ content, category, source: "ui" }) })
@@ -277,29 +285,38 @@ export function MemoryView() {
})} })}
</div> </div>
<div className="space-y-3">
{items.length === 0 ? ( {items.length === 0 ? (
<div className="text-xs text-muted-foreground border border-dashed border-border/60 rounded-2xl p-12 text-center"> <div className="text-xs text-muted-foreground border border-dashed border-border/60 rounded-2xl p-12 text-center">
Keine Einträge für die aktuellen Filterkriterien gefunden. Keine Einträge für die aktuellen Filterkriterien gefunden.
</div> </div>
) : items.map((m) => { ) : (
const conf = CAT_CONFIG[m.category] || DEFAULT_CAT <div className="space-y-5">
{[...CATEGORIES, "__other"].map((c) => {
const list = c === "__other" ? otherItems : (grouped[c] || [])
if (!list.length) return null
const conf = CAT_CONFIG[c] || DEFAULT_CAT
const Icon = conf.icon const Icon = conf.icon
return (
<div key={c}>
{/* Sektions-Kopf je Kategorie */}
<div className="flex items-center gap-2 mb-2 px-1">
<Icon className={cn("h-3.5 w-3.5", conf.text)} />
<span className={cn("text-[11px] font-bold uppercase tracking-wider", conf.text)}>{conf.label}</span>
<span className="text-[10px] font-mono text-muted-foreground/60 bg-card/50 rounded px-1.5 py-0.5">{list.length}</span>
<div className="ml-1 h-px flex-1 bg-border/30" />
</div>
<div className="space-y-2">
{list.map((m) => {
const isAuto = AUTO_SOURCES.has(m.source) const isAuto = AUTO_SOURCES.has(m.source)
return ( return (
<div key={m.id} className={cn("flex items-start justify-between gap-4 p-4 rounded-xl border border-l-4 bg-card/45 backdrop-blur-md border-border/60 shadow-md shadow-black/5 hover:border-primary/20 transition-all group", <div key={m.id} className={cn("flex items-start justify-between gap-4 p-3.5 rounded-xl border border-l-4 bg-card/45 backdrop-blur-md border-border/60 hover:border-primary/20 transition-all group",
BORDER_CLASSES[m.category] || "border-l-muted")}> BORDER_CLASSES[m.category] || "border-l-muted")}>
<div className="flex items-start gap-3 flex-1 min-w-0"> <span className="text-xs text-foreground leading-relaxed break-words whitespace-pre-wrap flex-1 min-w-0">{m.content}</span>
<span className={cn("flex items-center gap-1 px-2 py-0.5 rounded text-[9px] font-bold uppercase tracking-wider font-mono shrink-0", conf.bg, conf.text)}> <div className="flex items-center gap-2 shrink-0">
<Icon className="h-3 w-3" /><span className="hidden sm:inline">{conf.label}</span>
</span>
<span className="text-xs text-foreground leading-relaxed break-words whitespace-pre-wrap flex-1">{m.content}</span>
</div>
<div className="flex items-center gap-3 shrink-0">
{typeof m.score === "number" && ( {typeof m.score === "number" && (
<span className="text-[9px] font-mono text-primary bg-primary/10 px-1.5 py-0.5 rounded uppercase tracking-wider" title="Relevanz der semantischen Suche">{Math.round(m.score * 100)}%</span> <span className="text-[9px] font-mono text-primary bg-primary/10 px-1.5 py-0.5 rounded" title="Relevanz der semantischen Suche">{Math.round(m.score * 100)}%</span>
)} )}
<span className={cn("flex items-center gap-1 text-[9px] font-mono px-1.5 py-0.5 rounded uppercase tracking-wider", <span className={cn("flex items-center gap-1 text-[9px] font-mono px-1.5 py-0.5 rounded",
isAuto ? "text-emerald-400 bg-emerald-500/10" : "text-muted-foreground/60 bg-background/20")} isAuto ? "text-emerald-400 bg-emerald-500/10" : "text-muted-foreground/60 bg-background/20")}
title={isAuto ? "Automatisch gelernt" : "Manuell angelegt"}> title={isAuto ? "Automatisch gelernt" : "Manuell angelegt"}>
{isAuto && <Sparkles className="h-2.5 w-2.5" />}{m.source} {isAuto && <Sparkles className="h-2.5 w-2.5" />}{m.source}
@@ -313,6 +330,11 @@ export function MemoryView() {
) )
})} })}
</div> </div>
</div>
)
})}
</div>
)}
</> </>
)} )}