diff --git a/app/frontend/ui.py b/app/frontend/ui.py index d3e714b..adb7cdc 100644 --- a/app/frontend/ui.py +++ b/app/frontend/ui.py @@ -14,6 +14,8 @@ load_dotenv() API_BASE_URL = os.getenv("MINDNET_API_URL", "http://localhost:8002") CHAT_ENDPOINT = f"{API_BASE_URL}/chat" FEEDBACK_ENDPOINT = f"{API_BASE_URL}/feedback" +INGEST_ANALYZE_ENDPOINT = f"{API_BASE_URL}/ingest/analyze" +INGEST_SAVE_ENDPOINT = f"{API_BASE_URL}/ingest/save" HISTORY_FILE = Path("data/logs/search_history.jsonl") # Timeout Strategy @@ -52,10 +54,13 @@ st.markdown(""" font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif; } - .debug-info { - font-size: 0.7rem; - color: #888; - margin-bottom: 5px; + .suggestion-card { + border-left: 3px solid #1a73e8; + background-color: #ffffff; + padding: 10px; + margin-bottom: 8px; + border-radius: 4px; + box-shadow: 0 1px 3px rgba(0,0,0,0.1); } """, unsafe_allow_html=True) @@ -67,20 +72,14 @@ if "user_id" not in st.session_state: st.session_state.user_id = str(uuid.uuid4( # --- HELPER FUNCTIONS --- def normalize_meta_and_body(meta, body): - """ - Sanitizer: Stellt sicher, dass nur erlaubte Felder im Frontmatter bleiben. - Alles andere wird in den Body verschoben (Repair-Strategie). - """ + """Sanitizer: Stellt sicher, dass nur erlaubte Felder im Frontmatter bleiben.""" ALLOWED_KEYS = {"title", "type", "status", "tags", "id", "created", "updated", "aliases", "lang"} - clean_meta = {} extra_content = [] - # 1. Title/Titel Normalisierung if "titel" in meta and "title" not in meta: meta["title"] = meta.pop("titel") - # 2. Tags Normalisierung (Synonyme) tag_candidates = ["tags", "emotionale_keywords", "keywords", "schluesselwoerter"] all_tags = [] for key in tag_candidates: @@ -89,14 +88,12 @@ def normalize_meta_and_body(meta, body): if isinstance(val, list): all_tags.extend(val) elif isinstance(val, str): all_tags.extend([t.strip() for t in val.split(",")]) - # 3. Filterung und Verschiebung for key, val in meta.items(): if key in ALLOWED_KEYS: clean_meta[key] = val elif key in tag_candidates: - pass # Schon oben behandelt + pass else: - # Unerlaubtes Feld (z.B. 'situation') -> Ab in den Body! if val and isinstance(val, str): header = key.replace("_", " ").title() extra_content.append(f"## {header}\n{val}\n") @@ -104,7 +101,6 @@ def normalize_meta_and_body(meta, body): if all_tags: clean_meta["tags"] = list(set(all_tags)) - # 4. Body Zusammenbau if extra_content: new_section = "\n".join(extra_content) final_body = f"{new_section}\n{body}" @@ -114,18 +110,14 @@ def normalize_meta_and_body(meta, body): return clean_meta, final_body def parse_markdown_draft(full_text): - """ - Robustes Parsing + Sanitization. - """ + """Robustes Parsing + Sanitization.""" clean_text = full_text - # Codeblock entfernen 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() - # Frontmatter splitten parts = re.split(r"^---+\s*$", clean_text, maxsplit=2, flags=re.MULTILINE) meta = {} @@ -152,7 +144,6 @@ def build_markdown_doc(meta, body): meta["updated"] = datetime.now().strftime("%Y-%m-%d") - # Sortierung für UX ordered_meta = {} prio_keys = ["id", "type", "title", "status", "tags"] for k in prio_keys: @@ -183,6 +174,8 @@ def load_history_from_logs(limit=10): except: pass return queries +# --- API CLIENT --- + def send_chat_message(message: str, top_k: int, explain: bool): try: response = requests.post( @@ -195,6 +188,32 @@ def send_chat_message(message: str, top_k: int, explain: bool): except Exception as e: 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, + json={"text": text, "type": n_type}, + timeout=10 + ) + response.raise_for_status() + return response.json() + except Exception as e: + 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=30 # Indizierung kann dauern + ) + response.raise_for_status() + return response.json() + except Exception as e: + return {"error": str(e)} + 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) @@ -230,15 +249,14 @@ def render_draft_editor(msg): 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) - st.session_state[f"{key_base}_body"] = body.strip() st.session_state[f"{key_base}_meta"] = meta + st.session_state[f"{key_base}_suggestions"] = [] st.session_state[f"{key_base}_init"] = True - # 2. UI + # 2. UI Layout st.markdown(f'
', unsafe_allow_html=True) st.markdown("### 📝 Entwurf bearbeiten") @@ -254,11 +272,21 @@ def render_draft_editor(msg): new_tags = st.text_input("Tags (kommagetrennt)", value=st.session_state.get(f"{key_base}_tags", ""), key=f"{key_base}_inp_tags") - # Tabs - tab_edit, tab_view = st.tabs(["✏️ Inhalt", "👁️ Vorschau"]) + # Tabs (Jetzt mit "Intelligence") + tab_edit, tab_intel, tab_view = st.tabs(["✏️ Inhalt", "🧠 Intelligence", "👁️ Vorschau"]) + # Live Reassembly für alle Tabs + final_tags_list = [t.strip() for t in new_tags.split(",") if t.strip()] + final_meta = { + "id": "generated_on_save", + "type": new_type, + "title": new_title, + "status": "draft", + "tags": final_tags_list + } + + # --- TAB 1: EDITOR --- with tab_edit: - st.caption("Bearbeite hier den Inhalt. Metadaten (oben) werden automatisch hinzugefügt.") new_body = st.text_area( "Body", value=st.session_state.get(f"{key_base}_body", ""), @@ -267,19 +295,43 @@ def render_draft_editor(msg): label_visibility="collapsed" ) - # Reassembly - final_tags_list = [t.strip() for t in new_tags.split(",") if t.strip()] - final_meta = st.session_state.get(f"{key_base}_meta", {}).copy() - final_meta.update({ - "id": "generated_on_save", - "type": new_type, - "title": new_title, - "status": "draft", - "tags": final_tags_list - }) - - final_doc = build_markdown_doc(final_meta, new_body) - + # --- TAB 2: INTELLIGENCE (WP-11 Features) --- + with tab_intel: + st.info("Klicke auf 'Analysieren', um Verknüpfungen zu finden.") + + if st.button("🔍 Draft Analysieren", key=f"{key_base}_analyze"): + with st.spinner("Analysiere Text und suche Verknüpfungen..."): + analysis = analyze_draft_text(new_body, new_type) + if "error" in analysis: + st.error(f"Fehler: {analysis['error']}") + else: + st.session_state[f"{key_base}_suggestions"] = analysis.get("suggestions", []) + if not analysis.get("suggestions"): + st.warning("Keine offensichtlichen Verknüpfungen gefunden.") + + suggestions = st.session_state.get(f"{key_base}_suggestions", []) + if suggestions: + st.markdown(f"**{len(suggestions)} Vorschläge gefunden:**") + for idx, sugg in enumerate(suggestions): + with st.container(): + st.markdown(f""" +
+ {sugg['target_title']} ({sugg['type']})
+ Grund: {sugg.get('reason', 'N/A')}
+ {sugg['suggested_markdown']} +
+ """, unsafe_allow_html=True) + + if st.button("➕ Einfügen", key=f"{key_base}_add_{idx}"): + # Append to body + current_body = st.session_state[f"{key_base}_body"] + updated_body = f"{current_body}\n\n{sugg['suggested_markdown']}" + st.session_state[f"{key_base}_body"] = updated_body + st.toast(f"Link zu '{sugg['target_title']}' eingefügt!") + st.rerun() + + # --- TAB 3: PREVIEW --- + final_doc = build_markdown_doc(final_meta, st.session_state.get(f"{key_base}_body", "")) with tab_view: st.markdown('
', unsafe_allow_html=True) st.markdown(final_doc) @@ -287,11 +339,23 @@ def render_draft_editor(msg): st.markdown("---") - # Actions + # Actions (SAVE & EXPORT) b1, b2 = st.columns([1, 1]) with b1: - fname = f"{datetime.now().strftime('%Y%m%d')}-{new_type}.md" - st.download_button("💾 Download .md", data=final_doc, file_name=fname, mime="text/markdown") + # Echter Save Button + if st.button("💾 Speichern & Indizieren", type="primary", key=f"{key_base}_save"): + with st.spinner("Speichere im Vault..."): + # Generiere Filename + safe_title = re.sub(r'[^a-zA-Z0-9]', '-', new_title).lower()[:30] + 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 beim Speichern: {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") @@ -303,13 +367,12 @@ def render_chat_interface(top_k, explain): for idx, msg in enumerate(st.session_state.messages): with st.chat_message(msg["role"]): if msg["role"] == "assistant": - # Meta + # Header intent = msg.get("intent", "UNKNOWN") src = msg.get("intent_source", "?") icon = {"EMPATHY":"❤️", "DECISION":"⚖️", "CODING":"💻", "FACT":"📚", "INTERVIEW":"📝"}.get(intent, "🧠") st.markdown(f'
{icon} Intent: {intent} ({src})
', unsafe_allow_html=True) - # Debugging (Always visible for safety) with st.expander("🐞 Debug Raw Payload", expanded=False): st.json(msg) @@ -364,9 +427,17 @@ def render_manual_editor(): 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("Code anzeigen"): + + if st.button("Speichern (Via API)"): meta = {"type": n_type, "status": "draft", "tags": [t.strip() for t in tags.split(",")]} - st.code(build_markdown_doc(meta, body), language="markdown") + doc = build_markdown_doc(meta, body) + + # Test Call + res = save_draft_to_vault(doc, filename=f"manual-{uuid.uuid4().hex[:6]}.md") + if "error" in res: + st.error(res["error"]) + else: + st.success(f"Gespeichert: {res.get('file_path')}") mode, top_k, explain = render_sidebar() if mode == "💬 Chat":