diff --git a/app/frontend/ui_callbacks.py b/app/frontend/ui_callbacks.py
index 93c2d6e..b9bf955 100644
--- a/app/frontend/ui_callbacks.py
+++ b/app/frontend/ui_callbacks.py
@@ -1,37 +1,61 @@
import streamlit as st
+import os
from ui_utils import build_markdown_doc
def switch_to_editor_callback(note_payload):
"""
Callback für den 'Bearbeiten'-Button im Graphen.
- Bereitet den Session-State vor, damit der Editor im Update-Modus startet.
+ Versucht, die Datei direkt aus dem Vault (Dateisystem) zu lesen.
+ Das garantiert, dass Frontmatter und Inhalt vollständig sind (Single Source of Truth).
"""
- # 1. Inhalt extrahieren (Fulltext bevorzugt, sonst Fallback)
- content = note_payload.get('fulltext', '')
- if not content:
- content = build_markdown_doc(note_payload, "Inhalt konnte nicht geladen werden (nur Metadaten verfügbar).")
-
- # 2. Single Source of Truth: 'path' Feld (Absoluter Pfad)
+ # 1. Pfad ermitteln (Priorität auf 'path' aus Qdrant)
origin_fname = note_payload.get('path')
- # Fallback: Falls 'path' leer ist (Legacy Daten)
+ # Fallback für Legacy-Datenfelder
if not origin_fname:
origin_fname = note_payload.get('file_path') or note_payload.get('filename')
- # Notfall-Fallback: Konstruktion aus ID
+ content = ""
+ file_loaded = False
+
+ # 2. Versuch: Direkt von der Festplatte lesen
+ # Wir prüfen, ob der Pfad existiert und lesen den aktuellen Stand der Datei.
+ if origin_fname and os.path.exists(origin_fname):
+ try:
+ with open(origin_fname, "r", encoding="utf-8") as f:
+ content = f.read()
+ file_loaded = True
+ except Exception as e:
+ # Fehler im Terminal loggen, aber UI nicht crashen lassen
+ print(f"Fehler beim Lesen von {origin_fname}: {e}")
+
+ # 3. Fallback: Inhalt aus Qdrant nehmen (wenn Datei nicht zugreifbar)
+ if not file_loaded:
+ # Wir nehmen 'fulltext' aus dem Payload
+ content = note_payload.get('fulltext', '')
+
+ if not content:
+ # Letzter Ausweg: Metadaten nehmen und Dummy-Content bauen
+ content = build_markdown_doc(note_payload, "Inhalt konnte nicht geladen werden (Datei nicht gefunden).")
+ else:
+ # Check: Hat der Text ein Frontmatter? Wenn nein, rekonstruieren wir es.
+ if not content.strip().startswith("---"):
+ content = build_markdown_doc(note_payload, content)
+
+ # Notfall-Pfad Konstruktion (falls gar kein Pfad im System ist)
if not origin_fname and 'note_id' in note_payload:
origin_fname = f"{note_payload['note_id']}.md"
- # 3. Message in den Chat-Verlauf injecten
- # WICHTIG: query_id muss mit 'edit_' beginnen, damit render_manual_editor sie erkennt!
+ # 4. Daten an den Editor übergeben
+ # Wir nutzen den Chat-Verlauf als Transportmittel für den State
st.session_state.messages.append({
"role": "assistant",
"intent": "INTERVIEW",
"content": content,
- "query_id": f"edit_{note_payload['note_id']}", # Trigger für Erkennung
+ "query_id": f"edit_{note_payload.get('note_id', 'unknown')}", # Trigger für den Editor
"origin_filename": origin_fname,
- "origin_note_id": note_payload['note_id']
+ "origin_note_id": note_payload.get('note_id')
})
- # 4. Modus umschalten (erzwingt Wechsel zum Editor-Tab beim nächsten Re-Run)
+ # 5. Modus umschalten (wechselt den Tab beim nächsten Rerun)
st.session_state["sidebar_mode_selection"] = "📝 Manueller Editor"
\ No newline at end of file
diff --git a/app/frontend/ui_chat.py b/app/frontend/ui_chat.py
index 3b5c56d..31b552a 100644
--- a/app/frontend/ui_chat.py
+++ b/app/frontend/ui_chat.py
@@ -3,46 +3,68 @@ from ui_api import send_chat_message, submit_feedback
from ui_editor import render_draft_editor
def render_chat_interface(top_k, explain):
+ """
+ Rendert das Chat-Interface.
+ Zeigt Nachrichten an und behandelt User-Input.
+ """
+ # 1. Verlauf anzeigen
for idx, msg in enumerate(st.session_state.messages):
with st.chat_message(msg["role"]):
if msg["role"] == "assistant":
+ # Intent Badge
intent = msg.get("intent", "UNKNOWN")
st.markdown(f'
Intent: {intent}
', unsafe_allow_html=True)
+ # Debugging (optional, gut für Entwicklung)
with st.expander("🐞 Payload", expanded=False):
st.json(msg)
+ # Unterscheidung: Normaler Text oder Editor-Modus (Interview)
if intent == "INTERVIEW":
render_draft_editor(msg)
else:
st.markdown(msg["content"])
+ # Quellen anzeigen
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})"):
+ score = hit.get('total_score', 0)
+ # Wenn score None ist, 0.0 annehmen
+ if score is None: score = 0.0
+
+ with st.expander(f"📄 {hit.get('note_id', '?')} ({score:.2f})"):
st.markdown(f"_{hit.get('source', {}).get('text', '')[:300]}..._")
+
+ # Explanation Layer
if hit.get('explanation'):
st.caption(f"Grund: {hit['explanation']['reasons'][0]['message']}")
+ # Feedback Buttons pro Source
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)
+ # Globales Feedback für die Antwort
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:
+ # User Nachricht
st.markdown(msg["content"])
+ # 2. Input Feld
if prompt := st.chat_input("Frage Mindnet..."):
st.session_state.messages.append({"role": "user", "content": prompt})
st.rerun()
+ # 3. Antwort generieren (wenn letzte Nachricht vom User ist)
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:
diff --git a/app/frontend/ui_editor.py b/app/frontend/ui_editor.py
index 29e37bd..0dd2f80 100644
--- a/app/frontend/ui_editor.py
+++ b/app/frontend/ui_editor.py
@@ -75,8 +75,8 @@ def render_draft_editor(msg):
# Dateiname extrahieren für saubere Anzeige
display_name = str(origin_fname).split("/")[-1]
st.success(f"📂 **Update-Modus**: `{display_name}`")
- # Debugging: Zeige vollen Pfad im Tooltip oder klein darunter
- with st.expander("Pfad-Details", expanded=False):
+ # Debugging: Zeige vollen Pfad im Expander
+ with st.expander("Dateipfad Details", expanded=False):
st.code(origin_fname)
st.markdown(f'', unsafe_allow_html=True)
else:
diff --git a/app/frontend/ui_graph.py b/app/frontend/ui_graph.py
index 6d438e3..3db5989 100644
--- a/app/frontend/ui_graph.py
+++ b/app/frontend/ui_graph.py
@@ -7,11 +7,13 @@ from ui_callbacks import switch_to_editor_callback
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
- # Defaults
+ # Defaults speichern für Persistenz
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)
@@ -19,8 +21,11 @@ def render_graph_explorer(graph_service):
with col_ctrl:
st.subheader("Fokus")
+
+ # Suche
search_term = st.text_input("Suche Notiz", placeholder="Titel eingeben...")
+ options = {}
if search_term:
hits, _ = graph_service.client.scroll(
collection_name=f"{COLLECTION_PREFIX}_notes",
@@ -28,6 +33,7 @@ def render_graph_explorer(graph_service):
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):
@@ -35,13 +41,16 @@ def render_graph_explorer(graph_service):
st.rerun()
st.divider()
+
+ # View Settings
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)
+ # 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?")
if st.button("Reset Layout"):
st.session_state.graph_spacing = 150
@@ -57,46 +66,62 @@ def render_graph_explorer(graph_service):
center_id = st.session_state.graph_center_id
if center_id:
- # Action Container oben
+ # Container für Action Bar OBERHALB des Graphen (Layout Fix)
action_container = st.container()
+ # Graph 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)
- # Action Bar rendern
+ # --- ACTION BAR RENDEREN ---
with action_container:
- c1, c2 = st.columns([3, 1])
- with c1: st.caption(f"Aktives Zentrum: **{center_id}**")
- with c2:
+ 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,))
+ st.button("📝 Bearbeiten",
+ use_container_width=True,
+ on_click=switch_to_editor_callback,
+ args=(note_data,))
else:
- st.error("Datenfehler")
+ st.error("Daten nicht verfügbar")
- with st.expander("🕵️ Data Inspector", expanded=False):
+ # DATA INSPECTOR (Payload Debug)
+ with st.expander("🕵️ Data Inspector (Payload Debug)", 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 '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 not nodes:
st.warning("Keine Daten gefunden.")
else:
- # Physik Config für BarnesHut + Height-Trick
+ # --- 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) % 5)
config = Config(
width=1000,
- height=dyn_height,
+ 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,
@@ -116,11 +141,15 @@ def render_graph_explorer(graph_service):
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}")
+
else:
st.info("👈 Bitte wähle links eine Notiz aus.")
\ No newline at end of file