2025-12-14 08:34:23 +01:00
import streamlit as st
import uuid
import re
from datetime import datetime
from streamlit_agraph import agraph , Config
from qdrant_client import models
2025-12-14 12:06:25 +01:00
# Importe aus den anderen Modulen
2025-12-14 08:34:23 +01:00
from ui_utils import parse_markdown_draft , build_markdown_doc , load_history_from_logs , slugify
from ui_api import save_draft_to_vault , analyze_draft_text , send_chat_message , submit_feedback
2025-12-14 08:49:55 +01:00
from ui_config import HISTORY_FILE , COLLECTION_PREFIX , GRAPH_COLORS
2025-12-14 08:34:23 +01:00
2025-12-14 11:49:13 +01:00
# --- CALLBACKS ---
2025-12-14 12:42:24 +01:00
# Diese müssen zwingend VOR dem Aufruf definiert sein.
2025-12-14 11:49:13 +01:00
def switch_to_editor_callback ( note_payload ) :
"""
2025-12-14 12:06:25 +01:00
Callback für den Edit - Button im Graphen .
Lädt die Notiz in den Editor und setzt den Modus .
Nutzt den ' path ' - Parameter aus Qdrant als Single Source of Truth für Updates .
2025-12-14 11:49:13 +01:00
"""
2025-12-14 12:06:25 +01:00
# 1. Inhalt extrahieren (Fulltext oder Fallback)
2025-12-14 11:39:49 +01:00
content = note_payload . get ( ' fulltext ' , ' ' )
if not content :
content = build_markdown_doc ( note_payload , " Inhalt konnte nicht geladen werden (nur Metadaten verfügbar). " )
2025-12-14 12:06:25 +01:00
# 2. Single Source of Truth Bestimmung (Dateipfad)
# Priorität 1: Der absolute Pfad aus dem Ingest-Prozess ('path')
origin_fname = note_payload . get ( ' path ' )
2025-12-14 11:54:20 +01:00
2025-12-14 12:42:24 +01:00
# Priorität 2: 'file_path' oder 'filename' (Legacy Felder, Fallback)
2025-12-14 12:06:25 +01:00
if not origin_fname :
origin_fname = note_payload . get ( ' file_path ' ) or note_payload . get ( ' filename ' )
2025-12-14 12:42:24 +01:00
# Priorität 3: Konstruktion aus ID (Notlösung, falls Metadaten unvollständig)
2025-12-14 12:06:25 +01:00
if not origin_fname and ' note_id ' in note_payload :
# Annahme: Datei heißt {note_id}.md im Vault Root
origin_fname = f " { note_payload [ ' note_id ' ] } .md "
# 3. Message in den Chat-Verlauf injecten (dient als Datencontainer für den Editor)
2025-12-14 12:42:24 +01:00
# Wir fügen eine "künstliche" Assistant-Nachricht hinzu, die der Editor dann ausliest.
2025-12-14 11:39:49 +01:00
st . session_state . messages . append ( {
" role " : " assistant " ,
" intent " : " INTERVIEW " ,
" content " : content ,
2025-12-14 11:54:20 +01:00
" query_id " : f " edit_ { note_payload [ ' note_id ' ] } " ,
2025-12-14 12:06:25 +01:00
" origin_filename " : origin_fname , # Pfad für Speicher-Logik
" origin_note_id " : note_payload [ ' note_id ' ]
2025-12-14 11:39:49 +01:00
} )
2025-12-14 12:06:25 +01:00
# 4. Modus umschalten
2025-12-14 12:42:24 +01:00
# Wir setzen den Key des Radio-Buttons im Session State
2025-12-14 11:39:49 +01:00
st . session_state [ " sidebar_mode_selection " ] = " 📝 Manueller Editor "
2025-12-14 11:49:13 +01:00
# --- UI RENDERER ---
2025-12-14 11:39:49 +01:00
2025-12-14 08:34:23 +01:00
def render_sidebar ( ) :
2025-12-14 12:06:25 +01:00
"""
Rendert die Seitenleiste mit Navigation und Einstellungen .
"""
2025-12-14 08:34:23 +01:00
with st . sidebar :
st . title ( " 🧠 mindnet " )
st . caption ( " v2.6 | WP-19 Graph View " )
2025-12-14 11:39:49 +01:00
2025-12-14 12:06:25 +01:00
# Modus-Auswahl mit State-Key für programmatische Umschaltung
2025-12-14 11:39:49 +01:00
if " sidebar_mode_selection " not in st . session_state :
st . session_state [ " sidebar_mode_selection " ] = " 💬 Chat "
mode = st . radio (
" Modus " ,
[ " 💬 Chat " , " 📝 Manueller Editor " , " 🕸️ Graph Explorer " ] ,
key = " sidebar_mode_selection "
)
2025-12-14 08:49:55 +01:00
2025-12-14 08:34:23 +01:00
st . divider ( )
st . subheader ( " ⚙️ Settings " )
top_k = st . slider ( " Quellen (Top-K) " , 1 , 10 , 5 )
explain = st . toggle ( " Explanation Layer " , True )
2025-12-14 08:49:55 +01:00
2025-12-14 08:34:23 +01:00
st . divider ( )
st . subheader ( " 🕒 Verlauf " )
2025-12-14 12:06:25 +01:00
# Suchhistorie laden
2025-12-14 08:34:23 +01:00
for q in load_history_from_logs ( HISTORY_FILE , 8 ) :
if st . button ( f " 🔎 { q [ : 25 ] } ... " , key = f " hist_ { q } " , use_container_width = True ) :
st . session_state . messages . append ( { " role " : " user " , " content " : q } )
st . rerun ( )
2025-12-14 12:06:25 +01:00
2025-12-14 08:34:23 +01:00
return mode , top_k , explain
def render_draft_editor ( msg ) :
2025-12-14 08:49:55 +01:00
"""
2025-12-14 12:06:25 +01:00
Der Markdown - Editor .
Wird für neue Entwürfe UND für das Bearbeiten bestehender Notizen genutzt .
2025-12-14 08:49:55 +01:00
"""
2025-12-14 12:06:25 +01:00
# ID Generierung für Unique Keys
2025-12-14 08:34:23 +01:00
if " query_id " not in msg or not msg [ " query_id " ] :
msg [ " query_id " ] = str ( uuid . uuid4 ( ) )
qid = msg [ " query_id " ]
key_base = f " draft_ { qid } "
2025-12-14 12:06:25 +01:00
# State Keys definieren
2025-12-14 08:34:23 +01:00
data_meta_key = f " { key_base } _data_meta "
data_sugg_key = f " { key_base } _data_suggestions "
widget_body_key = f " { key_base } _widget_body "
data_body_key = f " { key_base } _data_body "
2025-12-14 12:06:25 +01:00
# --- INITIALISIERUNG ---
2025-12-14 08:34:23 +01:00
if f " { key_base } _init " not in st . session_state :
2025-12-14 12:06:25 +01:00
# Markdown parsen
2025-12-14 08:34:23 +01:00
meta , body = parse_markdown_draft ( msg [ " content " ] )
if " type " not in meta : meta [ " type " ] = " default "
if " title " not in meta : meta [ " title " ] = " "
tags = meta . get ( " tags " , [ ] )
meta [ " tags_str " ] = " , " . join ( tags ) if isinstance ( tags , list ) else str ( tags )
2025-12-14 12:06:25 +01:00
# Daten in Session State schreiben
2025-12-14 08:34:23 +01:00
st . session_state [ data_meta_key ] = meta
st . session_state [ data_sugg_key ] = [ ]
st . session_state [ data_body_key ] = body . strip ( )
2025-12-14 08:49:55 +01:00
2025-12-14 12:06:25 +01:00
# Widget-Werte vorbelegen
2025-12-14 08:34:23 +01:00
st . session_state [ f " { key_base } _wdg_title " ] = meta [ " title " ]
st . session_state [ f " { key_base } _wdg_type " ] = meta [ " type " ]
st . session_state [ f " { key_base } _wdg_tags " ] = meta [ " tags_str " ]
2025-12-14 11:54:20 +01:00
2025-12-14 12:06:25 +01:00
# WICHTIG: Original-Pfad aus der Message übernehmen (für Update-Logik)
st . session_state [ f " { key_base } _origin_filename " ] = msg . get ( " origin_filename " )
2025-12-14 08:34:23 +01:00
st . session_state [ f " { key_base } _init " ] = True
2025-12-14 12:06:25 +01:00
# --- STATE RESURRECTION (falls Streamlit rerunt) ---
2025-12-14 08:34:23 +01:00
if widget_body_key not in st . session_state and data_body_key in st . session_state :
st . session_state [ widget_body_key ] = st . session_state [ data_body_key ]
2025-12-14 12:06:25 +01:00
# --- SYNC FUNKTIONEN ---
2025-12-14 08:34:23 +01:00
def _sync_meta ( ) :
meta = st . session_state [ data_meta_key ]
meta [ " title " ] = st . session_state . get ( f " { key_base } _wdg_title " , " " )
meta [ " type " ] = st . session_state . get ( f " { key_base } _wdg_type " , " default " )
meta [ " tags_str " ] = st . session_state . get ( f " { key_base } _wdg_tags " , " " )
st . session_state [ data_meta_key ] = meta
def _sync_body ( ) :
st . session_state [ data_body_key ] = st . session_state [ widget_body_key ]
def _insert_text ( text_to_insert ) :
current = st . session_state . get ( widget_body_key , " " )
new_text = f " { current } \n \n { text_to_insert } "
st . session_state [ widget_body_key ] = new_text
st . session_state [ data_body_key ] = new_text
def _remove_text ( text_to_remove ) :
current = st . session_state . get ( widget_body_key , " " )
new_text = current . replace ( text_to_remove , " " ) . strip ( )
st . session_state [ widget_body_key ] = new_text
st . session_state [ data_body_key ] = new_text
2025-12-14 12:06:25 +01:00
# --- LAYOUT HEADER ---
2025-12-14 11:54:20 +01:00
origin_fname = st . session_state . get ( f " { key_base } _origin_filename " )
2025-12-14 12:06:25 +01:00
2025-12-14 11:54:20 +01:00
if origin_fname :
2025-12-14 12:06:25 +01:00
# Update Modus
display_name = str ( origin_fname ) . split ( " / " ) [ - 1 ] # Nur Dateiname anzeigen
2025-12-14 12:42:24 +01:00
st . success ( f " 📂 **Datei-Modus**: ` { origin_fname } ` " ) # Voller Pfad zur Sicherheit
2025-12-14 11:54:20 +01:00
st . markdown ( f ' <div class= " draft-box " style= " border-left: 5px solid #ff9f43; " > ' , unsafe_allow_html = True )
else :
2025-12-14 12:06:25 +01:00
# Create Modus
st . info ( " ✨ **Erstell-Modus**: Neue Datei wird angelegt. " )
2025-12-14 11:54:20 +01:00
st . markdown ( f ' <div class= " draft-box " > ' , unsafe_allow_html = True )
st . markdown ( " ### Editor " )
2025-12-14 08:49:55 +01:00
2025-12-14 12:06:25 +01:00
# Metadaten-Editor
2025-12-14 11:54:20 +01:00
meta_ref = st . session_state [ data_meta_key ]
2025-12-14 08:34:23 +01:00
c1 , c2 = st . columns ( [ 2 , 1 ] )
with c1 :
st . text_input ( " Titel " , key = f " { key_base } _wdg_title " , on_change = _sync_meta )
with c2 :
known_types = [ " concept " , " project " , " decision " , " experience " , " journal " , " value " , " goal " , " principle " , " risk " , " belief " ]
curr_type = st . session_state . get ( f " { key_base } _wdg_type " , meta_ref [ " type " ] )
if curr_type not in known_types : known_types . append ( curr_type )
st . selectbox ( " Typ " , known_types , key = f " { key_base } _wdg_type " , on_change = _sync_meta )
st . text_input ( " Tags " , key = f " { key_base } _wdg_tags " , on_change = _sync_meta )
2025-12-14 12:06:25 +01:00
# Tabs
2025-12-14 08:34:23 +01:00
tab_edit , tab_intel , tab_view = st . tabs ( [ " ✏️ Inhalt " , " 🧠 Intelligence " , " 👁️ Vorschau " ] )
with tab_edit :
2025-12-14 12:06:25 +01:00
st . text_area ( " Body " , key = widget_body_key , height = 600 , on_change = _sync_body , label_visibility = " collapsed " )
2025-12-14 08:34:23 +01:00
with tab_intel :
st . info ( " Klicke auf ' Analysieren ' , um Verknüpfungen für den AKTUELLEN Text zu finden. " )
2025-12-14 08:49:55 +01:00
2025-12-14 08:34:23 +01:00
if st . button ( " 🔍 Analyse starten " , key = f " { key_base } _analyze " ) :
st . session_state [ data_sugg_key ] = [ ]
text_to_analyze = st . session_state . get ( widget_body_key , st . session_state . get ( data_body_key , " " ) )
current_doc_type = st . session_state . get ( f " { key_base } _wdg_type " , " concept " )
with st . spinner ( " Analysiere... " ) :
analysis = analyze_draft_text ( text_to_analyze , current_doc_type )
if " error " in analysis :
st . error ( f " Fehler: { analysis [ ' error ' ] } " )
else :
suggestions = analysis . get ( " suggestions " , [ ] )
st . session_state [ data_sugg_key ] = suggestions
if not suggestions : st . warning ( " Keine Vorschläge gefunden. " )
else : st . success ( f " { len ( suggestions ) } Vorschläge gefunden. " )
2025-12-14 12:06:25 +01:00
# Vorschläge rendern
2025-12-14 08:34:23 +01:00
suggestions = st . session_state [ data_sugg_key ]
if suggestions :
current_text_state = st . session_state . get ( widget_body_key , " " )
for idx , sugg in enumerate ( suggestions ) :
link_text = sugg . get ( ' suggested_markdown ' , ' ' )
is_inserted = link_text in current_text_state
2025-12-14 12:06:25 +01:00
2025-12-14 08:34:23 +01:00
bg_color = " #e6fffa " if is_inserted else " #ffffff "
border = " 3px solid #28a745 " if is_inserted else " 3px solid #1a73e8 "
2025-12-14 08:49:55 +01:00
2025-12-14 08:34:23 +01:00
st . markdown ( f """
< div style = " border-left: {border} ; background-color: {bg_color} ; padding: 10px; margin-bottom: 8px; border-radius: 4px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); " >
< b > { sugg . get ( ' target_title ' ) } < / b > < small > ( { sugg . get ( ' type ' ) } ) < / small > < br >
< i > { sugg . get ( ' reason ' ) } < / i > < br >
< code > { link_text } < / code >
< / div >
""" , unsafe_allow_html=True)
2025-12-14 08:49:55 +01:00
2025-12-14 08:34:23 +01:00
if is_inserted :
st . button ( " ❌ Entfernen " , key = f " del_ { idx } _ { key_base } " , on_click = _remove_text , args = ( link_text , ) )
else :
st . button ( " ➕ Einfügen" , key = f " add_ { idx } _ { key_base } " , on_click = _insert_text , args = ( link_text , ) )
2025-12-14 12:06:25 +01:00
# Dokument für Vorschau/Save bauen
2025-12-14 08:34:23 +01:00
final_tags_str = st . session_state . get ( f " { key_base } _wdg_tags " , " " )
final_tags = [ t . strip ( ) for t in final_tags_str . split ( " , " ) if t . strip ( ) ]
2025-12-14 08:49:55 +01:00
2025-12-14 08:34:23 +01:00
final_meta = {
" id " : " generated_on_save " ,
" type " : st . session_state . get ( f " { key_base } _wdg_type " , " default " ) ,
" title " : st . session_state . get ( f " { key_base } _wdg_title " , " " ) . strip ( ) ,
" status " : " draft " ,
" tags " : final_tags
}
2025-12-14 11:54:20 +01:00
2025-12-14 12:06:25 +01:00
# ID behalten wenn vorhanden (Wichtig für Source of Truth)
2025-12-14 11:54:20 +01:00
if " origin_note_id " in msg :
2025-12-14 12:06:25 +01:00
final_meta [ " id " ] = msg [ " origin_note_id " ]
2025-12-14 11:54:20 +01:00
2025-12-14 08:34:23 +01:00
final_body = st . session_state . get ( widget_body_key , st . session_state [ data_body_key ] )
2025-12-14 08:49:55 +01:00
2025-12-14 12:06:25 +01:00
# Fallback Titel aus H1
2025-12-14 08:34:23 +01:00
if not final_meta [ " title " ] :
h1_match = re . search ( r " ^# \ s+(.*)$ " , final_body , re . MULTILINE )
if h1_match : final_meta [ " title " ] = h1_match . group ( 1 ) . strip ( )
final_doc = build_markdown_doc ( final_meta , final_body )
with tab_view :
st . markdown ( ' <div class= " preview-box " > ' , unsafe_allow_html = True )
st . markdown ( final_doc )
st . markdown ( ' </div> ' , unsafe_allow_html = True )
st . markdown ( " --- " )
2025-12-14 08:49:55 +01:00
2025-12-14 12:06:25 +01:00
# Footer Actions
2025-12-14 08:34:23 +01:00
b1 , b2 = st . columns ( [ 1 , 1 ] )
with b1 :
2025-12-14 12:06:25 +01:00
# Label dynamisch
2025-12-14 12:42:24 +01:00
save_label = " 💾 Speichern (Überschreiben) " if origin_fname else " 💾 Neu anlegen & Indizieren "
2025-12-14 11:54:20 +01:00
2025-12-14 12:06:25 +01:00
if st . button ( save_label , type = " primary " , key = f " { key_base } _save " ) :
2025-12-14 08:34:23 +01:00
with st . spinner ( " Speichere im Vault... " ) :
2025-12-14 08:49:55 +01:00
2025-12-14 11:54:20 +01:00
# ENTSCHEIDUNG: Update oder Neu?
if origin_fname :
2025-12-14 12:06:25 +01:00
# UPDATE: Wir nutzen den exakten Pfad aus Qdrant
2025-12-14 11:54:20 +01:00
target_filename = origin_fname
else :
# NEU: Wir generieren einen Namen
2025-12-14 12:42:24 +01:00
raw_title = final_meta . get ( " title " , " draft " )
target_file = f " { datetime . now ( ) . strftime ( ' % Y % m %d ' ) } - { slugify ( raw_title ) [ : 60 ] } .md "
target_filename = target_file
2025-12-14 11:54:20 +01:00
2025-12-14 12:06:25 +01:00
# Senden an API
2025-12-14 11:54:20 +01:00
result = save_draft_to_vault ( final_doc , filename = target_filename )
if " error " in result :
st . error ( f " Fehler: { result [ ' error ' ] } " )
2025-12-14 08:34:23 +01:00
else :
st . success ( f " Gespeichert: { result . get ( ' file_path ' ) } " )
st . balloons ( )
2025-12-14 11:54:20 +01:00
2025-12-14 08:34:23 +01:00
with b2 :
if st . button ( " 📋 Code anzeigen " , key = f " { key_base } _btn_copy " ) :
st . code ( final_doc , language = " markdown " )
2025-12-14 08:49:55 +01:00
2025-12-14 08:34:23 +01:00
st . markdown ( " </div> " , unsafe_allow_html = True )
def render_chat_interface ( top_k , explain ) :
2025-12-14 12:06:25 +01:00
"""
Rendert das Chat - Interface .
"""
2025-12-14 08:34:23 +01:00
for idx , msg in enumerate ( st . session_state . messages ) :
with st . chat_message ( msg [ " role " ] ) :
if msg [ " role " ] == " assistant " :
intent = msg . get ( " intent " , " UNKNOWN " )
src = msg . get ( " intent_source " , " ? " )
icon = { " EMPATHY " : " ❤️ " , " DECISION " : " ⚖️ " , " CODING " : " 💻 " , " FACT " : " 📚 " , " INTERVIEW " : " 📝 " } . get ( intent , " 🧠 " )
st . markdown ( f ' <div class= " intent-badge " > { icon } Intent: { intent } <span style= " opacity:0.6; font-size:0.8em " >( { src } )</span></div> ' , unsafe_allow_html = True )
with st . expander ( " 🐞 Debug Raw Payload " , expanded = False ) :
st . json ( msg )
if intent == " INTERVIEW " :
render_draft_editor ( msg )
else :
st . markdown ( msg [ " content " ] )
if " sources " in msg and msg [ " sources " ] :
for hit in msg [ " sources " ] :
with st . expander ( f " 📄 { hit . get ( ' note_id ' , ' ? ' ) } ( { hit . get ( ' total_score ' , 0 ) : .2f } ) " ) :
st . markdown ( f " _ { hit . get ( ' source ' , { } ) . get ( ' text ' , ' ' ) [ : 300 ] } ..._ " )
if hit . get ( ' explanation ' ) :
st . caption ( f " Grund: { hit [ ' explanation ' ] [ ' reasons ' ] [ 0 ] [ ' message ' ] } " )
2025-12-14 08:49:55 +01:00
2025-12-14 08:34:23 +01:00
def _cb ( qid = msg . get ( " query_id " ) , nid = hit . get ( ' node_id ' ) ) :
val = st . session_state . get ( f " fb_src_ { qid } _ { nid } " )
if val is not None : submit_feedback ( qid , nid , val + 1 )
st . feedback ( " faces " , key = f " fb_src_ { msg . get ( ' query_id ' ) } _ { hit . get ( ' node_id ' ) } " , on_change = _cb )
if " query_id " in msg :
qid = msg [ " query_id " ]
st . feedback ( " stars " , key = f " fb_glob_ { qid } " , on_change = lambda : submit_feedback ( qid , " generated_answer " , st . session_state [ f " fb_glob_ { qid } " ] + 1 ) )
else :
st . markdown ( msg [ " content " ] )
if prompt := st . chat_input ( " Frage Mindnet... " ) :
st . session_state . messages . append ( { " role " : " user " , " content " : prompt } )
st . rerun ( )
if len ( st . session_state . messages ) > 0 and st . session_state . messages [ - 1 ] [ " role " ] == " user " :
with st . chat_message ( " assistant " ) :
with st . spinner ( " Thinking... " ) :
resp = send_chat_message ( st . session_state . messages [ - 1 ] [ " content " ] , top_k , explain )
if " error " in resp :
st . error ( resp [ " error " ] )
else :
st . session_state . messages . append ( {
" role " : " assistant " ,
" content " : resp . get ( " answer " ) ,
" intent " : resp . get ( " intent " , " FACT " ) ,
" intent_source " : resp . get ( " intent_source " , " Unknown " ) ,
" sources " : resp . get ( " sources " , [ ] ) ,
" query_id " : resp . get ( " query_id " )
} )
st . rerun ( )
def render_manual_editor ( ) :
2025-12-14 12:06:25 +01:00
"""
Wrapper für den manuellen Editor - Modus ( Startet mit leerem Template ) .
"""
2025-12-14 08:34:23 +01:00
mock_msg = {
" content " : " --- \n type: concept \n title: Neue Notiz \n status: draft \n tags: [] \n --- \n # Titel \n " ,
" query_id " : " manual_mode_v2 "
}
render_draft_editor ( mock_msg )
2025-12-14 11:49:13 +01:00
# --- GRAPH EXPLORER ---
2025-12-14 11:39:49 +01:00
2025-12-14 08:34:23 +01:00
def render_graph_explorer ( graph_service ) :
2025-12-14 08:49:55 +01:00
st . header ( " 🕸️ Graph Explorer " )
2025-12-14 08:34:23 +01:00
2025-12-14 12:06:25 +01:00
# Session State initialisieren
2025-12-14 11:39:49 +01:00
if " graph_center_id " not in st . session_state : st . session_state . graph_center_id = None
2025-12-14 12:06:25 +01:00
# Defaults speichern für Persistenz
2025-12-14 11:39:49 +01:00
st . session_state . setdefault ( " graph_depth " , 2 )
st . session_state . setdefault ( " graph_show_labels " , True )
2025-12-14 12:42:24 +01:00
# Defaults angepasst für BarnesHut (Skalierung angepasst)
2025-12-14 12:25:52 +01:00
st . session_state . setdefault ( " graph_spacing " , 150 )
2025-12-14 11:39:49 +01:00
st . session_state . setdefault ( " graph_gravity " , - 3000 )
2025-12-14 08:49:55 +01:00
2025-12-14 11:27:43 +01:00
col_ctrl , col_graph = st . columns ( [ 1 , 4 ] )
2025-12-14 08:34:23 +01:00
with col_ctrl :
2025-12-14 08:49:55 +01:00
st . subheader ( " Fokus " )
2025-12-14 08:34:23 +01:00
2025-12-14 12:06:25 +01:00
# Suche
2025-12-14 08:49:55 +01:00
search_term = st . text_input ( " Suche Notiz " , placeholder = " Titel eingeben... " )
options = { }
2025-12-14 08:34:23 +01:00
if search_term :
hits , _ = graph_service . client . scroll (
collection_name = f " { COLLECTION_PREFIX } _notes " ,
2025-12-14 11:39:49 +01:00
scroll_filter = models . Filter ( must = [ models . FieldCondition ( key = " title " , match = models . MatchText ( text = search_term ) ) ] ) ,
2025-12-14 08:34:23 +01:00
limit = 10
)
options = { h . payload [ ' title ' ] : h . payload [ ' note_id ' ] for h in hits }
2025-12-14 11:39:49 +01:00
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 ( )
2025-12-14 08:34:23 +01:00
2025-12-14 11:39:49 +01:00
st . divider ( )
2025-12-14 08:49:55 +01:00
2025-12-14 12:06:25 +01:00
# View Settings
2025-12-14 11:39:49 +01:00
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 )
2025-12-14 12:25:52 +01:00
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? " )
2025-12-14 11:39:49 +01:00
2025-12-14 12:25:52 +01:00
if st . button ( " Reset Layout " ) :
st . session_state . graph_spacing = 150
2025-12-14 11:39:49 +01:00
st . session_state . graph_gravity = - 3000
st . rerun ( )
2025-12-14 08:49:55 +01:00
st . divider ( )
2025-12-14 11:39:49 +01:00
st . caption ( " Legende (Top Typen) " )
2025-12-14 08:49:55 +01:00
for k , v in list ( GRAPH_COLORS . items ( ) ) [ : 8 ] :
st . markdown ( f " <span style= ' color: { v } ' >●</span> { k } " , unsafe_allow_html = True )
2025-12-14 08:34:23 +01:00
with col_graph :
2025-12-14 08:49:55 +01:00
center_id = st . session_state . graph_center_id
if center_id :
2025-12-14 12:42:24 +01:00
# Container für Action Bar OBERHALB des Graphen (Layout Fix)
action_container = st . container ( )
# Graph Laden
2025-12-14 11:39:49 +01:00
with st . spinner ( f " Lade Graph... " ) :
2025-12-14 12:42:24 +01:00
# Daten laden (Cache wird genutzt)
2025-12-14 11:39:49 +01:00
nodes , edges = graph_service . get_ego_graph (
center_id ,
depth = st . session_state . graph_depth ,
show_labels = st . session_state . graph_show_labels
)
2025-12-14 08:34:23 +01:00
2025-12-14 12:42:24 +01:00
# Fetch Note Data für Button & Debug
note_data = graph_service . _fetch_note_cached ( center_id )
# --- ACTION BAR RENDEREN ---
with action_container :
c_act1 , c_act2 = st . columns ( [ 3 , 1 ] )
with c_act1 :
st . caption ( f " Aktives Zentrum: ** { center_id } ** " )
with c_act2 :
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 " )
# DATA INSPECTOR (Payload Debug)
with st . expander ( " 🕵️ Data Inspector (Payload Debug) " , 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. " )
2025-12-14 11:39:49 +01:00
else :
2025-12-14 12:42:24 +01:00
st . success ( f " Pfad gefunden: { note_data [ ' path ' ] } " )
else :
st . info ( " Keine Daten geladen. " )
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
dyn_height = 800 + ( abs ( st . session_state . graph_gravity ) % 3 )
config = Config (
width = 1000 ,
height = dyn_height ,
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 ,
" 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 )
# Interaktions-Logik
if return_value :
if return_value != center_id :
# Navigation: Neues Zentrum setzen
st . session_state . graph_center_id = return_value
st . rerun ( )
else :
# Klick auf das Zentrum selbst
st . toast ( f " Zentrum: { return_value } " )
2025-12-14 11:39:49 +01:00
2025-12-14 08:34:23 +01:00
else :
2025-12-14 12:42:24 +01:00
st . info ( " 👈 Bitte wähle links eine Notiz aus. " )