neue ui Save-logik

This commit is contained in:
Lars 2025-12-16 14:11:17 +01:00
parent 7639bb8472
commit 6011b96fc1
2 changed files with 71 additions and 41 deletions

View File

@ -1,10 +1,10 @@
""" """
FILE: app/frontend/ui_editor.py 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. DESCRIPTION: Markdown-Editor mit Live-Vorschau.
VERSION: 2.6.0 Refactored für WP-14: Asynchrones Feedback-Handling (Queued State).
VERSION: 2.7.0 (Fix: Async Save UI)
STATUS: Active STATUS: Active
DEPENDENCIES: streamlit, uuid, re, datetime, ui_utils, ui_api DEPENDENCIES: streamlit, uuid, re, datetime, ui_utils, ui_api
LAST_ANALYSIS: 2025-12-15
""" """
import streamlit as st import streamlit as st
import uuid import uuid
@ -76,14 +76,11 @@ def render_draft_editor(msg):
# --- UI LAYOUT --- # --- UI LAYOUT ---
# Header Info (Debug Pfad anzeigen, damit wir sicher sind)
origin_fname = st.session_state.get(f"{key_base}_origin_filename") origin_fname = st.session_state.get(f"{key_base}_origin_filename")
if origin_fname: if origin_fname:
# Dateiname extrahieren für saubere Anzeige
display_name = str(origin_fname).split("/")[-1] display_name = str(origin_fname).split("/")[-1]
st.success(f"📂 **Update-Modus**: `{display_name}`") st.success(f"📂 **Update-Modus**: `{display_name}`")
# Debugging: Zeige vollen Pfad im Expander
with st.expander("Dateipfad Details", expanded=False): with st.expander("Dateipfad Details", expanded=False):
st.code(origin_fname) st.code(origin_fname)
st.markdown(f'<div class="draft-box" style="border-left: 5px solid #ff9f43;">', unsafe_allow_html=True) st.markdown(f'<div class="draft-box" style="border-left: 5px solid #ff9f43;">', unsafe_allow_html=True)
@ -173,21 +170,33 @@ def render_draft_editor(msg):
save_label = "💾 Update speichern" if origin_fname else "💾 Neu anlegen & Indizieren" save_label = "💾 Update speichern" if origin_fname else "💾 Neu anlegen & Indizieren"
if st.button(save_label, type="primary", key=f"{key_base}_save"): 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: if origin_fname:
# UPDATE: Ziel ist der exakte Pfad
target_file = origin_fname target_file = origin_fname
else: else:
# CREATE: Neuer Dateiname
raw_title = final_meta.get("title", "draft") raw_title = final_meta.get("title", "draft")
target_file = f"{datetime.now().strftime('%Y%m%d')}-{slugify(raw_title)[:60]}.md" target_file = f"{datetime.now().strftime('%Y%m%d')}-{slugify(raw_title)[:60]}.md"
result = save_draft_to_vault(final_doc, filename=target_file) result = save_draft_to_vault(final_doc, filename=target_file)
# --- WP-14 CHANGE START: Handling Async Response ---
if "error" in result: if "error" in result:
st.error(f"Fehler: {result['error']}") st.error(f"Fehler: {result['error']}")
else: 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() st.balloons()
# --- WP-14 CHANGE END ---
with b2: with b2:
if st.button("📋 Code anzeigen", key=f"{key_base}_btn_copy"): if st.button("📋 Code anzeigen", key=f"{key_base}_btn_copy"):
st.code(final_doc, language="markdown") st.code(final_doc, language="markdown")
@ -197,25 +206,18 @@ def render_draft_editor(msg):
def render_manual_editor(): def render_manual_editor():
""" """
Rendert den manuellen Editor. Rendert den manuellen Editor.
PRÜFT, ob eine Edit-Anfrage aus dem Graphen vorliegt!
""" """
target_msg = None target_msg = None
# 1. Prüfen: Gibt es Nachrichten im Verlauf?
if st.session_state.messages: if st.session_state.messages:
last_msg = st.session_state.messages[-1] 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", "")) qid = str(last_msg.get("query_id", ""))
if qid.startswith("edit_"): if qid.startswith("edit_"):
target_msg = last_msg target_msg = last_msg
# 3. Fallback: Leeres Template, falls keine Edit-Anfrage vorliegt
if not target_msg: if not target_msg:
target_msg = { target_msg = {
"content": "---\ntype: concept\ntitle: Neue Notiz\nstatus: draft\ntags: []\n---\n# Titel\n", "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) render_draft_editor(target_msg)

View File

@ -1,16 +1,17 @@
""" """
FILE: app/routers/ingest.py FILE: app/routers/ingest.py
DESCRIPTION: Endpunkte für WP-11. Nimmt Markdown entgegen, steuert Ingestion und Discovery (Link-Vorschläge). DESCRIPTION: Endpunkte für WP-11. Nimmt Markdown entgegen.
VERSION: 0.6.0 Refactored für WP-14: Nutzt BackgroundTasks für non-blocking Save.
VERSION: 0.7.0 (Fix: Timeout WP-14)
STATUS: Active STATUS: Active
DEPENDENCIES: app.core.ingestion, app.services.discovery, fastapi, pydantic DEPENDENCIES: app.core.ingestion, app.services.discovery, fastapi, pydantic
LAST_ANALYSIS: 2025-12-15
""" """
import os import os
import time import time
import logging import logging
from fastapi import APIRouter, HTTPException import asyncio
from fastapi import APIRouter, HTTPException, BackgroundTasks
from pydantic import BaseModel from pydantic import BaseModel
from typing import Optional, Dict, Any from typing import Optional, Dict, Any
@ -20,7 +21,7 @@ from app.services.discovery import DiscoveryService
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
router = APIRouter() router = APIRouter()
# Services Init (Global oder via Dependency Injection) # Services Init
discovery_service = DiscoveryService() discovery_service = DiscoveryService()
class AnalyzeRequest(BaseModel): class AnalyzeRequest(BaseModel):
@ -36,7 +37,32 @@ class SaveResponse(BaseModel):
status: str status: str
file_path: str file_path: str
note_id: 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") @router.post("/analyze")
async def analyze_draft(req: AnalyzeRequest): 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. WP-11 Intelligence: Liefert Link-Vorschläge via DiscoveryService.
""" """
try: try:
# Hier rufen wir jetzt den verbesserten Service auf
result = await discovery_service.analyze_draft(req.text, req.type) result = await discovery_service.analyze_draft(req.text, req.type)
return result return result
except Exception as e: except Exception as e:
@ -52,9 +77,10 @@ async def analyze_draft(req: AnalyzeRequest):
return {"suggestions": [], "error": str(e)} return {"suggestions": [], "error": str(e)}
@router.post("/save", response_model=SaveResponse) @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: try:
vault_root = os.getenv("MINDNET_VAULT_ROOT", "./vault") vault_root = os.getenv("MINDNET_VAULT_ROOT", "./vault")
@ -65,29 +91,31 @@ async def save_note(req: SaveRequest):
except: pass except: pass
final_filename = req.filename or f"draft_{int(time.time())}.md" final_filename = req.filename or f"draft_{int(time.time())}.md"
ingest_service = IngestionService()
# Async Call # Wir geben sofort eine ID zurück (optimistisch),
result = await ingest_service.create_from_text( # 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, markdown_content=req.markdown_content,
filename=final_filename, filename=final_filename,
vault_root=abs_vault_root, vault_root=abs_vault_root,
folder=req.folder folder=req.folder
) )
if result.get("status") == "error":
raise HTTPException(status_code=500, detail=result.get("error"))
return SaveResponse( return SaveResponse(
status="success", status="queued",
file_path=result.get("path", "unknown"), file_path=os.path.join(req.folder, final_filename),
note_id=result.get("note_id", "unknown"), note_id="pending",
message="Speicherung & KI-Analyse im Hintergrund gestartet.",
stats={ stats={
"chunks": result.get("chunks_count", 0), "chunks": -1, # Indikator für Async
"edges": result.get("edges_count", 0) "edges": -1
} }
) )
except HTTPException as he: raise he
except Exception as e: except Exception as e:
logger.error(f"Save failed: {e}", exc_info=True) logger.error(f"Save dispatch failed: {e}", exc_info=True)
raise HTTPException(status_code=500, detail=f"Save failed: {str(e)}") raise HTTPException(status_code=500, detail=f"Save dispatch failed: {str(e)}")