From 1fefc538ac2a38a41a7bd13b540ead4a869b69b9 Mon Sep 17 00:00:00 2001 From: Lars Date: Wed, 10 Dec 2025 17:14:51 +0100 Subject: [PATCH] UI Work --- app/frontend/ui.py | 218 ++++++++++++++++++++++++--------------------- 1 file changed, 117 insertions(+), 101 deletions(-) diff --git a/app/frontend/ui.py b/app/frontend/ui.py index 882a674..b3f52a8 100644 --- a/app/frontend/ui.py +++ b/app/frontend/ui.py @@ -5,6 +5,7 @@ import os import json import re import yaml +from datetime import datetime from pathlib import Path from dotenv import load_dotenv @@ -25,10 +26,8 @@ st.set_page_config(page_title="mindnet v2.3.2", page_icon="🧠", layout="wide") # --- CSS STYLING --- st.markdown(""" """, unsafe_allow_html=True) @@ -62,15 +64,19 @@ if "user_id" not in st.session_state: st.session_state.user_id = str(uuid.uuid4( def parse_markdown_draft(full_text): """ - Zerlegt einen Markdown-Text in Frontmatter (Dict) und Body (String). + Zerlegt die LLM-Antwort in Frontmatter (Dict) und Body (String). + Robust gegen Einleitungstexte vor dem Codeblock. """ - # 1. Versuch: Markdown Codeblock entfernen + # 1. Extrahiere Codeblock pattern_block = r"```markdown\s*(.*?)\s*```" match_block = re.search(pattern_block, full_text, re.DOTALL) + + # Fallback: Wenn kein Codeblock, nimm ganzen Text clean_text = match_block.group(1).strip() if match_block else full_text - # 2. Frontmatter parsen (YAML zwischen ---) - pattern_fm = r"^---\s+(.*?)\s+---\s+(.*)$" + # 2. Trenne Frontmatter (YAML) vom Body + # Sucht nach --- am Anfang, gefolgt von YAML, gefolgt von --- + pattern_fm = r"^---\s+(.*?)\s+---\s*(.*)$" match_fm = re.search(pattern_fm, clean_text, re.DOTALL) if match_fm: @@ -78,15 +84,30 @@ def parse_markdown_draft(full_text): body = match_fm.group(2) try: meta = yaml.safe_load(yaml_str) or {} - except: - meta = {} + except Exception: + meta = {} # Fallback bei kaputtem YAML return meta, body else: + # Kein Frontmatter gefunden -> Alles ist Body return {}, clean_text -def build_markdown_draft(meta, body): - """Baut das Dokument aus Metadaten und Text wieder zusammen.""" - yaml_str = yaml.dump(meta, default_flow_style=None, sort_keys=False).strip() +def generate_filename(meta): + """Generiert einen Dateinamen basierend auf Typ und Datum.""" + date_str = datetime.now().strftime("%Y%m%d") + n_type = meta.get("type", "note") + # Versuche Titel aus Body zu raten wäre zu komplex, wir nehmen Generic + return f"{date_str}-{n_type}-draft.md" + +def build_markdown_doc(meta, body): + """Baut das finale Dokument zusammen.""" + # ID Generierung beim 'Speichern' (hier simuliert für Download) + if "id" not in meta or meta["id"] == "generated_on_save": + meta["id"] = f"{datetime.now().strftime('%Y%m%d')}-{meta.get('type', 'note')}-{uuid.uuid4().hex[:6]}" + + # Updated timestamp + meta["updated"] = datetime.now().strftime("%Y-%m-%d") + + yaml_str = yaml.dump(meta, default_flow_style=None, sort_keys=False, allow_unicode=True).strip() return f"---\n{yaml_str}\n---\n\n{body}" def load_history_from_logs(limit=10): @@ -103,8 +124,7 @@ def load_history_from_logs(limit=10): queries.append(q) if len(queries) >= limit: break except: continue - except Exception as e: - st.sidebar.warning(f"Log-Fehler: {e}") + except: pass return queries def send_chat_message(message: str, top_k: int, explain: bool): @@ -123,7 +143,7 @@ def submit_feedback(query_id, node_id, score, comment=None): try: requests.post(FEEDBACK_ENDPOINT, json={"query_id": query_id, "node_id": node_id, "score": score, "comment": comment}, timeout=2) target = "Antwort" if node_id == "generated_answer" else "Quelle" - st.toast(f"Feedback für {target} gespeichert! (Score: {score})") + st.toast(f"Feedback ({score}) gesendet!") except: pass # --- UI COMPONENTS --- @@ -133,7 +153,7 @@ def render_sidebar(): st.title("🧠 mindnet") st.caption("v2.3.2 | WP-10 UI") - mode = st.radio("Modus", ["💬 Chat", "📝 Manueller Eintrag"], index=0) + mode = st.radio("Modus", ["💬 Chat", "📝 Manueller Editor"], index=0) st.divider() st.subheader("⚙️ Settings") @@ -142,69 +162,75 @@ def render_sidebar(): st.divider() st.subheader("🕒 Verlauf") - history = load_history_from_logs(8) - if not history: - st.caption("Noch keine Einträge.") - for q in history: - if st.button(f"🔎 {q[:30]}...", key=f"hist_{q}", help=q, use_container_width=True): + for q in load_history_from_logs(8): + if st.button(f"🔎 {q[:25]}...", key=f"hist_{q}", use_container_width=True): st.session_state.messages.append({"role": "user", "content": q}) st.rerun() - return mode, top_k, explain def render_draft_editor(msg): """ - Spezial-Widget für den INTERVIEW Intent. - Zeigt Tabs für Edit/Preview und separiert Metadaten. + Rendert den Split-Screen Editor für INTERVIEW Drafts. """ qid = msg.get('query_id', str(uuid.uuid4())) key_base = f"draft_{qid}" - # 1. Parsing (Initialisierung) - if f"{key_base}_initialized" not in st.session_state: + # 1. State Initialisierung (Nur einmal 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", "concept") - st.session_state[f"{key_base}_tags"] = meta.get("tags", []) + st.session_state[f"{key_base}_type"] = meta.get("type", "default") + + # Tags Behandlung (Liste oder String) + tags_raw = meta.get("tags", []) + if isinstance(tags_raw, list): + st.session_state[f"{key_base}_tags"] = ", ".join(tags_raw) + else: + st.session_state[f"{key_base}_tags"] = str(tags_raw) + st.session_state[f"{key_base}_body"] = body.strip() - st.session_state[f"{key_base}_initialized"] = True + st.session_state[f"{key_base}_init"] = True - # 2. Container Style + # 2. Editor Container st.markdown(f'
', unsafe_allow_html=True) st.markdown("### 📝 Entwurf bearbeiten") - - # 3. Metadaten-Controls (Oberer Bereich) + st.caption("Das System hat diesen Entwurf vorbereitet. Ergänze die fehlenden [TODO] Bereiche.") + + # 3. Metadaten (Grid Layout) c1, c2 = st.columns([1, 2]) with c1: - # Typ-Auswahl (Liste synchron mit types.yaml) - valid_types = ["concept", "project", "decision", "experience", "journal", "person", "value", "goal"] - # Fallback falls LLM etwas Exotisches erfunden hat - current_type = st.session_state[f"{key_base}_type"] - if current_type not in valid_types: valid_types.append(current_type) + # Typen müssen mit types.yaml synchron sein + known_types = ["concept", "project", "decision", "experience", "journal", "person", "value", "goal", "principle"] + curr_type = st.session_state[f"{key_base}_type"] + if curr_type not in known_types: known_types.append(curr_type) - new_type = st.selectbox("Typ", valid_types, key=f"{key_base}_sel_type", index=valid_types.index(current_type)) + new_type = st.selectbox("Typ", known_types, index=known_types.index(curr_type), key=f"{key_base}_sel_type") with c2: - # Tag-Editor (als Chips) - current_tags = st.session_state[f"{key_base}_tags"] - if isinstance(current_tags, str): current_tags = [current_tags] # Safety catch - new_tags = st.text_input("Tags (Kommagetrennt)", value=", ".join(current_tags), key=f"{key_base}_inp_tags") + new_tags = st.text_input("Tags (kommagetrennt)", value=st.session_state[f"{key_base}_tags"], key=f"{key_base}_inp_tags") - # 4. Tabs: Edit vs Preview - tab_edit, tab_view = st.tabs(["✏️ Bearbeiten", "👁️ Vorschau"]) + # 4. Inhalt (Tabs) + tab_edit, tab_view = st.tabs(["✏️ Editor", "👁️ Vorschau"]) with tab_edit: + # Hier landet der Body mit den ## Headings und [TODO]s new_body = st.text_area( "Inhalt", value=st.session_state[f"{key_base}_body"], - height=400, + height=500, key=f"{key_base}_txt_body", - label_visibility="collapsed" + label_visibility="collapsed", + help="Hier kannst du Markdown schreiben." ) - # 5. Rekonstruktion des Dokuments - final_tags = [t.strip() for t in new_tags.split(",") if t.strip()] - final_meta = {"type": new_type, "tags": final_tags, "status": "draft"} # Basic meta - final_doc = build_markdown_draft(final_meta, new_body) + # Live-Zusammenbau für Vorschau & Download + final_tags_list = [t.strip() for t in new_tags.split(",") if t.strip()] + final_meta = { + "id": "generated_on_save", # Placeholder, wird beim Download ersetzt + "type": new_type, + "status": "draft", + "tags": final_tags_list + } + final_doc = build_markdown_doc(final_meta, new_body) with tab_view: st.markdown('
', unsafe_allow_html=True) @@ -213,25 +239,25 @@ def render_draft_editor(msg): st.markdown("---") - # 6. Actions - b1, b2, b3 = st.columns([1, 1, 2]) + # 5. Actions (Writeback Simulation) + b1, b2 = st.columns([1, 1]) with b1: + # Download Button (Client-Side) st.download_button( - "💾 Download .md", - data=final_doc, - file_name=f"draft_{new_type}_{qid[:6]}.md", + label="💾 Als .md Datei speichern", + data=final_doc, + file_name=generate_filename(final_meta), mime="text/markdown" ) with b2: - if st.button("📋 Copy Code", key=f"{key_base}_btn_copy"): + # Copy Button Logic + if st.button("📋 Code anzeigen (Copy)", key=f"{key_base}_btn_copy"): st.code(final_doc, language="markdown") - st.toast("Code unten ausgeklappt zum Kopieren!") st.markdown("
", unsafe_allow_html=True) def render_chat_interface(top_k, explain): - # Render History for msg in st.session_state.messages: with st.chat_message(msg["role"]): if msg["role"] == "assistant": @@ -239,16 +265,16 @@ def render_chat_interface(top_k, explain): if "intent" in msg: intent = msg["intent"] icon = {"EMPATHY": "❤️", "DECISION": "⚖️", "CODING": "💻", "FACT": "📚", "INTERVIEW": "📝"}.get(intent, "🧠") - source_info = msg.get("intent_source", "Unknown") - st.markdown(f'
{icon} Intent: {intent} via {source_info}
', unsafe_allow_html=True) + src = msg.get("intent_source", "?") + st.markdown(f'
{icon} Intent: {intent} ({src})
', unsafe_allow_html=True) - # WEICHE: Editor vs. Text + # INTERVIEW Logic vs NORMAL Chat if msg.get("intent") == "INTERVIEW": render_draft_editor(msg) else: st.markdown(msg["content"]) - # Sources & Feedback (nur bei non-interview meist sinnvoll, oder immer?) + # Sources & Feedback (nur bei RAG sinnvoll) if "sources" in msg and msg["sources"]: for hit in msg["sources"]: score = hit.get('total_score', 0) @@ -260,67 +286,57 @@ def render_chat_interface(top_k, explain): def _cb(qid=msg["query_id"], nid=hit['node_id']): val = st.session_state.get(f"fb_src_{qid}_{nid}") - if val is not None: submit_feedback(qid, nid, val+1, "Faces UI") - + if val is not None: submit_feedback(qid, nid, val+1) st.feedback("faces", key=f"fb_src_{msg['query_id']}_{hit['node_id']}", on_change=_cb) if "query_id" in msg: 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)) - else: st.markdown(msg["content"]) - # Input Logic - last_msg_is_user = len(st.session_state.messages) > 0 and st.session_state.messages[-1]["role"] == "user" - - if prompt := st.chat_input("Frage Mindnet..."): + # Input Loop + if prompt := st.chat_input("Frage Mindnet... (z.B. 'Neues Projekt anlegen')"): st.session_state.messages.append({"role": "user", "content": prompt}) st.rerun() + last_msg_is_user = len(st.session_state.messages) > 0 and st.session_state.messages[-1]["role"] == "user" if last_msg_is_user: - last_prompt = st.session_state.messages[-1]["content"] with st.chat_message("assistant"): with st.spinner("Thinking..."): - resp = send_chat_message(last_prompt, top_k, explain) + resp = send_chat_message(st.session_state.messages[-1]["content"], top_k, explain) if "error" in resp: st.error(resp["error"]) else: - msg_data = { + st.session_state.messages.append({ "role": "assistant", "content": resp.get("answer"), "intent": resp.get("intent", "FACT"), "intent_source": resp.get("intent_source", "Unknown"), "sources": resp.get("sources", []), "query_id": resp.get("query_id") - } - st.session_state.messages.append(msg_data) + }) st.rerun() -def render_creation_interface(): - st.header("📝 Manueller Eintrag (Legacy)") - st.info("Nutze lieber den Chat ('Neues Projekt anlegen') für den Interview-Modus.") +def render_manual_editor(): + st.header("📝 Manueller Editor") + st.info("Hier kannst du eine Notiz komplett von Null erstellen.") - with st.form("new_entry"): - col1, col2 = st.columns([3, 1]) - title = col1.text_input("Titel der Notiz", placeholder="z.B. Projekt Gamma Meeting") - n_type = col2.selectbox("Typ", ["concept", "meeting", "person", "project", "decision"]) - - content = st.text_area("Inhalt (Markdown)", height=300, placeholder="# Protokoll\n\n- Punkt 1...") - - st.markdown("**Automatische Vernetzung:**") - st.caption("Verwende `[[Link]]` für Referenzen und `[[rel:depends_on X]]` für logische Kanten.") - - submitted = st.form_submit_button("💾 Speichern & Indizieren") - if submitted: - st.success(f"Mockup: Notiz '{title}' ({n_type}) wäre jetzt gespeichert worden!") - st.balloons() + # Wiederverwendung der Editor-Logik wäre ideal, hier vereinfacht: + c1, c2 = st.columns([1, 2]) + n_type = c1.selectbox("Typ", ["concept", "project", "decision", "experience", "value", "goal"]) + tags = c2.text_input("Tags") + body = st.text_area("Inhalt", height=400, placeholder="# Titel\n\nText...") + + if st.button("Generieren & Download"): + meta = {"type": n_type, "status": "draft", "tags": [t.strip() for t in tags.split(",")]} + doc = build_markdown_doc(meta, body) + st.code(doc, language="markdown") -# --- MAIN LOOP --- +# --- MAIN --- mode, top_k, explain = render_sidebar() - if mode == "💬 Chat": render_chat_interface(top_k, explain) else: - render_creation_interface() \ No newline at end of file + render_manual_editor() \ No newline at end of file