WP19 #10

Merged
Lars merged 36 commits from WP19 into main 2025-12-14 20:50:04 +01:00
4 changed files with 108 additions and 33 deletions
Showing only changes of commit 3861246ac6 - Show all commits

View File

@ -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"

View File

@ -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'<div class="intent-badge">Intent: {intent}</div>', 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:

View File

@ -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'<div class="draft-box" style="border-left: 5px solid #ff9f43;">', unsafe_allow_html=True)
else:

View File

@ -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.")