From f7a4dab7078ff8c4553769798bdffdf18a5203ab Mon Sep 17 00:00:00 2001 From: Lars Date: Sun, 14 Dec 2025 14:45:50 +0100 Subject: [PATCH] problem fix ausgehende Kanten --- app/frontend/ui_graph.py | 73 +++++++++++++++----------------- app/frontend/ui_graph_service.py | 50 +++++++++++++++++----- 2 files changed, 75 insertions(+), 48 deletions(-) diff --git a/app/frontend/ui_graph.py b/app/frontend/ui_graph.py index 3db5989..512e63d 100644 --- a/app/frontend/ui_graph.py +++ b/app/frontend/ui_graph.py @@ -8,24 +8,25 @@ def render_graph_explorer(graph_service): st.header("🕸️ Graph Explorer") # Session State initialisieren - if "graph_center_id" not in st.session_state: st.session_state.graph_center_id = None + if "graph_center_id" not in st.session_state: + st.session_state.graph_center_id = None - # Defaults speichern für Persistenz + # Defaults für View & Physik setzen st.session_state.setdefault("graph_depth", 2) st.session_state.setdefault("graph_show_labels", True) - # Defaults angepasst für BarnesHut (andere Skala!) - st.session_state.setdefault("graph_spacing", 150) - st.session_state.setdefault("graph_gravity", -3000) + st.session_state.setdefault("graph_spacing", 250) + st.session_state.setdefault("graph_gravity", -4000) col_ctrl, col_graph = st.columns([1, 4]) + # --- LINKE SPALTE: CONTROLS --- with col_ctrl: st.subheader("Fokus") - # Suche + # Sucheingabe search_term = st.text_input("Suche Notiz", placeholder="Titel eingeben...") - options = {} + # Suchlogik Qdrant if search_term: hits, _ = graph_service.client.scroll( collection_name=f"{COLLECTION_PREFIX}_notes", @@ -42,19 +43,18 @@ def render_graph_explorer(graph_service): st.divider() - # View Settings + # Layout & Physik Einstellungen 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)**") - # ACHTUNG: BarnesHut reagiert anders. Spring Length ist wichtig. - st.session_state.graph_spacing = st.slider("Federlänge (Abstand)", 50, 500, st.session_state.graph_spacing, help="Wie lang sollen die Verbindungen sein?") - st.session_state.graph_gravity = st.slider("Abstoßung (Gravity)", -20000, -1000, st.session_state.graph_gravity, help="Wie stark sollen sich Knoten abstoßen?") + st.session_state.graph_spacing = st.slider("Federlänge (Abstand)", 50, 800, st.session_state.graph_spacing) + st.session_state.graph_gravity = st.slider("Abstoßung (Gravity)", -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.session_state.graph_spacing = 250 + st.session_state.graph_gravity = -4000 st.rerun() st.divider() @@ -62,57 +62,55 @@ def render_graph_explorer(graph_service): for k, v in list(GRAPH_COLORS.items())[:8]: st.markdown(f" {k}", unsafe_allow_html=True) + # --- RECHTE SPALTE: GRAPH & ACTION BAR --- with col_graph: center_id = st.session_state.graph_center_id if center_id: - # Container für Action Bar OBERHALB des Graphen (Layout Fix) + # Action Container oben fixieren (Layout Fix) action_container = st.container() - # Graph Laden + # Graph und Daten laden with st.spinner(f"Lade Graph..."): - # Daten laden (Cache wird genutzt) nodes, edges = graph_service.get_ego_graph( center_id, depth=st.session_state.graph_depth, show_labels=st.session_state.graph_show_labels ) - # Fetch Note Data für Button & Debug - # Wir holen die Metadaten (inkl. path), was für den Editor-Callback reicht. - note_data = graph_service._fetch_note_cached(center_id) + # WICHTIG: Volle Daten inkl. Stitching für Editor holen + note_data = graph_service.get_note_with_full_content(center_id) - # --- ACTION BAR RENDEREN --- + # Action Bar rendern with action_container: - c_act1, c_act2 = st.columns([3, 1]) - with c_act1: + c1, c2 = st.columns([3, 1]) + with c1: st.caption(f"Aktives Zentrum: **{center_id}**") - with c_act2: + with c2: if note_data: st.button("📝 Bearbeiten", use_container_width=True, on_click=switch_to_editor_callback, args=(note_data,)) else: - st.error("Daten nicht verfügbar") + st.error("Datenfehler: Note nicht gefunden") - # DATA INSPECTOR (Payload Debug) - with st.expander("🕵️ Data Inspector (Payload Debug)", expanded=False): + # Debug Inspector + with st.expander("🕵️ Data Inspector", expanded=False): if note_data: st.json(note_data) - if 'path' not in note_data: - st.error("ACHTUNG: Feld 'path' fehlt im Qdrant-Payload! Update-Modus wird nicht funktionieren.") - else: - st.success(f"Pfad gefunden: {note_data['path']}") - else: - st.info("Keine Daten geladen.") + 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: # --- CONFIGURATION (BarnesHut) --- - # Height-Trick für Re-Render (da key-Parameter nicht funktioniert) - # Ändere Height minimal basierend auf Gravity + # Height-Trick für Re-Render (da key-Parameter manchmal crasht) dyn_height = 800 + (abs(st.session_state.graph_gravity) % 5) config = Config( @@ -121,11 +119,10 @@ def render_graph_explorer(graph_service): directed=True, physics={ "enabled": True, - # BarnesHut ist der Standard und stabilste Solver für Agraph "solver": "barnesHut", "barnesHut": { "gravitationalConstant": st.session_state.graph_gravity, - "centralGravity": 0.3, + "centralGravity": 0.005, "springLength": st.session_state.graph_spacing, "springConstant": 0.04, "damping": 0.09, @@ -141,7 +138,7 @@ def render_graph_explorer(graph_service): return_value = agraph(nodes=nodes, edges=edges, config=config) - # Interaktions-Logik + # Interaktions-Logik (Klick auf Node) if return_value: if return_value != center_id: # Navigation: Neues Zentrum setzen @@ -152,4 +149,4 @@ def render_graph_explorer(graph_service): st.toast(f"Zentrum: {return_value}") else: - st.info("👈 Bitte wähle links eine Notiz aus.") \ No newline at end of file + st.info("👈 Bitte wähle links eine Notiz aus, um den Graphen zu starten.") \ No newline at end of file diff --git a/app/frontend/ui_graph_service.py b/app/frontend/ui_graph_service.py index 3032e62..a32971a 100644 --- a/app/frontend/ui_graph_service.py +++ b/app/frontend/ui_graph_service.py @@ -12,6 +12,26 @@ class GraphExplorerService: self.edges_col = f"{prefix}_edges" self._note_cache = {} + def get_note_with_full_content(self, note_id): + """ + Lädt die Metadaten der Note und rekonstruiert den gesamten Text + aus den Chunks (Stitching). Wichtig für den Editor-Fallback. + """ + # 1. Metadaten holen + meta = self._fetch_note_cached(note_id) + if not meta: return None + + # 2. Volltext aus Chunks bauen + full_text = self._fetch_full_text_stitched(note_id) + + # 3. Ergebnis kombinieren (Wir überschreiben das 'fulltext' Feld mit dem frischen Stitching) + # Wir geben eine Kopie zurück, um den Cache nicht zu verfälschen + complete_note = meta.copy() + if full_text: + complete_note['fulltext'] = full_text + + return complete_note + def get_ego_graph(self, center_note_id: str, depth=2, show_labels=True): """ Erstellt den Ego-Graphen um eine zentrale Notiz. @@ -25,6 +45,7 @@ class GraphExplorerService: if not center_note: return [], [] self._add_node_to_dict(nodes_dict, center_note, level=0) + # Initialset für Suche level_1_ids = {center_note_id} # Suche Kanten für Center (L1) @@ -36,7 +57,7 @@ class GraphExplorerService: if tgt_id: level_1_ids.add(tgt_id) # Level 2 Suche (begrenzt für Performance) - if depth > 1 and len(level_1_ids) > 1 and len(level_1_ids) < 60: + if depth > 1 and len(level_1_ids) > 1 and len(level_1_ids) < 80: l1_subset = list(level_1_ids - {center_note_id}) if l1_subset: l2_edges = self._find_connected_edges_batch(l1_subset) @@ -107,6 +128,7 @@ class GraphExplorerService: full_text = [] for c in chunks: + # 'text' ist der reine Inhalt ohne Overlap txt = c.payload.get('text', '') if txt: full_text.append(txt) @@ -133,13 +155,16 @@ class GraphExplorerService: def _find_connected_edges(self, note_ids, note_title=None): """Findet eingehende und ausgehende Kanten für Nodes.""" - # 1. Chunk IDs ermitteln (da Edges oft an Chunks hängen) - scroll_filter = models.Filter(must=[models.FieldCondition(key="note_id", match=models.MatchAny(any=note_ids))]) - chunks, _ = self.client.scroll(self.chunks_col, scroll_filter=scroll_filter, limit=200) - chunk_ids = [c.id for c in chunks] results = [] + # 1. Chunk IDs ermitteln (da Edges oft an Chunks hängen) + chunk_ids = [] + if note_ids: + c_filter = models.Filter(must=[models.FieldCondition(key="note_id", match=models.MatchAny(any=note_ids))]) + chunks, _ = self.client.scroll(self.chunks_col, scroll_filter=c_filter, limit=300) + chunk_ids = [c.id for c in chunks] + # 2. Outgoing Edges suchen # Source kann sein: Eine Chunk ID ODER die Note ID selbst (bei direkten Links) source_candidates = chunk_ids + note_ids @@ -150,22 +175,27 @@ class GraphExplorerService: # FIX: MatchExcept Workaround für Pydantic models.FieldCondition(key="kind", match=models.MatchExcept(**{"except": SYSTEM_EDGES})) ]) - res_out, _ = self.client.scroll(self.edges_col, scroll_filter=out_f, limit=100, with_payload=True) + res_out, _ = self.client.scroll(self.edges_col, scroll_filter=out_f, limit=500, with_payload=True) results.extend(res_out) # 3. Incoming Edges suchen # Target kann sein: Chunk ID, Note ID, oder Note Titel (Wikilinks) shoulds = [] - if chunk_ids: shoulds.append(models.FieldCondition(key="target_id", match=models.MatchAny(any=chunk_ids))) - if note_title: shoulds.append(models.FieldCondition(key="target_id", match=models.MatchValue(value=note_title))) - shoulds.append(models.FieldCondition(key="target_id", match=models.MatchAny(any=note_ids))) + if chunk_ids: + shoulds.append(models.FieldCondition(key="target_id", match=models.MatchAny(any=chunk_ids))) + + if note_ids: + shoulds.append(models.FieldCondition(key="target_id", match=models.MatchAny(any=note_ids))) + + if note_title: + shoulds.append(models.FieldCondition(key="target_id", match=models.MatchValue(value=note_title))) if shoulds: in_f = models.Filter( must=[models.FieldCondition(key="kind", match=models.MatchExcept(**{"except": SYSTEM_EDGES}))], should=shoulds ) - res_in, _ = self.client.scroll(self.edges_col, scroll_filter=in_f, limit=100, with_payload=True) + res_in, _ = self.client.scroll(self.edges_col, scroll_filter=in_f, limit=500, with_payload=True) results.extend(res_in) return results