mindnet/app/frontend/ui_graph.py
2025-12-14 12:50:54 +01:00

126 lines
5.4 KiB
Python

import streamlit as st
from streamlit_agraph import agraph, Config
from qdrant_client import models
from ui_config import COLLECTION_PREFIX, GRAPH_COLORS
from ui_callbacks import switch_to_editor_callback
def render_graph_explorer(graph_service):
st.header("🕸️ Graph Explorer")
if "graph_center_id" not in st.session_state: st.session_state.graph_center_id = None
# Defaults
st.session_state.setdefault("graph_depth", 2)
st.session_state.setdefault("graph_show_labels", True)
st.session_state.setdefault("graph_spacing", 150)
st.session_state.setdefault("graph_gravity", -3000)
col_ctrl, col_graph = st.columns([1, 4])
with col_ctrl:
st.subheader("Fokus")
search_term = st.text_input("Suche Notiz", placeholder="Titel eingeben...")
if search_term:
hits, _ = graph_service.client.scroll(
collection_name=f"{COLLECTION_PREFIX}_notes",
scroll_filter=models.Filter(must=[models.FieldCondition(key="title", match=models.MatchText(text=search_term))]),
limit=10
)
options = {h.payload['title']: h.payload['note_id'] for h in hits}
if options:
selected_title = st.selectbox("Ergebnisse:", list(options.keys()))
if st.button("Laden", use_container_width=True):
st.session_state.graph_center_id = options[selected_title]
st.rerun()
st.divider()
with st.expander("👁️ Ansicht & Layout", expanded=True):
st.session_state.graph_depth = st.slider("Tiefe (Tier)", 1, 3, st.session_state.graph_depth)
st.session_state.graph_show_labels = st.checkbox("Kanten-Beschriftung", st.session_state.graph_show_labels)
st.markdown("**Physik (BarnesHut)**")
st.session_state.graph_spacing = st.slider("Federlänge", 50, 500, st.session_state.graph_spacing)
st.session_state.graph_gravity = st.slider("Abstoßung", -20000, -500, st.session_state.graph_gravity)
if st.button("Reset Layout"):
st.session_state.graph_spacing = 150
st.session_state.graph_gravity = -3000
st.rerun()
st.divider()
st.caption("Legende (Top Typen)")
for k, v in list(GRAPH_COLORS.items())[:8]:
st.markdown(f"<span style='color:{v}'>●</span> {k}", unsafe_allow_html=True)
with col_graph:
center_id = st.session_state.graph_center_id
if center_id:
# Action Container oben
action_container = st.container()
with st.spinner(f"Lade Graph..."):
nodes, edges = graph_service.get_ego_graph(
center_id,
depth=st.session_state.graph_depth,
show_labels=st.session_state.graph_show_labels
)
note_data = graph_service._fetch_note_cached(center_id)
# Action Bar rendern
with action_container:
c1, c2 = st.columns([3, 1])
with c1: st.caption(f"Aktives Zentrum: **{center_id}**")
with c2:
if note_data:
st.button("📝 Bearbeiten", use_container_width=True, on_click=switch_to_editor_callback, args=(note_data,))
else:
st.error("Datenfehler")
with st.expander("🕵️ Data Inspector", expanded=False):
if note_data:
st.json(note_data)
if 'path' in note_data: st.success(f"Pfad OK: {note_data['path']}")
else: st.error("Pfad fehlt!")
else: st.info("Leer.")
if not nodes:
st.warning("Keine Daten gefunden.")
else:
# Physik Config für BarnesHut + Height-Trick
dyn_height = 800 + (abs(st.session_state.graph_gravity) % 5)
config = Config(
width=1000,
height=dyn_height,
directed=True,
physics={
"enabled": True,
"solver": "barnesHut",
"barnesHut": {
"gravitationalConstant": st.session_state.graph_gravity,
"centralGravity": 0.3,
"springLength": st.session_state.graph_spacing,
"springConstant": 0.04,
"damping": 0.09,
"avoidOverlap": 0.1
},
"stabilization": {"enabled": True, "iterations": 600}
},
hierarchical=False,
nodeHighlightBehavior=True,
highlightColor="#F7A7A6",
collapsible=False
)
return_value = agraph(nodes=nodes, edges=edges, config=config)
if return_value:
if return_value != center_id:
st.session_state.graph_center_id = return_value
st.rerun()
else:
st.toast(f"Zentrum: {return_value}")
else:
st.info("👈 Bitte wähle links eine Notiz aus.")