diff --git a/app/frontend/ui.py b/app/frontend/ui.py index b3f52a8..0fa5783 100644 --- a/app/frontend/ui.py +++ b/app/frontend/ui.py @@ -21,7 +21,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.2", page_icon="๐Ÿง ", layout="wide") +st.set_page_config(page_title="mindnet v2.3.2 (Debug Mode)", page_icon="๐Ÿž", layout="wide") # --- CSS STYLING --- st.markdown(""" @@ -35,7 +35,6 @@ st.markdown(""" border: 1px solid #d2e3fc; display: inline-block; margin-bottom: 0.5rem; } - /* Editor Box Styling */ .draft-box { border: 1px solid #d0d7de; border-radius: 6px; @@ -45,7 +44,6 @@ st.markdown(""" margin-bottom: 10px; } - /* Preview Styling mimics GitHub Markdown */ .preview-box { border: 1px solid #e0e0e0; border-radius: 6px; @@ -60,54 +58,57 @@ st.markdown(""" if "messages" not in st.session_state: st.session_state.messages = [] if "user_id" not in st.session_state: st.session_state.user_id = str(uuid.uuid4()) -# --- HELPER FUNCTIONS --- +# --- HELPER FUNCTIONS (ROBUST PARSING) --- def parse_markdown_draft(full_text): """ - Zerlegt die LLM-Antwort in Frontmatter (Dict) und Body (String). - Robust gegen Einleitungstexte vor dem Codeblock. + Versucht extrem tolerant, Frontmatter und Body zu trennen. """ - # 1. Extrahiere Codeblock - pattern_block = r"```markdown\s*(.*?)\s*```" - match_block = re.search(pattern_block, full_text, re.DOTALL) + clean_text = full_text - # Fallback: Wenn kein Codeblock, nimm ganzen Text - clean_text = match_block.group(1).strip() if match_block else full_text + # 1. Versuch: Markdown Fences entfernen (egal ob ```markdown, ```md oder nur ```) + # re.IGNORECASE und re.DOTALL sind wichtig! + pattern_block = r"```(?:markdown|md)?\s*(.*?)\s*```" + match_block = re.search(pattern_block, full_text, re.DOTALL | re.IGNORECASE) + + if match_block: + clean_text = match_block.group(1).strip() + # Debugging Info im UI anzeigen ist schwer hier, wir verlassen uns auf den Return - # 2. Trenne Frontmatter (YAML) vom Body - # Sucht nach --- am Anfang, gefolgt von YAML, gefolgt von --- - pattern_fm = r"^---\s+(.*?)\s+---\s*(.*)$" + # 2. Versuch: YAML Frontmatter finden + # Suche nach --- am Anfang (oder nach Whitespace), gefolgt von Inhalt, gefolgt von --- + pattern_fm = r"^\s*---\s+(.*?)\s+---\s*(.*)$" match_fm = re.search(pattern_fm, clean_text, re.DOTALL) + meta = {} + body = clean_text + if match_fm: yaml_str = match_fm.group(1) body = match_fm.group(2) try: - meta = yaml.safe_load(yaml_str) or {} - except Exception: - meta = {} # Fallback bei kaputtem YAML - return meta, body - else: - # Kein Frontmatter gefunden -> Alles ist Body - return {}, clean_text - -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" + # YAML laden, aber Fehler abfangen + parsed = yaml.safe_load(yaml_str) + if isinstance(parsed, dict): + meta = parsed + except Exception as e: + print(f"YAML Parsing Error: {e}") # Geht in Server Log + # Wir behalten body, meta bleibt leer + + return meta, body 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() + try: + yaml_str = yaml.dump(meta, default_flow_style=None, sort_keys=False, allow_unicode=True).strip() + except: + yaml_str = "error: generating_yaml" + return f"---\n{yaml_str}\n---\n\n{body}" def load_history_from_logs(limit=10): @@ -151,7 +152,7 @@ def submit_feedback(query_id, node_id, score, comment=None): def render_sidebar(): with st.sidebar: st.title("๐Ÿง  mindnet") - st.caption("v2.3.2 | WP-10 UI") + st.caption("DEBUG MODE | WP-10 UI") mode = st.radio("Modus", ["๐Ÿ’ฌ Chat", "๐Ÿ“ Manueller Editor"], index=0) @@ -176,8 +177,11 @@ def render_draft_editor(msg): key_base = f"draft_{qid}" # 1. State Initialisierung (Nur einmal pro Nachricht) + # Wir parsen IMMER neu, wenn der Key noch nicht im State ist if f"{key_base}_init" not in st.session_state: meta, body = parse_markdown_draft(msg["content"]) + + # Fallback Defaults st.session_state[f"{key_base}_type"] = meta.get("type", "default") # Tags Behandlung (Liste oder String) @@ -193,39 +197,36 @@ def render_draft_editor(msg): # 2. Editor Container st.markdown(f'
', unsafe_allow_html=True) st.markdown("### ๐Ÿ“ Entwurf bearbeiten") - 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: - # 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"] + known_types = ["concept", "project", "decision", "experience", "journal", "person", "value", "goal", "principle", "default"] + curr_type = st.session_state.get(f"{key_base}_type", "default") if curr_type not in known_types: known_types.append(curr_type) + # Selectbox mit State-Sync new_type = st.selectbox("Typ", known_types, index=known_types.index(curr_type), key=f"{key_base}_sel_type") with c2: - new_tags = st.text_input("Tags (kommagetrennt)", value=st.session_state[f"{key_base}_tags"], key=f"{key_base}_inp_tags") + new_tags = st.text_input("Tags (kommagetrennt)", value=st.session_state.get(f"{key_base}_tags", ""), key=f"{key_base}_inp_tags") # 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"], + "Inhalt (Markdown Body)", + value=st.session_state.get(f"{key_base}_body", ""), height=500, key=f"{key_base}_txt_body", - label_visibility="collapsed", - help="Hier kannst du Markdown schreiben." + label_visibility="collapsed" ) - # Live-Zusammenbau fรผr Vorschau & Download + # Live-Zusammenbau 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 + "id": "generated_on_save", "type": new_type, "status": "draft", "tags": final_tags_list @@ -234,47 +235,48 @@ def render_draft_editor(msg): with tab_view: st.markdown('
', unsafe_allow_html=True) - st.markdown(final_doc) # Rendered Markdown + st.markdown(final_doc) st.markdown('
', unsafe_allow_html=True) st.markdown("---") - # 5. Actions (Writeback Simulation) + # 5. Actions b1, b2 = st.columns([1, 1]) with b1: - # Download Button (Client-Side) st.download_button( - label="๐Ÿ’พ Als .md Datei speichern", + label="๐Ÿ’พ Download .md", data=final_doc, file_name=generate_filename(final_meta), mime="text/markdown" ) with b2: - # Copy Button Logic - if st.button("๐Ÿ“‹ Code anzeigen (Copy)", key=f"{key_base}_btn_copy"): + if st.button("๐Ÿ“‹ Code Copy", key=f"{key_base}_btn_copy"): st.code(final_doc, language="markdown") st.markdown("
", unsafe_allow_html=True) def render_chat_interface(top_k, explain): - for msg in st.session_state.messages: + for idx, msg in enumerate(st.session_state.messages): with st.chat_message(msg["role"]): if msg["role"] == "assistant": # Intent Badge - if "intent" in msg: - intent = msg["intent"] - icon = {"EMPATHY": "โค๏ธ", "DECISION": "โš–๏ธ", "CODING": "๐Ÿ’ป", "FACT": "๐Ÿ“š", "INTERVIEW": "๐Ÿ“"}.get(intent, "๐Ÿง ") - src = msg.get("intent_source", "?") - st.markdown(f'
{icon} Intent: {intent} ({src})
', unsafe_allow_html=True) + intent = msg.get("intent", "UNKNOWN") + src = msg.get("intent_source", "?") - # INTERVIEW Logic vs NORMAL Chat - if msg.get("intent") == "INTERVIEW": + # Icon Mapping + icon_map = {"EMPATHY":"โค๏ธ", "DECISION":"โš–๏ธ", "CODING":"๐Ÿ’ป", "FACT":"๐Ÿ“š", "INTERVIEW":"๐Ÿ“"} + icon = icon_map.get(intent, "๐Ÿง ") + + st.markdown(f'
{icon} Intent: {intent} ({src})
', unsafe_allow_html=True) + + # --- LOGIC SWITCH --- + if intent == "INTERVIEW": render_draft_editor(msg) else: st.markdown(msg["content"]) - # Sources & Feedback (nur bei RAG sinnvoll) + # --- SOURCES --- if "sources" in msg and msg["sources"]: for hit in msg["sources"]: score = hit.get('total_score', 0) @@ -284,18 +286,20 @@ def render_chat_interface(top_k, explain): if hit.get('explanation'): st.caption(f"Grund: {hit['explanation']['reasons'][0]['message']}") - def _cb(qid=msg["query_id"], nid=hit['node_id']): + def _cb(qid=msg.get("query_id"), nid=hit.get('node_id')): val = st.session_state.get(f"fb_src_{qid}_{nid}") 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) + + st.feedback("faces", key=f"fb_src_{msg.get('query_id')}_{hit.get('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)) + # --- DEBUGGING (NEU) --- + with st.expander("๐Ÿž Debug Raw Payload"): + st.json(msg) # Zeigt exakt, was das Backend geschickt hat + else: st.markdown(msg["content"]) - # Input Loop + # Input if prompt := st.chat_input("Frage Mindnet... (z.B. 'Neues Projekt anlegen')"): st.session_state.messages.append({"role": "user", "content": prompt}) st.rerun() @@ -322,8 +326,6 @@ def render_chat_interface(top_k, explain): def render_manual_editor(): st.header("๐Ÿ“ Manueller Editor") st.info("Hier kannst du eine Notiz komplett von Null erstellen.") - - # 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")