WP19 #10
|
|
@ -1,37 +1,61 @@
|
||||||
import streamlit as st
|
import streamlit as st
|
||||||
|
import os
|
||||||
from ui_utils import build_markdown_doc
|
from ui_utils import build_markdown_doc
|
||||||
|
|
||||||
def switch_to_editor_callback(note_payload):
|
def switch_to_editor_callback(note_payload):
|
||||||
"""
|
"""
|
||||||
Callback für den 'Bearbeiten'-Button im Graphen.
|
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)
|
# 1. Pfad ermitteln (Priorität auf 'path' aus Qdrant)
|
||||||
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)
|
|
||||||
origin_fname = note_payload.get('path')
|
origin_fname = note_payload.get('path')
|
||||||
|
|
||||||
# Fallback: Falls 'path' leer ist (Legacy Daten)
|
# Fallback für Legacy-Datenfelder
|
||||||
if not origin_fname:
|
if not origin_fname:
|
||||||
origin_fname = note_payload.get('file_path') or note_payload.get('filename')
|
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:
|
if not origin_fname and 'note_id' in note_payload:
|
||||||
origin_fname = f"{note_payload['note_id']}.md"
|
origin_fname = f"{note_payload['note_id']}.md"
|
||||||
|
|
||||||
# 3. Message in den Chat-Verlauf injecten
|
# 4. Daten an den Editor übergeben
|
||||||
# WICHTIG: query_id muss mit 'edit_' beginnen, damit render_manual_editor sie erkennt!
|
# Wir nutzen den Chat-Verlauf als Transportmittel für den State
|
||||||
st.session_state.messages.append({
|
st.session_state.messages.append({
|
||||||
"role": "assistant",
|
"role": "assistant",
|
||||||
"intent": "INTERVIEW",
|
"intent": "INTERVIEW",
|
||||||
"content": content,
|
"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_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"
|
st.session_state["sidebar_mode_selection"] = "📝 Manueller Editor"
|
||||||
|
|
@ -3,46 +3,68 @@ from ui_api import send_chat_message, submit_feedback
|
||||||
from ui_editor import render_draft_editor
|
from ui_editor import render_draft_editor
|
||||||
|
|
||||||
def render_chat_interface(top_k, explain):
|
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):
|
for idx, msg in enumerate(st.session_state.messages):
|
||||||
with st.chat_message(msg["role"]):
|
with st.chat_message(msg["role"]):
|
||||||
if msg["role"] == "assistant":
|
if msg["role"] == "assistant":
|
||||||
|
# Intent Badge
|
||||||
intent = msg.get("intent", "UNKNOWN")
|
intent = msg.get("intent", "UNKNOWN")
|
||||||
st.markdown(f'<div class="intent-badge">Intent: {intent}</div>', unsafe_allow_html=True)
|
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):
|
with st.expander("🐞 Payload", expanded=False):
|
||||||
st.json(msg)
|
st.json(msg)
|
||||||
|
|
||||||
|
# Unterscheidung: Normaler Text oder Editor-Modus (Interview)
|
||||||
if intent == "INTERVIEW":
|
if intent == "INTERVIEW":
|
||||||
render_draft_editor(msg)
|
render_draft_editor(msg)
|
||||||
else:
|
else:
|
||||||
st.markdown(msg["content"])
|
st.markdown(msg["content"])
|
||||||
|
|
||||||
|
# Quellen anzeigen
|
||||||
if "sources" in msg and msg["sources"]:
|
if "sources" in msg and msg["sources"]:
|
||||||
for hit in 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]}..._")
|
st.markdown(f"_{hit.get('source', {}).get('text', '')[:300]}..._")
|
||||||
|
|
||||||
|
# Explanation Layer
|
||||||
if hit.get('explanation'):
|
if hit.get('explanation'):
|
||||||
st.caption(f"Grund: {hit['explanation']['reasons'][0]['message']}")
|
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')):
|
def _cb(qid=msg.get("query_id"), nid=hit.get('node_id')):
|
||||||
val = st.session_state.get(f"fb_src_{qid}_{nid}")
|
val = st.session_state.get(f"fb_src_{qid}_{nid}")
|
||||||
if val is not None: submit_feedback(qid, nid, val+1)
|
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)
|
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:
|
if "query_id" in msg:
|
||||||
qid = msg["query_id"]
|
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))
|
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:
|
else:
|
||||||
|
# User Nachricht
|
||||||
st.markdown(msg["content"])
|
st.markdown(msg["content"])
|
||||||
|
|
||||||
|
# 2. Input Feld
|
||||||
if prompt := st.chat_input("Frage Mindnet..."):
|
if prompt := st.chat_input("Frage Mindnet..."):
|
||||||
st.session_state.messages.append({"role": "user", "content": prompt})
|
st.session_state.messages.append({"role": "user", "content": prompt})
|
||||||
st.rerun()
|
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":
|
if len(st.session_state.messages) > 0 and st.session_state.messages[-1]["role"] == "user":
|
||||||
with st.chat_message("assistant"):
|
with st.chat_message("assistant"):
|
||||||
with st.spinner("Thinking..."):
|
with st.spinner("Thinking..."):
|
||||||
resp = send_chat_message(st.session_state.messages[-1]["content"], top_k, explain)
|
resp = send_chat_message(st.session_state.messages[-1]["content"], top_k, explain)
|
||||||
|
|
||||||
if "error" in resp:
|
if "error" in resp:
|
||||||
st.error(resp["error"])
|
st.error(resp["error"])
|
||||||
else:
|
else:
|
||||||
|
|
|
||||||
|
|
@ -75,8 +75,8 @@ def render_draft_editor(msg):
|
||||||
# Dateiname extrahieren für saubere Anzeige
|
# Dateiname extrahieren für saubere Anzeige
|
||||||
display_name = str(origin_fname).split("/")[-1]
|
display_name = str(origin_fname).split("/")[-1]
|
||||||
st.success(f"📂 **Update-Modus**: `{display_name}`")
|
st.success(f"📂 **Update-Modus**: `{display_name}`")
|
||||||
# Debugging: Zeige vollen Pfad im Tooltip oder klein darunter
|
# Debugging: Zeige vollen Pfad im Expander
|
||||||
with st.expander("Pfad-Details", expanded=False):
|
with st.expander("Dateipfad Details", expanded=False):
|
||||||
st.code(origin_fname)
|
st.code(origin_fname)
|
||||||
st.markdown(f'<div class="draft-box" style="border-left: 5px solid #ff9f43;">', unsafe_allow_html=True)
|
st.markdown(f'<div class="draft-box" style="border-left: 5px solid #ff9f43;">', unsafe_allow_html=True)
|
||||||
else:
|
else:
|
||||||
|
|
|
||||||
|
|
@ -7,11 +7,13 @@ from ui_callbacks import switch_to_editor_callback
|
||||||
def render_graph_explorer(graph_service):
|
def render_graph_explorer(graph_service):
|
||||||
st.header("🕸️ Graph Explorer")
|
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
|
# Defaults speichern für Persistenz
|
||||||
st.session_state.setdefault("graph_depth", 2)
|
st.session_state.setdefault("graph_depth", 2)
|
||||||
st.session_state.setdefault("graph_show_labels", True)
|
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_spacing", 150)
|
||||||
st.session_state.setdefault("graph_gravity", -3000)
|
st.session_state.setdefault("graph_gravity", -3000)
|
||||||
|
|
||||||
|
|
@ -19,8 +21,11 @@ def render_graph_explorer(graph_service):
|
||||||
|
|
||||||
with col_ctrl:
|
with col_ctrl:
|
||||||
st.subheader("Fokus")
|
st.subheader("Fokus")
|
||||||
|
|
||||||
|
# Suche
|
||||||
search_term = st.text_input("Suche Notiz", placeholder="Titel eingeben...")
|
search_term = st.text_input("Suche Notiz", placeholder="Titel eingeben...")
|
||||||
|
|
||||||
|
options = {}
|
||||||
if search_term:
|
if search_term:
|
||||||
hits, _ = graph_service.client.scroll(
|
hits, _ = graph_service.client.scroll(
|
||||||
collection_name=f"{COLLECTION_PREFIX}_notes",
|
collection_name=f"{COLLECTION_PREFIX}_notes",
|
||||||
|
|
@ -28,6 +33,7 @@ def render_graph_explorer(graph_service):
|
||||||
limit=10
|
limit=10
|
||||||
)
|
)
|
||||||
options = {h.payload['title']: h.payload['note_id'] for h in hits}
|
options = {h.payload['title']: h.payload['note_id'] for h in hits}
|
||||||
|
|
||||||
if options:
|
if options:
|
||||||
selected_title = st.selectbox("Ergebnisse:", list(options.keys()))
|
selected_title = st.selectbox("Ergebnisse:", list(options.keys()))
|
||||||
if st.button("Laden", use_container_width=True):
|
if st.button("Laden", use_container_width=True):
|
||||||
|
|
@ -35,13 +41,16 @@ def render_graph_explorer(graph_service):
|
||||||
st.rerun()
|
st.rerun()
|
||||||
|
|
||||||
st.divider()
|
st.divider()
|
||||||
|
|
||||||
|
# View Settings
|
||||||
with st.expander("👁️ Ansicht & Layout", expanded=True):
|
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_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.session_state.graph_show_labels = st.checkbox("Kanten-Beschriftung", st.session_state.graph_show_labels)
|
||||||
|
|
||||||
st.markdown("**Physik (BarnesHut)**")
|
st.markdown("**Physik (BarnesHut)**")
|
||||||
st.session_state.graph_spacing = st.slider("Federlänge", 50, 500, st.session_state.graph_spacing)
|
# ACHTUNG: BarnesHut reagiert anders. Spring Length ist wichtig.
|
||||||
st.session_state.graph_gravity = st.slider("Abstoßung", -20000, -500, st.session_state.graph_gravity)
|
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"):
|
if st.button("Reset Layout"):
|
||||||
st.session_state.graph_spacing = 150
|
st.session_state.graph_spacing = 150
|
||||||
|
|
@ -57,38 +66,53 @@ def render_graph_explorer(graph_service):
|
||||||
center_id = st.session_state.graph_center_id
|
center_id = st.session_state.graph_center_id
|
||||||
|
|
||||||
if center_id:
|
if center_id:
|
||||||
# Action Container oben
|
# Container für Action Bar OBERHALB des Graphen (Layout Fix)
|
||||||
action_container = st.container()
|
action_container = st.container()
|
||||||
|
|
||||||
|
# Graph Laden
|
||||||
with st.spinner(f"Lade Graph..."):
|
with st.spinner(f"Lade Graph..."):
|
||||||
|
# Daten laden (Cache wird genutzt)
|
||||||
nodes, edges = graph_service.get_ego_graph(
|
nodes, edges = graph_service.get_ego_graph(
|
||||||
center_id,
|
center_id,
|
||||||
depth=st.session_state.graph_depth,
|
depth=st.session_state.graph_depth,
|
||||||
show_labels=st.session_state.graph_show_labels
|
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)
|
note_data = graph_service._fetch_note_cached(center_id)
|
||||||
|
|
||||||
# Action Bar rendern
|
# --- ACTION BAR RENDEREN ---
|
||||||
with action_container:
|
with action_container:
|
||||||
c1, c2 = st.columns([3, 1])
|
c_act1, c_act2 = st.columns([3, 1])
|
||||||
with c1: st.caption(f"Aktives Zentrum: **{center_id}**")
|
with c_act1:
|
||||||
with c2:
|
st.caption(f"Aktives Zentrum: **{center_id}**")
|
||||||
|
with c_act2:
|
||||||
if note_data:
|
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:
|
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:
|
if note_data:
|
||||||
st.json(note_data)
|
st.json(note_data)
|
||||||
if 'path' in note_data: st.success(f"Pfad OK: {note_data['path']}")
|
if 'path' not in note_data:
|
||||||
else: st.error("Pfad fehlt!")
|
st.error("ACHTUNG: Feld 'path' fehlt im Qdrant-Payload! Update-Modus wird nicht funktionieren.")
|
||||||
else: st.info("Leer.")
|
else:
|
||||||
|
st.success(f"Pfad gefunden: {note_data['path']}")
|
||||||
|
else:
|
||||||
|
st.info("Keine Daten geladen.")
|
||||||
|
|
||||||
if not nodes:
|
if not nodes:
|
||||||
st.warning("Keine Daten gefunden.")
|
st.warning("Keine Daten gefunden.")
|
||||||
else:
|
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)
|
dyn_height = 800 + (abs(st.session_state.graph_gravity) % 5)
|
||||||
|
|
||||||
config = Config(
|
config = Config(
|
||||||
|
|
@ -97,6 +121,7 @@ def render_graph_explorer(graph_service):
|
||||||
directed=True,
|
directed=True,
|
||||||
physics={
|
physics={
|
||||||
"enabled": True,
|
"enabled": True,
|
||||||
|
# BarnesHut ist der Standard und stabilste Solver für Agraph
|
||||||
"solver": "barnesHut",
|
"solver": "barnesHut",
|
||||||
"barnesHut": {
|
"barnesHut": {
|
||||||
"gravitationalConstant": st.session_state.graph_gravity,
|
"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)
|
return_value = agraph(nodes=nodes, edges=edges, config=config)
|
||||||
|
|
||||||
|
# Interaktions-Logik
|
||||||
if return_value:
|
if return_value:
|
||||||
if return_value != center_id:
|
if return_value != center_id:
|
||||||
|
# Navigation: Neues Zentrum setzen
|
||||||
st.session_state.graph_center_id = return_value
|
st.session_state.graph_center_id = return_value
|
||||||
st.rerun()
|
st.rerun()
|
||||||
else:
|
else:
|
||||||
|
# Klick auf das Zentrum selbst
|
||||||
st.toast(f"Zentrum: {return_value}")
|
st.toast(f"Zentrum: {return_value}")
|
||||||
|
|
||||||
else:
|
else:
|
||||||
st.info("👈 Bitte wähle links eine Notiz aus.")
|
st.info("👈 Bitte wähle links eine Notiz aus.")
|
||||||
Loading…
Reference in New Issue
Block a user