This commit is contained in:
Lars 2025-12-14 11:54:20 +01:00
parent c6ccad1d18
commit ccc848f2e2

View File

@ -10,30 +10,33 @@ from ui_api import save_draft_to_vault, analyze_draft_text, send_chat_message, s
from ui_config import HISTORY_FILE, COLLECTION_PREFIX, GRAPH_COLORS
# --- CALLBACKS ---
# Diese müssen oben definiert sein, damit sie VOR dem Re-Run bekannt sind.
def switch_to_editor_callback(note_payload):
"""
Callback-Funktion: Wird ausgeführt, wenn der 'Bearbeiten'-Button geklickt wird.
Da dies ein Callback ist, können wir session_state Werte ändern, bevor die UI neu gezeichnet wird.
Lädt eine Note in den Editor.
Versucht, den Original-Dateinamen zu erraten oder zu finden, um Duplikate zu vermeiden.
"""
# 1. Inhalt vorbereiten
# 1. Inhalt holen
content = note_payload.get('fulltext', '')
if not content:
# Fallback: Markdown aus Metadaten rekonstruieren, falls kein Fulltext da ist
content = build_markdown_doc(note_payload, "Inhalt konnte nicht geladen werden (nur Metadaten verfügbar).")
# 2. Nachricht simulieren (als ob der Chatbot sie generiert hätte)
# Dies füllt den Editor mit dem Inhalt der Notiz
# 2. Dateinamen-Heuristik (Single Source of Truth)
# Idealfall: Qdrant hat das Feld 'file_path' oder 'filename' gespeichert.
# Fallback: Wir nutzen die note_id oder den Titel, müssen aber beim Speichern aufpassen.
origin_fname = note_payload.get('file_path') or note_payload.get('filename')
# Nachricht simulieren, die Daten in den Editor trägt
st.session_state.messages.append({
"role": "assistant",
"intent": "INTERVIEW",
"content": content,
"query_id": f"edit_{note_payload['note_id']}"
"query_id": f"edit_{note_payload['note_id']}",
"origin_filename": origin_fname, # WICHTIG: Pfad mitschleifen
"origin_note_id": note_payload['note_id'] # ID für Fallback mitschleifen
})
# 3. Modus umschalten
# Das ist der entscheidende Fix: Wir ändern den Wert des Radio-Buttons im State direkt.
st.session_state["sidebar_mode_selection"] = "📝 Manueller Editor"
# --- UI RENDERER ---
@ -44,7 +47,6 @@ def render_sidebar():
st.caption("v2.6 | WP-19 Graph View")
# State-gebundenes Radio Widget
# Wir nutzen 'sidebar_mode_selection' als Key, damit wir ihn programmgesteuert (Callback) ändern können.
if "sidebar_mode_selection" not in st.session_state:
st.session_state["sidebar_mode_selection"] = "💬 Chat"
@ -69,7 +71,7 @@ def render_sidebar():
def render_draft_editor(msg):
"""
Der Editor-Kern. Wird sowohl im Chat (Interview-Modus) als auch im manuellen Modus verwendet.
Smart Editor: Unterscheidet zwischen 'Neu' und 'Update'.
"""
if "query_id" not in msg or not msg["query_id"]:
msg["query_id"] = str(uuid.uuid4())
@ -77,7 +79,7 @@ def render_draft_editor(msg):
qid = msg["query_id"]
key_base = f"draft_{qid}"
# State Keys für Persistenz
# State Keys
data_meta_key = f"{key_base}_data_meta"
data_sugg_key = f"{key_base}_data_suggestions"
widget_body_key = f"{key_base}_widget_body"
@ -85,27 +87,40 @@ def render_draft_editor(msg):
# --- INIT STATE ---
if f"{key_base}_init" not in st.session_state:
# Metadaten parsen
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)
# Daten in Session State laden
st.session_state[data_meta_key] = meta
st.session_state[data_sugg_key] = []
st.session_state[data_body_key] = body.strip()
# Widget States initialisieren
# Widget States
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"]
# --- EDITOR LOGIK: Origin Filename ---
# Wir speichern den Original-Namen im State, um beim Speichern zu wissen, ob wir überschreiben müssen.
origin_file = msg.get("origin_filename")
if not origin_file and "origin_note_id" in msg:
# Fallback: Wenn wir keinen Pfad haben, aber eine ID, merken wir uns diese,
# um später ggf. intelligent zu speichern (z.B. {id}.md suchen)
# Hier vereinfacht: Wir setzen es erstmal auf None, User muss aufpassen.
pass
st.session_state[f"{key_base}_origin_filename"] = origin_file
st.session_state[f"{key_base}_init"] = True
# --- STATE RESURRECTION ---
# --- RESURRECTION ---
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]
# --- CALLBACKS ---
# --- SYNC FUNCTIONS ---
def _sync_meta():
meta = st.session_state[data_meta_key]
meta["title"] = st.session_state.get(f"{key_base}_wdg_title", "")
@ -129,11 +144,19 @@ def render_draft_editor(msg):
st.session_state[data_body_key] = new_text
# --- UI LAYOUT ---
st.markdown(f'<div class="draft-box">', unsafe_allow_html=True)
st.markdown("### 📝 Entwurf bearbeiten")
# Header: Status anzeigen
origin_fname = st.session_state.get(f"{key_base}_origin_filename")
if origin_fname:
st.info(f"📝 Bearbeitungs-Modus: Du editierst **{origin_fname}**")
st.markdown(f'<div class="draft-box" style="border-left: 5px solid #ff9f43;">', unsafe_allow_html=True)
else:
st.info("✨ Neuer Entwurf (Wird als neue Datei angelegt)")
st.markdown(f'<div class="draft-box">', unsafe_allow_html=True)
st.markdown("### Editor")
meta_ref = st.session_state[data_meta_key]
c1, c2 = st.columns([2, 1])
with c1:
st.text_input("Titel", key=f"{key_base}_wdg_title", on_change=_sync_meta)
@ -174,7 +197,6 @@ def render_draft_editor(msg):
for idx, sugg in enumerate(suggestions):
link_text = sugg.get('suggested_markdown', '')
is_inserted = link_text in current_text_state
bg_color = "#e6fffa" if is_inserted else "#ffffff"
border = "3px solid #28a745" if is_inserted else "3px solid #1a73e8"
@ -201,6 +223,11 @@ def render_draft_editor(msg):
"status": "draft",
"tags": final_tags
}
# ID wiederherstellen, falls vorhanden
if "origin_note_id" in msg:
final_meta["id"] = msg["origin_note_id"]
final_body = st.session_state.get(widget_body_key, st.session_state[data_body_key])
if not final_meta["title"]:
@ -218,20 +245,33 @@ def render_draft_editor(msg):
b1, b2 = st.columns([1, 1])
with b1:
if st.button("💾 Speichern & Indizieren", type="primary", key=f"{key_base}_save"):
# Button Text dynamisch machen
btn_label = "💾 Update speichern" if origin_fname else "💾 Neu anlegen & Indizieren"
if st.button(btn_label, type="primary", key=f"{key_base}_save"):
with st.spinner("Speichere im Vault..."):
raw_title = final_meta.get("title", "")
if not raw_title:
clean_body = re.sub(r"[#*_\[\]()]", "", final_body).strip()
raw_title = clean_body[:40] if clean_body else "draft"
safe_title = slugify(raw_title)[:60] or "draft"
fname = f"{datetime.now().strftime('%Y%m%d')}-{safe_title}.md"
result = save_draft_to_vault(final_doc, filename=fname)
if "error" in result: st.error(f"Fehler: {result['error']}")
# ENTSCHEIDUNG: Update oder Neu?
if origin_fname:
# UPDATE: Wir nutzen den existierenden Dateinamen
target_filename = origin_fname
else:
# NEU: Wir generieren einen Namen
raw_title = final_meta.get("title", "")
if not raw_title:
clean_body = re.sub(r"[#*_\[\]()]", "", final_body).strip()
raw_title = clean_body[:40] if clean_body else "draft"
safe_title = slugify(raw_title)[:60] or "draft"
target_filename = f"{datetime.now().strftime('%Y%m%d')}-{safe_title}.md"
result = save_draft_to_vault(final_doc, filename=target_filename)
if "error" in result:
st.error(f"Fehler: {result['error']}")
else:
st.success(f"Gespeichert: {result.get('file_path')}")
st.balloons()
with b2:
if st.button("📋 Code anzeigen", key=f"{key_base}_btn_copy"):
st.code(final_doc, language="markdown")
@ -239,9 +279,6 @@ def render_draft_editor(msg):
st.markdown("</div>", unsafe_allow_html=True)
def render_chat_interface(top_k, explain):
"""
Rendert den Chat-Verlauf und das Eingabefeld.
"""
for idx, msg in enumerate(st.session_state.messages):
with st.chat_message(msg["role"]):
if msg["role"] == "assistant":
@ -298,7 +335,6 @@ def render_chat_interface(top_k, explain):
st.rerun()
def render_manual_editor():
"""Rendert einen leeren Editor für manuelle Eingaben."""
mock_msg = {
"content": "---\ntype: concept\ntitle: Neue Notiz\nstatus: draft\ntags: []\n---\n# Titel\n",
"query_id": "manual_mode_v2"
@ -310,10 +346,8 @@ def render_manual_editor():
def render_graph_explorer(graph_service):
st.header("🕸️ Graph Explorer")
# State Init
if "graph_center_id" not in st.session_state: st.session_state.graph_center_id = None
# Defaults speichern für Persistenz während der Session
st.session_state.setdefault("graph_depth", 2)
st.session_state.setdefault("graph_show_labels", True)
st.session_state.setdefault("graph_spacing", 200)
@ -324,7 +358,6 @@ def render_graph_explorer(graph_service):
with col_ctrl:
st.subheader("Fokus")
# 1. Suchfeld
search_term = st.text_input("Suche Notiz", placeholder="Titel eingeben...")
options = {}
@ -344,7 +377,6 @@ def render_graph_explorer(graph_service):
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)
@ -367,12 +399,11 @@ def render_graph_explorer(graph_service):
center_id = st.session_state.graph_center_id
if center_id:
# Action Bar
c_action1, c_action2 = st.columns([3, 1])
with c_action1:
st.caption(f"Aktives Zentrum: **{center_id}**")
with c_action2:
# FIX: Button mit Callback (on_click)
# Button mit Callback (on_click)
note_data = graph_service._fetch_note_cached(center_id)
if note_data:
st.button("📝 Bearbeiten",
@ -390,11 +421,10 @@ def render_graph_explorer(graph_service):
)
if not nodes:
st.warning("Keine Daten gefunden. (Notiz existiert evtl. nicht mehr)")
st.warning("Keine Daten gefunden.")
else:
# FIX: Dynamischer Key erzwingt Neu-Rendern bei Physics-Änderung
graph_key = f"graph_{center_id}_{st.session_state.graph_gravity}_{st.session_state.graph_spacing}"
# FIX: Key entfernt, da er in deiner Version TypeError verursacht.
# Wir verlassen uns auf Config-Update.
config = Config(
width=1000,
height=800,
@ -404,7 +434,6 @@ def render_graph_explorer(graph_service):
nodeHighlightBehavior=True,
highlightColor="#F7A7A6",
collapsible=False,
# Solver Wechsel: ForceAtlas2Based
solver="forceAtlas2Based",
forceAtlas2Based={
"theta": 0.5,
@ -418,7 +447,7 @@ def render_graph_explorer(graph_service):
stabilization={"enabled": True, "iterations": 800}
)
return_value = agraph(nodes=nodes, edges=edges, config=config, key=graph_key)
return_value = agraph(nodes=nodes, edges=edges, config=config)
if return_value:
if return_value != center_id: