""" FILE: app/frontend/ui_editor.py DESCRIPTION: Markdown-Editor mit Live-Vorschau. Refactored für WP-14: Asynchrones Feedback-Handling (Queued State). VERSION: 2.7.0 (Fix: Async Save UI) STATUS: Active DEPENDENCIES: streamlit, uuid, re, datetime, ui_utils, ui_api """ import streamlit as st import uuid import re from datetime import datetime from ui_utils import parse_markdown_draft, build_markdown_doc, slugify from ui_api import save_draft_to_vault, analyze_draft_text def render_draft_editor(msg): """ Rendert den Markdown-Editor. Nutzt 'origin_filename' aus der Message, um zwischen Update und Neu zu unterscheiden. """ if "query_id" not in msg or not msg["query_id"]: msg["query_id"] = str(uuid.uuid4()) qid = msg["query_id"] key_base = f"draft_{qid}" # 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" data_body_key = f"{key_base}_data_body" # --- INIT STATE --- if f"{key_base}_init" not in st.session_state: 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) st.session_state[data_meta_key] = meta st.session_state[data_sugg_key] = [] st.session_state[data_body_key] = body.strip() 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"] # Pfad übernehmen (Source of Truth) st.session_state[f"{key_base}_origin_filename"] = msg.get("origin_filename") st.session_state[f"{key_base}_init"] = True # --- 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] # --- SYNC HELPER --- def _sync_meta(): meta = st.session_state[data_meta_key] meta["title"] = st.session_state.get(f"{key_base}_wdg_title", "") meta["type"] = st.session_state.get(f"{key_base}_wdg_type", "default") meta["tags_str"] = st.session_state.get(f"{key_base}_wdg_tags", "") st.session_state[data_meta_key] = meta def _sync_body(): st.session_state[data_body_key] = st.session_state[widget_body_key] def _insert_text(t): st.session_state[widget_body_key] = f"{st.session_state.get(widget_body_key, '')}\n\n{t}" st.session_state[data_body_key] = st.session_state[widget_body_key] def _remove_text(t): st.session_state[widget_body_key] = st.session_state.get(widget_body_key, '').replace(t, "").strip() st.session_state[data_body_key] = st.session_state[widget_body_key] # --- UI LAYOUT --- origin_fname = st.session_state.get(f"{key_base}_origin_filename") if origin_fname: display_name = str(origin_fname).split("/")[-1] st.success(f"📂 **Update-Modus**: `{display_name}`") with st.expander("Dateipfad Details", expanded=False): st.code(origin_fname) st.markdown(f'
', unsafe_allow_html=True) else: st.info("✨ **Erstell-Modus**: Neue Datei wird angelegt.") st.markdown(f'
', unsafe_allow_html=True) st.markdown("### Editor") # Meta Felder 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) with c2: known_types = ["concept", "project", "decision", "experience", "journal", "value", "goal", "principle", "risk", "belief"] curr_type = st.session_state.get(f"{key_base}_wdg_type", meta_ref["type"]) if curr_type not in known_types: known_types.append(curr_type) st.selectbox("Typ", known_types, key=f"{key_base}_wdg_type", on_change=_sync_meta) st.text_input("Tags", key=f"{key_base}_wdg_tags", on_change=_sync_meta) # Tabs tab_edit, tab_intel, tab_view = st.tabs(["✏️ Inhalt", "🧠 Intelligence", "👁️ Vorschau"]) with tab_edit: st.text_area("Body", key=widget_body_key, height=600, on_change=_sync_body, label_visibility="collapsed") with tab_intel: st.info("Analysiert den Text auf Verknüpfungsmöglichkeiten.") if st.button("🔍 Analyse starten", key=f"{key_base}_analyze"): st.session_state[data_sugg_key] = [] text_to_analyze = st.session_state.get(widget_body_key, st.session_state.get(data_body_key, "")) with st.spinner("Analysiere..."): analysis = analyze_draft_text(text_to_analyze, st.session_state.get(f"{key_base}_wdg_type", "concept")) if "error" in analysis: st.error(f"Fehler: {analysis['error']}") else: suggestions = analysis.get("suggestions", []) st.session_state[data_sugg_key] = suggestions if not suggestions: st.warning("Keine Vorschläge.") else: st.success(f"{len(suggestions)} Vorschläge gefunden.") suggestions = st.session_state[data_sugg_key] if suggestions: current_text = st.session_state.get(widget_body_key, "") for idx, sugg in enumerate(suggestions): link_text = sugg.get('suggested_markdown', '') is_inserted = link_text in current_text bg_color = "#e6fffa" if is_inserted else "#ffffff" border = "3px solid #28a745" if is_inserted else "3px solid #1a73e8" st.markdown(f"
{sugg.get('target_title')} ({sugg.get('type')})
{sugg.get('reason')}
{link_text}
", unsafe_allow_html=True) if is_inserted: st.button("❌ Entfernen", key=f"del_{idx}_{key_base}", on_click=_remove_text, args=(link_text,)) else: st.button("➕ Einfügen", key=f"add_{idx}_{key_base}", on_click=_insert_text, args=(link_text,)) # Save Logic Preparation final_tags = [t.strip() for t in st.session_state.get(f"{key_base}_wdg_tags", "").split(",") if t.strip()] final_meta = { "id": "generated_on_save", "type": st.session_state.get(f"{key_base}_wdg_type", "default"), "title": st.session_state.get(f"{key_base}_wdg_title", "").strip(), "status": "draft", "tags": final_tags } 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"]: h1_match = re.search(r"^#\s+(.*)$", final_body, re.MULTILINE) if h1_match: final_meta["title"] = h1_match.group(1).strip() final_doc = build_markdown_doc(final_meta, final_body) with tab_view: st.markdown('
', unsafe_allow_html=True) st.markdown(final_doc) st.markdown('
', unsafe_allow_html=True) st.markdown("---") # Save Actions b1, b2 = st.columns([1, 1]) with b1: save_label = "💾 Update speichern" if origin_fname else "💾 Neu anlegen & Indizieren" if st.button(save_label, type="primary", key=f"{key_base}_save"): with st.spinner("Sende an Backend..."): if origin_fname: target_file = origin_fname else: raw_title = final_meta.get("title", "draft") target_file = f"{datetime.now().strftime('%Y%m%d')}-{slugify(raw_title)[:60]}.md" result = save_draft_to_vault(final_doc, filename=target_file) # --- WP-14 CHANGE START: Handling Async Response --- if "error" in result: st.error(f"Fehler: {result['error']}") else: status = result.get("status", "success") file_path = result.get("file_path", "unbekannt") if status == "queued": # Neuer Status für Async Processing st.info(f"✅ **Eingereiht:** Datei `{file_path}` wurde gespeichert.") st.caption("Die KI-Analyse und Indizierung läuft im Hintergrund. Du kannst weiterarbeiten.") else: # Legacy / Synchroner Fall st.success(f"Gespeichert: {file_path}") st.balloons() # --- WP-14 CHANGE END --- with b2: if st.button("📋 Code anzeigen", key=f"{key_base}_btn_copy"): st.code(final_doc, language="markdown") st.markdown("
", unsafe_allow_html=True) def render_manual_editor(): """ Rendert den manuellen Editor. """ target_msg = None if st.session_state.messages: last_msg = st.session_state.messages[-1] qid = str(last_msg.get("query_id", "")) if qid.startswith("edit_"): target_msg = last_msg if not target_msg: target_msg = { "content": "---\ntype: concept\ntitle: Neue Notiz\nstatus: draft\ntags: []\n---\n# Titel\n", "query_id": f"manual_{uuid.uuid4()}" } render_draft_editor(target_msg)