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
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'<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"
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)

View File

@ -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)}")
logger.error(f"Save dispatch failed: {e}", exc_info=True)
raise HTTPException(status_code=500, detail=f"Save dispatch failed: {str(e)}")