diff --git a/app/frontend/ui.py b/app/frontend/ui.py index d5c1706..5f04d9d 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.2", page_icon="🧠", layout="wide") +st.set_page_config(page_title="mindnet v2.3.4", page_icon="🧠", layout="wide") # --- CSS STYLING --- st.markdown(""" @@ -194,7 +194,7 @@ def analyze_draft_text(text: str, n_type: str): response = requests.post( INGEST_ANALYZE_ENDPOINT, json={"text": text, "type": n_type}, - timeout=15 # Erhöhtes Timeout für Suche + timeout=15 ) response.raise_for_status() return response.json() @@ -225,7 +225,7 @@ def submit_feedback(query_id, node_id, score, comment=None): def render_sidebar(): with st.sidebar: st.title("🧠 mindnet") - st.caption("v2.3.3 | WP-10b (Intelligence)") + st.caption("v2.3.4 | WP-10b (Intelligence)") mode = st.radio("Modus", ["💬 Chat", "📝 Manueller Editor"], index=0) st.divider() st.subheader("⚙️ Settings") @@ -243,7 +243,7 @@ def render_draft_editor(msg): qid = msg.get('query_id', str(uuid.uuid4())) key_base = f"draft_{qid}" - # 1. Init + # 1. Init (Nur beim allerersten Laden) if f"{key_base}_init" not in st.session_state: meta, body = parse_markdown_draft(msg["content"]) @@ -263,19 +263,89 @@ def render_draft_editor(msg): # Metadata c1, c2 = st.columns([2, 1]) with c1: - new_title = st.text_input("Titel", value=st.session_state.get(f"{key_base}_title", ""), key=f"{key_base}_inp_title") + # Titel immer aus State lesen/schreiben + new_title = st.text_input("Titel", key=f"{key_base}_inp_title", value=st.session_state.get(f"{key_base}_title", "")) 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") 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_tags = st.text_input("Tags (kommagetrennt)", value=st.session_state.get(f"{key_base}_tags", ""), key=f"{key_base}_inp_tags") + new_tags = st.text_input("Tags (kommagetrennt)", key=f"{key_base}_inp_tags", value=st.session_state.get(f"{key_base}_tags", "")) - # Tabs (Jetzt mit "Intelligence") + # Tabs tab_edit, tab_intel, tab_view = st.tabs(["✏️ Inhalt", "🧠 Intelligence", "👁️ Vorschau"]) - # Live Reassembly für alle Tabs + # --- TAB 1: EDITOR --- + with tab_edit: + # WICHTIG: Das Text-Area ist an session_state gebunden via 'key'. + current_body = st.text_area( + "Body", + key=f"{key_base}_txt_body", # Master-Key + value=st.session_state.get(f"{key_base}_body", ""), + height=500, + label_visibility="collapsed" + ) + # Sync zurück zum generischen Key für andere Tabs + 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"): + with st.spinner("Analysiere..."): + # Wir nehmen explizit den Text aus dem Widget-State + text_to_analyze = st.session_state[f"{key_base}_txt_body"] + analysis = analyze_draft_text(text_to_analyze, new_type) + + if "error" in analysis: + st.error(f"Fehler: {analysis['error']}") + else: + suggestions = analysis.get("suggestions", []) + st.session_state[f"{key_base}_suggestions"] = suggestions + if not suggestions: + st.warning("Keine Vorschläge gefunden.") + + suggestions = st.session_state.get(f"{key_base}_suggestions", []) + if suggestions: + st.write(f"**{len(suggestions)} Vorschläge:**") + for idx, sugg in enumerate(suggestions): + link_text = sugg.get('suggested_markdown', '') + + # Check: Ist der Link schon im Text? + is_inserted = link_text in st.session_state[f"{key_base}_txt_body"] + + # 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" + + st.markdown(f""" +
+ {sugg.get('target_title', 'Unbekannt')} ({sugg.get('type', 'semantic')})
+ {sugg.get('reason', 'N/A')}
+ {link_text} +
+ """, unsafe_allow_html=True) + + # Button Logik (Toggle) + if is_inserted: + if st.button(f"❌ Entfernen", key=f"del_{idx}_{key_base}"): + new_text = st.session_state[f"{key_base}_txt_body"].replace(link_text, "").strip() + st.session_state[f"{key_base}_txt_body"] = new_text + st.session_state[f"{key_base}_body"] = new_text + st.rerun() + else: + if st.button(f"➕ Einfügen", key=f"add_{idx}_{key_base}"): + old_text = st.session_state[f"{key_base}_txt_body"] + new_text = f"{old_text}\n\n{link_text}" + st.session_state[f"{key_base}_txt_body"] = new_text + st.session_state[f"{key_base}_body"] = new_text + st.rerun() + + # --- TAB 3: PREVIEW & SAVE --- + + # Reassemble Metadata & Body (Always use latest state) final_tags_list = [t.strip() for t in new_tags.split(",") if t.strip()] final_meta = { "id": "generated_on_save", @@ -285,57 +355,10 @@ def render_draft_editor(msg): "tags": final_tags_list } - # --- TAB 1: EDITOR --- - with tab_edit: - new_body = st.text_area( - "Body", - value=st.session_state.get(f"{key_base}_body", ""), - height=500, - key=f"{key_base}_txt_body", - label_visibility="collapsed" - ) + # Wir nehmen den aktuellsten Body aus dem State + final_body_content = st.session_state.get(f"{key_base}_txt_body", "") + final_doc = build_markdown_doc(final_meta, final_body_content) - # --- TAB 2: INTELLIGENCE (WP-11 Features) --- - with tab_intel: - st.info("Klicke auf 'Analysieren', um Verknüpfungen zu finden.") - - if st.button("🔍 Analyse starten", key=f"{key_base}_analyze"): - with st.spinner("Analysiere Text und suche Verknüpfungen..."): - current_text = st.session_state[f"{key_base}_body"] - # API Call - analysis = analyze_draft_text(current_text, new_type) - - if "error" in analysis: - st.error(f"Fehler: {analysis['error']}") - else: - suggestions = analysis.get("suggestions", []) - st.session_state[f"{key_base}_suggestions"] = suggestions - if not suggestions: - st.warning("Keine offensichtlichen Verknüpfungen gefunden.") - - # Anzeige der Vorschläge - 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.get('target_title', 'Unbekannt')} ({sugg.get('type', 'semantic')})
- Grund: {sugg.get('reason', 'N/A')}
- {sugg.get('suggested_markdown', '')} -
- """, unsafe_allow_html=True) - - if st.button("➕ Einfügen", key=f"{key_base}_add_{idx}"): - 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.get('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) @@ -343,24 +366,19 @@ def render_draft_editor(msg): st.markdown("---") - # Actions (SAVE & EXPORT) + # Save Action b1, b2 = st.columns([1, 1]) with b1: - # Echter Save Button (Ruft API auf) 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" - # Wir holen den aktuellsten Stand aus dem State (inklusive eingefügter Links) - latest_body = st.session_state.get(f"{key_base}_body", "") - latest_doc = build_markdown_doc(final_meta, latest_body) - - result = save_draft_to_vault(latest_doc, filename=fname) + # Hier der entscheidende Call mit dem aktuellen Dokument + result = save_draft_to_vault(final_doc, filename=fname) if "error" in result: - st.error(f"Fehler beim Speichern: {result['error']}") + st.error(f"Fehler: {result['error']}") else: st.success(f"Gespeichert: {result.get('file_path')}") st.balloons() @@ -447,6 +465,7 @@ def render_manual_editor(): else: st.success(f"Gespeichert: {res.get('file_path')}") +# --- MAIN --- mode, top_k, explain = render_sidebar() if mode == "💬 Chat": render_chat_interface(top_k, explain) diff --git a/app/routers/ingest.py b/app/routers/ingest.py index 5533005..a1407b3 100644 --- a/app/routers/ingest.py +++ b/app/routers/ingest.py @@ -1,21 +1,27 @@ """ -app/routers/ingest.py - DEBUG VERSION +app/routers/ingest.py +API-Endpunkte für WP-11 (Discovery & Persistence). """ import os import time import logging from fastapi import APIRouter, HTTPException from pydantic import BaseModel -from typing import Optional, List, Dict, Any +from typing import Optional, Dict, Any from app.core.ingestion import IngestionService +# Fallback: Falls DiscoveryService noch fehlt, nutzen wir Ingest Service Features oder Mock +# Wir gehen hier davon aus, dass wir alles im IngestionService oder Router machen können, +# um Importfehler zu vermeiden. from app.core.retriever import Retriever from app.models.dto import QueryRequest logger = logging.getLogger(__name__) + router = APIRouter() # --- DTOs --- + class AnalyzeRequest(BaseModel): text: str type: str = "concept" @@ -37,43 +43,36 @@ class SaveResponse(BaseModel): async def analyze_draft(req: AnalyzeRequest): """ WP-11 Intelligence: Liefert Link-Vorschläge. - DEBUG MODE: Threshold gesenkt, Logging erhöht. + Implementiert direkt hier, um Abhängigkeiten zu reduzieren. """ try: retriever = Retriever() suggestions = [] query_text = req.text[:400] - logger.info(f"ANALYZING TEXT: '{query_text}' (Type: {req.type})") - if not query_text.strip(): return {"suggestions": []} - # Wir suchen - hits_result = await retriever.search(QueryRequest(query=query_text, top_k=5, mode="hybrid")) - - logger.info(f"RETRIEVER FOUND: {len(hits_result.results)} raw hits") + # 1. Semantic Search + # Safe async call check + if hasattr(retriever.search, '__await__'): + hits_result = await retriever.search(QueryRequest(query=query_text, top_k=5, mode="hybrid")) + else: + hits_result = await retriever.search(QueryRequest(query=query_text, top_k=5, mode="hybrid")) seen_titles = set() for hit in hits_result.results: - # Titel holen - title = hit.payload.get("title") or hit.payload.get("note_id") or hit.node_id - - # Logging für jeden Treffer - logger.info(f" -> CHECK HIT: {title} | Score: {hit.total_score:.4f}") - - if not title or title in seen_titles: - continue + # Titel ermitteln + title = hit.payload.get("note_id") or hit.node_id + if not title or title in seen_titles: continue seen_titles.add(title) - # Edge Logic edge_kind = "related_to" if req.type == "project": edge_kind = "depends_on" if req.type == "decision": edge_kind = "references" - # --- ÄNDERUNG: THRESHOLD GESENKT --- - # War vorher 0.65. Jetzt 0.3 für Tests. - if hit.total_score > 0.3: + # Score Threshold + if hit.total_score > 0.4: # Etwas toleranter suggestions.append({ "target_title": title, "target_id": hit.node_id, @@ -81,19 +80,19 @@ async def analyze_draft(req: AnalyzeRequest): "reason": f"Semantisch ähnlich ({hit.total_score:.2f})", "type": "semantic" }) - else: - logger.info(f" -> SKIPPED (Score too low)") - logger.info(f"RETURNING {len(suggestions)} SUGGESTIONS") return {"suggestions": suggestions} except Exception as e: logger.error(f"Analyze failed: {e}", exc_info=True) - raise HTTPException(status_code=500, detail=f"Analysis failed: {str(e)}") + # Kein 500er werfen, lieber leere Liste, damit UI nicht crasht + return {"suggestions": [], "error": str(e)} @router.post("/save", response_model=SaveResponse) async def save_note(req: SaveRequest): - """WP-11 Persistence""" + """ + WP-11 Persistence: Speichert Markdown physisch und indiziert es sofort. + """ try: vault_root = os.getenv("MINDNET_VAULT_ROOT", "./vault") abs_vault_root = os.path.abspath(vault_root) @@ -106,19 +105,28 @@ async def save_note(req: SaveRequest): final_filename = f"draft_{int(time.time())}.md" ingest_service = IngestionService() - logger.info(f"Saving {final_filename}") - - result = await ingest_service.save_and_index( - markdown_content=req.markdown_content, - filename=final_filename - ) + logger.info(f"Saving {final_filename} to {req.folder}") + + # --- AWAIT WICHTIG! --- + # Wir rufen save_and_index auf (so hieß es in meiner IngestionService Implementierung) + # Wenn deine Methode create_from_text heißt, ändere es hier entsprechend. + # Ich nutze hier save_and_index als Standard aus WP-11. + + if hasattr(ingest_service, 'save_and_index'): + result = await ingest_service.save_and_index(req.markdown_content, final_filename) + elif hasattr(ingest_service, 'create_from_text'): + # Fallback falls du die alte Version hast + result = await ingest_service.create_from_text(req.markdown_content, final_filename, abs_vault_root, req.folder) + else: + raise RuntimeError("IngestionService hat weder save_and_index noch create_from_text") + if result.get("status") == "error": raise HTTPException(status_code=500, detail=result.get("error")) return SaveResponse( status="success", - file_path=result.get("file_path", "unknown"), + file_path=result.get("file_path") or result.get("path", "unknown"), note_id=result.get("note_id", "unknown"), stats=result.get("stats", {}) )