From ccc848f2e2cad1c4b00d4276bef6064b059f062b Mon Sep 17 00:00:00 2001 From: Lars Date: Sun, 14 Dec 2025 11:54:20 +0100 Subject: [PATCH] bug fix --- app/frontend/ui_components.py | 117 +++++++++++++++++++++------------- 1 file changed, 73 insertions(+), 44 deletions(-) diff --git a/app/frontend/ui_components.py b/app/frontend/ui_components.py index 733c778..df585d7 100644 --- a/app/frontend/ui_components.py +++ b/app/frontend/ui_components.py @@ -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'
', 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'
', unsafe_allow_html=True) + else: + st.info("✨ Neuer Entwurf (Wird als neue Datei angelegt)") + st.markdown(f'
', 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("
", 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: