diff --git a/app/frontend/ui_graph_cytoscape.py b/app/frontend/ui_graph_cytoscape.py index f6b65db..85470c7 100644 --- a/app/frontend/ui_graph_cytoscape.py +++ b/app/frontend/ui_graph_cytoscape.py @@ -1,5 +1,6 @@ import streamlit as st -from st_cytoscape import cytoscape +# KORREKTER IMPORT für 'pip install streamlit-cytoscapejs' +from streamlit_cytoscapejs import st_cytoscapejs from qdrant_client import models from ui_config import COLLECTION_PREFIX, GRAPH_COLORS from ui_callbacks import switch_to_editor_callback @@ -10,9 +11,9 @@ def render_graph_explorer_cytoscape(graph_service): if "graph_center_id" not in st.session_state: st.session_state.graph_center_id = None - # Layout Defaults für COSE (besser als Physik) - st.session_state.setdefault("cy_node_repulsion", 1000000) # Starke Abstoßung - st.session_state.setdefault("cy_ideal_edge_len", 150) # Ziel-Abstand + # Layout Defaults für COSE + st.session_state.setdefault("cy_node_repulsion", 1000000) + st.session_state.setdefault("cy_ideal_edge_len", 150) col_ctrl, col_graph = st.columns([1, 4]) @@ -36,7 +37,7 @@ def render_graph_explorer_cytoscape(graph_service): st.divider() with st.expander("👁️ Layout Einstellungen", expanded=True): - st.caption("COSE Layout (Spezialisiert auf Abstand)") + st.caption("COSE Layout") st.session_state.cy_ideal_edge_len = st.slider("Kantenlänge", 50, 500, st.session_state.cy_ideal_edge_len) st.session_state.cy_node_repulsion = st.slider("Knoten-Abstoßung", 100000, 2000000, st.session_state.cy_node_repulsion, step=100000) @@ -57,11 +58,7 @@ def render_graph_explorer_cytoscape(graph_service): # 1. Daten laden with st.spinner("Lade Graph..."): - # Wir nutzen die bestehende Logik aus dem Service - # get_ego_graph liefert Agraph-Objekte, die wir gleich konvertieren nodes_data, edges_data = graph_service.get_ego_graph(center_id) - - # Note Data für Editor Button note_data = graph_service.get_note_with_full_content(center_id) # 2. Action Bar @@ -72,39 +69,36 @@ def render_graph_explorer_cytoscape(graph_service): if note_data: st.button("📝 Bearbeiten", use_container_width=True, on_click=switch_to_editor_callback, args=(note_data,), key="cy_edit") - # 3. Konvertierung zu Cytoscape JSON Format + # 3. Konvertierung zu Cytoscape JSON cy_elements = [] - # Nodes konvertieren for n in nodes_data: - # Agraph Node -> Cytoscape Element - # Wir nutzen n.id, n.label, n.color aus dem Agraph Objekt + # Node cy_node = { "data": { "id": n.id, "label": n.label, - "color": n.color, - "size": 40 if n.id == center_id else 25, - # Tooltip Inhalt steht in n.title (aus graph_service) + "bg_color": n.color, # Wichtig: Eigenes Attribut für Style mapping + "size": 40 if n.id == center_id else 25, "tooltip": n.title }, "selected": (n.id == center_id) } cy_elements.append(cy_node) - # Edges konvertieren for e in edges_data: + # Edge cy_edge = { "data": { "source": e.source, "target": e.target, "label": e.label, - "color": e.color + "line_color": e.color } } cy_elements.append(cy_edge) - # 4. Stylesheet (Design) + # 4. Stylesheet stylesheet = [ { "selector": "node", @@ -112,14 +106,13 @@ def render_graph_explorer_cytoscape(graph_service): "label": "data(label)", "width": "data(size)", "height": "data(size)", - "background-color": "data(color)", + "background-color": "data(bg_color)", # Mapping auf unser data feld "color": "#333", "font-size": "12px", "text-valign": "center", "text-halign": "center", "border-width": 2, - "border-color": "#fff", - "content": "data(label)" + "border-color": "#fff" } }, { @@ -133,8 +126,8 @@ def render_graph_explorer_cytoscape(graph_service): "selector": "edge", "style": { "width": 2, - "line-color": "data(color)", - "target-arrow-color": "data(color)", + "line-color": "data(line_color)", + "target-arrow-color": "data(line_color)", "target-arrow-shape": "triangle", "curve-style": "bezier", "label": "data(label)", @@ -146,9 +139,9 @@ def render_graph_explorer_cytoscape(graph_service): } ] - # 5. Rendering mit COSE Layout - # COSE ist der beste Algorithmus für "Non-Overlapping" Graphen - selected_node = cytoscape( + # 5. Rendering + # Die Bibliothek gibt eine Liste von geklickten Elementen zurück + clicked_elements = st_cytoscapejs( elements=cy_elements, stylesheet=stylesheet, layout={ @@ -173,15 +166,22 @@ def render_graph_explorer_cytoscape(graph_service): height="800px" ) - # Interaktion: Klick Event - # Cytoscape gibt eine Liste zurück (z.B. {'nodes': ['id1'], 'edges': []}) - if selected_node: - clicked_nodes = selected_node.get("nodes", []) - if clicked_nodes: - clicked_id = clicked_nodes[0] + # Interaktion: Klick Event verarbeiten + if clicked_elements: + # clicked_elements ist eine Liste von Dictionaries + # Wir suchen nach einem Node-Klick + for el in clicked_elements: + # Prüfen ob es ein Node ist (hat 'id' aber keine 'source'/'target' im root, + # allerdings liefert die Lib oft die rohen Daten) + # Wir schauen auf die ID. + clicked_id = el.get("data", {}).get("id") if "data" in el else el.get("id") + if clicked_id and clicked_id != center_id: - st.session_state.graph_center_id = clicked_id - st.rerun() + # Safety check: ist es eine edge? Edges haben source/target + is_edge = "source" in el.get("data", {}) + if not is_edge: + st.session_state.graph_center_id = clicked_id + st.rerun() else: st.info("👈 Bitte wähle links eine Notiz aus.") \ No newline at end of file