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