diff --git a/app/frontend/ui_editor.py b/app/frontend/ui_editor.py index ec9d34d..7d103e6 100644 --- a/app/frontend/ui_editor.py +++ b/app/frontend/ui_editor.py @@ -1,10 +1,10 @@ """ FILE: app/frontend/ui_editor.py -DESCRIPTION: Markdown-Editor mit Live-Vorschau und Metadaten-Feldern. Unterstützt Intelligence-Features (Link-Vorschläge) und unterscheidet Create/Update-Modus. -VERSION: 2.6.0 +DESCRIPTION: Markdown-Editor mit Live-Vorschau. +Refactored für WP-14: Asynchrones Feedback-Handling (Queued State). +VERSION: 2.7.0 (Fix: Async Save UI) STATUS: Active DEPENDENCIES: streamlit, uuid, re, datetime, ui_utils, ui_api -LAST_ANALYSIS: 2025-12-15 """ import streamlit as st import uuid @@ -76,14 +76,11 @@ def render_draft_editor(msg): # --- UI LAYOUT --- - # Header Info (Debug Pfad anzeigen, damit wir sicher sind) origin_fname = st.session_state.get(f"{key_base}_origin_filename") if origin_fname: - # Dateiname extrahieren für saubere Anzeige display_name = str(origin_fname).split("/")[-1] st.success(f"📂 **Update-Modus**: `{display_name}`") - # Debugging: Zeige vollen Pfad im Expander with st.expander("Dateipfad Details", expanded=False): st.code(origin_fname) st.markdown(f'
', unsafe_allow_html=True) @@ -173,21 +170,33 @@ def render_draft_editor(msg): save_label = "💾 Update speichern" if origin_fname else "💾 Neu anlegen & Indizieren" if st.button(save_label, type="primary", key=f"{key_base}_save"): - with st.spinner("Speichere im Vault..."): + with st.spinner("Sende an Backend..."): if origin_fname: - # UPDATE: Ziel ist der exakte Pfad target_file = origin_fname else: - # CREATE: Neuer Dateiname raw_title = final_meta.get("title", "draft") target_file = f"{datetime.now().strftime('%Y%m%d')}-{slugify(raw_title)[:60]}.md" result = save_draft_to_vault(final_doc, filename=target_file) + + # --- WP-14 CHANGE START: Handling Async Response --- if "error" in result: st.error(f"Fehler: {result['error']}") else: - st.success(f"Gespeichert: {result.get('file_path')}") + status = result.get("status", "success") + file_path = result.get("file_path", "unbekannt") + + if status == "queued": + # Neuer Status für Async Processing + st.info(f"✅ **Eingereiht:** Datei `{file_path}` wurde gespeichert.") + st.caption("Die KI-Analyse und Indizierung läuft im Hintergrund. Du kannst weiterarbeiten.") + else: + # Legacy / Synchroner Fall + st.success(f"Gespeichert: {file_path}") + st.balloons() + # --- WP-14 CHANGE END --- + with b2: if st.button("📋 Code anzeigen", key=f"{key_base}_btn_copy"): st.code(final_doc, language="markdown") @@ -197,25 +206,18 @@ def render_draft_editor(msg): def render_manual_editor(): """ Rendert den manuellen Editor. - PRÜFT, ob eine Edit-Anfrage aus dem Graphen vorliegt! """ - target_msg = None - - # 1. Prüfen: Gibt es Nachrichten im Verlauf? if st.session_state.messages: last_msg = st.session_state.messages[-1] - - # 2. Ist die letzte Nachricht eine Edit-Anfrage? (Erkennbar am query_id prefix 'edit_') qid = str(last_msg.get("query_id", "")) if qid.startswith("edit_"): target_msg = last_msg - # 3. Fallback: Leeres Template, falls keine Edit-Anfrage vorliegt if not target_msg: target_msg = { "content": "---\ntype: concept\ntitle: Neue Notiz\nstatus: draft\ntags: []\n---\n# Titel\n", - "query_id": f"manual_{uuid.uuid4()}" # Eigene ID, damit neuer State entsteht + "query_id": f"manual_{uuid.uuid4()}" } render_draft_editor(target_msg) \ No newline at end of file diff --git a/app/routers/ingest.py b/app/routers/ingest.py index 9603171..cfac79d 100644 --- a/app/routers/ingest.py +++ b/app/routers/ingest.py @@ -1,16 +1,17 @@ """ FILE: app/routers/ingest.py -DESCRIPTION: Endpunkte für WP-11. Nimmt Markdown entgegen, steuert Ingestion und Discovery (Link-Vorschläge). -VERSION: 0.6.0 +DESCRIPTION: Endpunkte für WP-11. Nimmt Markdown entgegen. +Refactored für WP-14: Nutzt BackgroundTasks für non-blocking Save. +VERSION: 0.7.0 (Fix: Timeout WP-14) STATUS: Active DEPENDENCIES: app.core.ingestion, app.services.discovery, fastapi, pydantic -LAST_ANALYSIS: 2025-12-15 """ import os import time import logging -from fastapi import APIRouter, HTTPException +import asyncio +from fastapi import APIRouter, HTTPException, BackgroundTasks from pydantic import BaseModel from typing import Optional, Dict, Any @@ -20,7 +21,7 @@ from app.services.discovery import DiscoveryService logger = logging.getLogger(__name__) router = APIRouter() -# Services Init (Global oder via Dependency Injection) +# Services Init discovery_service = DiscoveryService() class AnalyzeRequest(BaseModel): @@ -36,7 +37,32 @@ class SaveResponse(BaseModel): status: str file_path: str note_id: str - stats: Dict[str, Any] + message: str # Neu für UX Feedback + stats: Dict[str, Any] # Kann leer sein bei async processing + +# --- Background Task Wrapper --- +async def run_ingestion_task(markdown_content: str, filename: str, vault_root: str, folder: str): + """ + Führt die Ingestion im Hintergrund aus, damit der Request nicht blockiert. + """ + logger.info(f"🔄 Background Task started: Ingesting {filename}...") + try: + ingest_service = IngestionService() + result = await ingest_service.create_from_text( + markdown_content=markdown_content, + filename=filename, + vault_root=vault_root, + folder=folder + ) + # Hier könnte man später Notification-Services (Websockets) triggern + if result.get("status") == "error": + logger.error(f"❌ Background Ingestion Error for {filename}: {result.get('error')}") + else: + logger.info(f"✅ Background Task finished: {filename} ({result.get('chunks_count')} Chunks)") + + except Exception as e: + logger.error(f"❌ Critical Background Task Failure: {e}", exc_info=True) + @router.post("/analyze") async def analyze_draft(req: AnalyzeRequest): @@ -44,7 +70,6 @@ async def analyze_draft(req: AnalyzeRequest): WP-11 Intelligence: Liefert Link-Vorschläge via DiscoveryService. """ try: - # Hier rufen wir jetzt den verbesserten Service auf result = await discovery_service.analyze_draft(req.text, req.type) return result except Exception as e: @@ -52,9 +77,10 @@ async def analyze_draft(req: AnalyzeRequest): return {"suggestions": [], "error": str(e)} @router.post("/save", response_model=SaveResponse) -async def save_note(req: SaveRequest): +async def save_note(req: SaveRequest, background_tasks: BackgroundTasks): """ - WP-11 Persistence: Speichert und indiziert. + WP-14 Fix: Startet Ingestion im Hintergrund (Fire & Forget). + Verhindert Timeouts bei aktiver Smart-Edge-Allocation (WP-15). """ try: vault_root = os.getenv("MINDNET_VAULT_ROOT", "./vault") @@ -65,29 +91,31 @@ async def save_note(req: SaveRequest): except: pass final_filename = req.filename or f"draft_{int(time.time())}.md" - ingest_service = IngestionService() - # Async Call - result = await ingest_service.create_from_text( + # Wir geben sofort eine ID zurück (optimistisch), + # auch wenn die echte ID erst nach dem Parsing feststeht. + # Für UI-Feedback nutzen wir den Filename. + + # Task in die Queue schieben + background_tasks.add_task( + run_ingestion_task, markdown_content=req.markdown_content, filename=final_filename, vault_root=abs_vault_root, folder=req.folder ) - if result.get("status") == "error": - raise HTTPException(status_code=500, detail=result.get("error")) - return SaveResponse( - status="success", - file_path=result.get("path", "unknown"), - note_id=result.get("note_id", "unknown"), + status="queued", + file_path=os.path.join(req.folder, final_filename), + note_id="pending", + message="Speicherung & KI-Analyse im Hintergrund gestartet.", stats={ - "chunks": result.get("chunks_count", 0), - "edges": result.get("edges_count", 0) + "chunks": -1, # Indikator für Async + "edges": -1 } ) - except HTTPException as he: raise he + except Exception as e: - logger.error(f"Save failed: {e}", exc_info=True) - raise HTTPException(status_code=500, detail=f"Save failed: {str(e)}") \ No newline at end of file + logger.error(f"Save dispatch failed: {e}", exc_info=True) + raise HTTPException(status_code=500, detail=f"Save dispatch failed: {str(e)}") \ No newline at end of file