From b4ebbe5c287e21a0e4cbb59ad188946833ecc5ec Mon Sep 17 00:00:00 2001 From: Lars Date: Thu, 11 Dec 2025 12:56:16 +0100 Subject: [PATCH] WP10 ui mit text buffer --- app/frontend/ui.py | 160 +++++++++++++++++++++++---------------------- 1 file changed, 82 insertions(+), 78 deletions(-) diff --git a/app/frontend/ui.py b/app/frontend/ui.py index 88dbf80..20bb3f9 100644 --- a/app/frontend/ui.py +++ b/app/frontend/ui.py @@ -23,7 +23,7 @@ timeout_setting = os.getenv("MINDNET_API_TIMEOUT") or os.getenv("MINDNET_LLM_TIM API_TIMEOUT = float(timeout_setting) if timeout_setting else 300.0 # --- PAGE SETUP --- -st.set_page_config(page_title="mindnet v2.3.6", page_icon="🧠", layout="wide") +st.set_page_config(page_title="mindnet v2.3.7", page_icon="🧠", layout="wide") # --- CSS STYLING --- st.markdown(""" @@ -112,7 +112,6 @@ def normalize_meta_and_body(meta, body): def parse_markdown_draft(full_text): """Robustes Parsing + Sanitization.""" clean_text = full_text - pattern_block = r"```(?:markdown|md)?\s*(.*?)\s*```" match_block = re.search(pattern_block, full_text, re.DOTALL | re.IGNORECASE) if match_block: @@ -189,7 +188,6 @@ def send_chat_message(message: str, top_k: int, explain: bool): return {"error": str(e)} def analyze_draft_text(text: str, n_type: str): - """Ruft den neuen Intelligence-Service (WP-11) auf.""" try: response = requests.post( INGEST_ANALYZE_ENDPOINT, @@ -202,12 +200,11 @@ def analyze_draft_text(text: str, n_type: str): return {"error": str(e)} def save_draft_to_vault(markdown_content: str, filename: str = None): - """Ruft den neuen Persistence-Service (WP-11) auf.""" try: response = requests.post( INGEST_SAVE_ENDPOINT, json={"markdown_content": markdown_content, "filename": filename}, - timeout=60 # Indizierung kann dauern + timeout=60 ) response.raise_for_status() return response.json() @@ -225,7 +222,7 @@ def submit_feedback(query_id, node_id, score, comment=None): def render_sidebar(): with st.sidebar: st.title("🧠 mindnet") - st.caption("v2.3.6 | WP-10b (Full)") + st.caption("v2.3.7 | Stable State") mode = st.radio("Modus", ["💬 Chat", "📝 Manueller Editor"], index=0) st.divider() st.subheader("⚙️ Settings") @@ -242,96 +239,119 @@ def render_sidebar(): def render_draft_editor(msg): qid = msg.get('query_id', str(uuid.uuid4())) key_base = f"draft_{qid}" - body_key = f"{key_base}_txt_body" - # --- CALLBACKS (Lösung für den State-Error) --- - def _append_text(k, text): - current = st.session_state.get(k, "") - st.session_state[k] = f"{current}\n\n{text}" - # Sync auch den generischen Key - st.session_state[f"{key_base}_body"] = st.session_state[k] + # === STATE MANAGEMENT KEYS === + # Wir nutzen getrennte Keys für Widget und Daten, um Streamlit's Bereinigung zu umgehen. + # Persistent Keys (bleiben erhalten auch beim Tab-Wechsel) + data_body_key = f"{key_base}_data_body" + data_meta_key = f"{key_base}_data_meta" + data_sugg_key = f"{key_base}_data_suggestions" + + # Widget Keys (können sich ändern/neu gezeichnet werden) + widget_body_key = f"{key_base}_widget_body" - def _remove_text(k, text): - current = st.session_state.get(k, "") - # Einfaches Replace (könnte man robuster machen) - st.session_state[k] = current.replace(text, "").strip() - st.session_state[f"{key_base}_body"] = st.session_state[k] - - # 1. Init (Nur beim allerersten Laden) + # --- 1. INIT STATE (Einmalig pro Nachricht) --- if f"{key_base}_init" not in st.session_state: meta, body = parse_markdown_draft(msg["content"]) - st.session_state[f"{key_base}_type"] = meta.get("type", "default") - st.session_state[f"{key_base}_title"] = meta.get("title", "") - tags_raw = meta.get("tags", []) - st.session_state[f"{key_base}_tags"] = ", ".join(tags_raw) if isinstance(tags_raw, list) else str(tags_raw) + # Defaults setzen + if "type" not in meta: meta["type"] = "default" + if "title" not in meta: meta["title"] = "" + if "tags" not in meta: meta["tags"] = [] - # Initialisiere beide Keys - st.session_state[body_key] = body.strip() - st.session_state[f"{key_base}_body"] = body.strip() - - st.session_state[f"{key_base}_meta"] = meta - st.session_state[f"{key_base}_suggestions"] = [] + # Tags Listen-Check + if isinstance(meta["tags"], list): + meta["tags_str"] = ", ".join(meta["tags"]) + else: + meta["tags_str"] = str(meta.get("tags", "")) + + # Persistent speichern + st.session_state[data_meta_key] = meta + st.session_state[data_body_key] = body.strip() + st.session_state[data_sugg_key] = [] st.session_state[f"{key_base}_init"] = True - # 2. UI Layout + # --- HELPER CALLBACKS --- + # Sync Widget -> Data + def _sync_body(): + st.session_state[data_body_key] = st.session_state[widget_body_key] + + # Insert Text (Daten ändern) + def _insert_text(text_to_insert): + current = st.session_state[data_body_key] + st.session_state[data_body_key] = f"{current}\n\n{text_to_insert}" + + def _remove_text(text_to_remove): + current = st.session_state[data_body_key] + st.session_state[data_body_key] = current.replace(text_to_remove, "").strip() + + # --- 2. UI LAYOUT --- st.markdown(f'
', unsafe_allow_html=True) st.markdown("### 📝 Entwurf bearbeiten") - # Metadata + # Load Data Reference + meta_ref = st.session_state[data_meta_key] + + # Metadata Form c1, c2 = st.columns([2, 1]) with c1: - new_title = st.text_input("Titel", key=f"{key_base}_inp_title", value=st.session_state.get(f"{key_base}_title", "")) + new_title = st.text_input("Titel", key=f"{key_base}_wdg_title", value=meta_ref["title"]) + meta_ref["title"] = new_title # Direct Sync with c2: known_types = ["concept", "project", "decision", "experience", "journal", "person", "value", "goal", "principle", "default"] - curr_type = st.session_state.get(f"{key_base}_type", "default") + curr_type = meta_ref["type"] if curr_type not in known_types: known_types.append(curr_type) - new_type = st.selectbox("Typ", known_types, index=known_types.index(curr_type), key=f"{key_base}_sel_type") + new_type = st.selectbox("Typ", known_types, index=known_types.index(curr_type), key=f"{key_base}_wdg_type") + meta_ref["type"] = new_type # Direct Sync - new_tags = st.text_input("Tags (kommagetrennt)", key=f"{key_base}_inp_tags", value=st.session_state.get(f"{key_base}_tags", "")) + new_tags = st.text_input("Tags", key=f"{key_base}_wdg_tags", value=meta_ref.get("tags_str", "")) + meta_ref["tags_str"] = new_tags # Direct Sync # Tabs tab_edit, tab_intel, tab_view = st.tabs(["✏️ Inhalt", "🧠 Intelligence", "👁️ Vorschau"]) # --- TAB 1: EDITOR --- with tab_edit: - # Das Widget rendert HIER. Änderungen am State müssen VORHER (via Callback) passieren. - current_body = st.text_area( + # Hier ist der Trick: Value kommt aus 'data_body_key', + # Änderungen triggern '_sync_body', der zurück in 'data_body_key' schreibt. + st.text_area( "Body", - key=body_key, + key=widget_body_key, + value=st.session_state[data_body_key], + on_change=_sync_body, height=500, label_visibility="collapsed" ) - # Sync manueller Änderungen in den generischen Key - st.session_state[f"{key_base}_body"] = current_body # --- TAB 2: INTELLIGENCE --- with tab_intel: st.info("Klicke auf 'Analysieren', um Verknüpfungen für den AKTUELLEN Text zu finden.") if st.button("🔍 Analyse starten", key=f"{key_base}_analyze"): + # 1. Alte Ergebnisse löschen für Feedback + st.session_state[data_sugg_key] = [] + with st.spinner("Analysiere..."): - text_to_analyze = st.session_state[body_key] - analysis = analyze_draft_text(text_to_analyze, new_type) + # Aktuellen Text nehmen + text_to_analyze = st.session_state[data_body_key] + analysis = analyze_draft_text(text_to_analyze, meta_ref["type"]) if "error" in analysis: st.error(f"Fehler: {analysis['error']}") else: suggestions = analysis.get("suggestions", []) - st.session_state[f"{key_base}_suggestions"] = suggestions + st.session_state[data_sugg_key] = suggestions if not suggestions: st.warning("Keine Vorschläge gefunden.") + else: + st.success(f"{len(suggestions)} Vorschläge gefunden.") - suggestions = st.session_state.get(f"{key_base}_suggestions", []) + suggestions = st.session_state[data_sugg_key] if suggestions: - st.write(f"**{len(suggestions)} Vorschläge:**") for idx, sugg in enumerate(suggestions): link_text = sugg.get('suggested_markdown', '') + is_inserted = link_text in st.session_state[data_body_key] - # Prüfe ob Text vorhanden (Case Insensitive Check wäre besser, hier simpel) - is_inserted = link_text in st.session_state[body_key] - - # Card Styling card_style = "border-left: 3px solid #28a745;" if is_inserted else "border-left: 3px solid #1a73e8;" bg_color = "#e6fffa" if is_inserted else "#ffffff" @@ -343,35 +363,22 @@ def render_draft_editor(msg):
""", unsafe_allow_html=True) - # Button Logik mit CALLBACKS (on_click) if is_inserted: - st.button( - f"❌ Entfernen", - key=f"del_{idx}_{key_base}", - on_click=_remove_text, # Callback - args=(body_key, link_text) # Argumente für Callback - ) + st.button("❌ Entfernen", key=f"del_{idx}_{key_base}", on_click=_remove_text, args=(link_text,)) else: - st.button( - f"➕ Einfügen", - key=f"add_{idx}_{key_base}", - on_click=_append_text, # Callback - args=(body_key, link_text) # Argumente für Callback - ) + st.button("➕ Einfügen", key=f"add_{idx}_{key_base}", on_click=_insert_text, args=(link_text,)) # --- TAB 3: PREVIEW & SAVE --- - final_tags_list = [t.strip() for t in new_tags.split(",") if t.strip()] + # Final Assembly + final_tags_list = [t.strip() for t in meta_ref["tags_str"].split(",") if t.strip()] final_meta = { "id": "generated_on_save", - "type": new_type, - "title": new_title, + "type": meta_ref["type"], + "title": meta_ref["title"], "status": "draft", "tags": final_tags_list } - - # Nimm immer den aktuellsten Text aus dem Widget-State - final_body_content = st.session_state[body_key] - final_doc = build_markdown_doc(final_meta, final_body_content) + final_doc = build_markdown_doc(final_meta, st.session_state[data_body_key]) with tab_view: st.markdown('
', unsafe_allow_html=True) @@ -380,18 +387,15 @@ def render_draft_editor(msg): st.markdown("---") - # Save Action b1, b2 = st.columns([1, 1]) with b1: if st.button("💾 Speichern & Indizieren", type="primary", key=f"{key_base}_save"): with st.spinner("Speichere im Vault..."): - safe_title = re.sub(r'[^a-zA-Z0-9]', '-', new_title).lower()[:30] + safe_title = re.sub(r'[^a-zA-Z0-9]', '-', meta_ref["title"]).lower()[:30] if not safe_title: safe_title = "draft" fname = f"{datetime.now().strftime('%Y%m%d')}-{safe_title}.md" - # Speichern mit aktuellstem Inhalt result = save_draft_to_vault(final_doc, filename=fname) - if "error" in result: st.error(f"Fehler: {result['error']}") else: @@ -463,11 +467,11 @@ def render_chat_interface(top_k, explain): st.rerun() def render_manual_editor(): - # Wir nutzen eine Fake-Message, um die render_draft_editor Logik wiederzuverwenden - # Aber mit leeren Defaults + # Wir nutzen dieselbe Logik wie beim Interview, aber mit einem "leeren" Mock-Objekt + # Wichtig: Feste Query-ID für Manuellen Modus, damit der State persistent bleibt mock_msg = { - "content": "---\ntype: default\nstatus: draft\ntitle: Neue Notiz\ntags: []\n---\n# Titel\n", - "query_id": "manual_mode_v2" # Feste ID für manuellen Modus + "content": "---\ntype: concept\ntitle: Neue Notiz\nstatus: draft\ntags: []\n---\n# Titel\n", + "query_id": "manual_editor_fixed_v1" } render_draft_editor(mock_msg)