diff --git a/app/frontend/ui_graph_cytoscape.py b/app/frontend/ui_graph_cytoscape.py index ca21b04..c5987fe 100644 --- a/app/frontend/ui_graph_cytoscape.py +++ b/app/frontend/ui_graph_cytoscape.py @@ -1,5 +1,4 @@ import streamlit as st -# Wir nutzen das mächtigere 'st-cytoscape' (pip install st-cytoscape) from st_cytoscape import cytoscape from qdrant_client import models from ui_config import COLLECTION_PREFIX, GRAPH_COLORS @@ -15,7 +14,7 @@ def render_graph_explorer_cytoscape(graph_service): # Defaults für Cytoscape Session State st.session_state.setdefault("cy_node_repulsion", 1000000) st.session_state.setdefault("cy_ideal_edge_len", 150) - st.session_state.setdefault("cy_depth", 2) # Eigene Tiefe für diesen Tab + st.session_state.setdefault("cy_depth", 2) col_ctrl, col_graph = st.columns([1, 4]) @@ -66,36 +65,57 @@ def render_graph_explorer_cytoscape(graph_service): # 1. Daten laden (Mit Tiefe!) with st.spinner(f"Lade Graph (Tiefe {st.session_state.cy_depth})..."): - # Hier nutzen wir die dynamische Tiefe aus dem Slider - # Wir holen die Agraph-Objekte vom Service, da die Logik dort zentralisiert ist + # Agraph-Objekte vom Service holen nodes_data, edges_data = graph_service.get_ego_graph( center_id, depth=st.session_state.cy_depth ) - # Note Data für Editor Button (Volltext/Pfad via Service) + # Note Data für Editor & Inspector (Volltext) note_data = graph_service.get_note_with_full_content(center_id) - # 2. Action Bar + # 2. Action Bar & Inspector with action_container: c1, c2 = st.columns([3, 1]) with c1: st.caption(f"Zentrum: **{center_id}**") with c2: if note_data: st.button("📝 Bearbeiten", use_container_width=True, on_click=switch_to_editor_callback, args=(note_data,), key="cy_edit_btn") - + + # --- DATA INSPECTOR --- + with st.expander("🕵️ Data Inspector (Details)", expanded=False): + if note_data: + col_i1, col_i2 = st.columns(2) + with col_i1: + st.markdown(f"**Titel:** {note_data.get('title')}") + st.markdown(f"**Typ:** `{note_data.get('type')}`") + with col_i2: + st.markdown(f"**Tags:** {', '.join(note_data.get('tags', []))}") + path_check = "✅" if note_data.get('path') else "❌" + st.markdown(f"**Pfad:** {path_check}") + + st.divider() + # Vorschau des Inhalts + content_preview = note_data.get('fulltext', '')[:500] + st.text_area("Inhalt (Vorschau)", content_preview + "...", height=100, disabled=True) + + with st.expander("Raw JSON"): + st.json(note_data) + else: + st.warning("Keine Daten für diesen Knoten.") + # 3. Daten Konvertierung (Agraph -> Cytoscape JSON) cy_elements = [] # Nodes for n in nodes_data: - # Wir holen den Hover-Text aus 'n.title', den der Service vorbereitet hat (Fulltext/Snippet) + # Hover-Text aus n.title (vom Service vorbereitet) tooltip_text = n.title if n.title else n.label - # Label kürzen für Anzeige im Kreis, aber voll im Tooltip + # Label kürzen für Anzeige im Kreis display_label = n.label if len(display_label) > 15 and " " in display_label: - display_label = display_label.replace(" ", "\n", 1) # Zeilenumbruch + display_label = display_label.replace(" ", "\n", 1) cy_node = { "data": { @@ -104,8 +124,8 @@ def render_graph_explorer_cytoscape(graph_service): "full_label": n.label, "bg_color": n.color, "size": 50 if n.id == center_id else 30, - # Dieses Feld wird oft automatisch als natives 'title' Attribut gerendert (Browser Tooltip) - "title": tooltip_text + # Tooltip Datenfeld + "tooltip": tooltip_text }, "selected": (n.id == center_id) } @@ -113,8 +133,7 @@ def render_graph_explorer_cytoscape(graph_service): # Edges for e in edges_data: - # Kompatibilität: Agraph nutzt 'to', Cytoscape braucht 'target' - # Wir prüfen sicherheitshalber beide Attribute + # Kompatibilität Agraph -> Cytoscape target_id = getattr(e, "to", getattr(e, "target", None)) if target_id: @@ -145,8 +164,8 @@ def render_graph_explorer_cytoscape(graph_service): "text-max-width": "80px", "border-width": 2, "border-color": "#fff", - # Tooltip Mapping (funktioniert je nach Browser/Wrapper) - "content": "data(label)" + # Tooltip Mapping (Browser abhängig) + "title": "data(tooltip)" } }, { @@ -168,8 +187,8 @@ def render_graph_explorer_cytoscape(graph_service): "curve-style": "bezier", "label": "data(label)", "font-size": "10px", - "color": "#666", - "text-background-opacity": 0.7, + "color": "#777", + "text-background-opacity": 0.8, "text-background-color": "#fff", "text-rotation": "autorotate" } @@ -177,8 +196,10 @@ def render_graph_explorer_cytoscape(graph_service): ] # 5. Rendering & Layout - # COSE Layout für optimale Abstände - selected_element = cytoscape( + graph_key = f"cy_{center_id}_{st.session_state.cy_depth}_{st.session_state.cy_ideal_edge_len}" + + # Das Event-Dictionary enthält die geklickten Elemente + clicked_elements = cytoscape( elements=cy_elements, stylesheet=stylesheet, layout={ @@ -199,24 +220,27 @@ def render_graph_explorer_cytoscape(graph_service): "coolingFactor": 0.95, "minTemp": 1.0 }, - # Dynamischer Key für Re-Render bei Einstellungsänderung - key=f"cy_{center_id}_{st.session_state.cy_depth}_{st.session_state.cy_ideal_edge_len}", + key=graph_key, height="800px" ) - # 6. Interaktions-Logik (Navigation) - if selected_element: - # st-cytoscape gibt ein Dictionary der selektierten Elemente zurück - # Struktur: {'nodes': ['id1'], 'edges': []} - clicked_nodes = selected_element.get("nodes", []) + # 6. Interaktions-Logik (Navigation Fix) + if clicked_elements: + # clicked_elements['nodes'] ist eine Liste von IDs + clicked_node_ids = clicked_elements.get("nodes", []) - if clicked_nodes: - clicked_id = clicked_nodes[0] - - # Wenn auf einen anderen Knoten geklickt wurde -> Navigieren - if clicked_id and clicked_id != center_id: - st.session_state.graph_center_id = clicked_id - st.rerun() + target_node = None + + # Wir suchen einen Node, der NICHT das aktuelle Zentrum ist + for nid in clicked_node_ids: + if nid != center_id: + target_node = nid + break + + # Wenn ein neuer Knoten gefunden wurde -> Navigieren + if target_node: + st.session_state.graph_center_id = target_node + st.rerun() else: st.info("👈 Bitte wähle links eine Notiz aus.") \ No newline at end of file