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", {})
)