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:
@@ -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()
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Executable
+33
@@ -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."
|
||||||
+312
File diff suppressed because one or more lines are too long
-3922
File diff suppressed because one or more lines are too long
-1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+329
-324
File diff suppressed because one or more lines are too long
Vendored
+2
-2
@@ -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>
|
||||||
|
|||||||
Generated
+17
-477
@@ -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",
|
||||||
|
|||||||
@@ -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"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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) => (
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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,42 +285,56 @@ 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) => {
|
<div className="space-y-5">
|
||||||
const conf = CAT_CONFIG[m.category] || DEFAULT_CAT
|
{[...CATEGORIES, "__other"].map((c) => {
|
||||||
const Icon = conf.icon
|
const list = c === "__other" ? otherItems : (grouped[c] || [])
|
||||||
const isAuto = AUTO_SOURCES.has(m.source)
|
if (!list.length) return null
|
||||||
return (
|
const conf = CAT_CONFIG[c] || DEFAULT_CAT
|
||||||
<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",
|
const Icon = conf.icon
|
||||||
BORDER_CLASSES[m.category] || "border-l-muted")}>
|
return (
|
||||||
<div className="flex items-start gap-3 flex-1 min-w-0">
|
<div key={c}>
|
||||||
<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)}>
|
{/* Sektions-Kopf je Kategorie */}
|
||||||
<Icon className="h-3 w-3" /><span className="hidden sm:inline">{conf.label}</span>
|
<div className="flex items-center gap-2 mb-2 px-1">
|
||||||
</span>
|
<Icon className={cn("h-3.5 w-3.5", conf.text)} />
|
||||||
<span className="text-xs text-foreground leading-relaxed break-words whitespace-pre-wrap flex-1">{m.content}</span>
|
<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)
|
||||||
|
return (
|
||||||
|
<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")}>
|
||||||
|
<span className="text-xs text-foreground leading-relaxed break-words whitespace-pre-wrap flex-1 min-w-0">{m.content}</span>
|
||||||
|
<div className="flex items-center gap-2 shrink-0">
|
||||||
|
{typeof m.score === "number" && (
|
||||||
|
<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",
|
||||||
|
isAuto ? "text-emerald-400 bg-emerald-500/10" : "text-muted-foreground/60 bg-background/20")}
|
||||||
|
title={isAuto ? "Automatisch gelernt" : "Manuell angelegt"}>
|
||||||
|
{isAuto && <Sparkles className="h-2.5 w-2.5" />}{m.source}
|
||||||
|
</span>
|
||||||
|
<button onClick={() => del(m.id)} title="Eintrag löschen"
|
||||||
|
className="h-7 w-7 rounded-lg flex items-center justify-center border border-border/40 text-muted-foreground hover:text-red-400 hover:bg-red-500/5 transition-all opacity-0 group-hover:opacity-100">
|
||||||
|
<Trash2 className="h-3.5 w-3.5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3 shrink-0">
|
)
|
||||||
{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>
|
</div>
|
||||||
)}
|
)}
|
||||||
<span className={cn("flex items-center gap-1 text-[9px] font-mono px-1.5 py-0.5 rounded uppercase tracking-wider",
|
|
||||||
isAuto ? "text-emerald-400 bg-emerald-500/10" : "text-muted-foreground/60 bg-background/20")}
|
|
||||||
title={isAuto ? "Automatisch gelernt" : "Manuell angelegt"}>
|
|
||||||
{isAuto && <Sparkles className="h-2.5 w-2.5" />}{m.source}
|
|
||||||
</span>
|
|
||||||
<button onClick={() => del(m.id)} title="Eintrag löschen"
|
|
||||||
className="h-7 w-7 rounded-lg flex items-center justify-center border border-border/40 text-muted-foreground hover:text-red-400 hover:bg-red-500/5 transition-all opacity-0 group-hover:opacity-100">
|
|
||||||
<Trash2 className="h-3.5 w-3.5" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user