diff --git a/app/frontend/ui.py b/app/frontend/ui.py
index 88dbf80..20bb3f9 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.6", page_icon="🧠", layout="wide")
+st.set_page_config(page_title="mindnet v2.3.7", page_icon="🧠", layout="wide")
# --- CSS STYLING ---
st.markdown("""
@@ -112,7 +112,6 @@ def normalize_meta_and_body(meta, body):
def parse_markdown_draft(full_text):
"""Robustes Parsing + Sanitization."""
clean_text = full_text
-
pattern_block = r"```(?:markdown|md)?\s*(.*?)\s*```"
match_block = re.search(pattern_block, full_text, re.DOTALL | re.IGNORECASE)
if match_block:
@@ -189,7 +188,6 @@ def send_chat_message(message: str, top_k: int, explain: bool):
return {"error": str(e)}
def analyze_draft_text(text: str, n_type: str):
- """Ruft den neuen Intelligence-Service (WP-11) auf."""
try:
response = requests.post(
INGEST_ANALYZE_ENDPOINT,
@@ -202,12 +200,11 @@ def analyze_draft_text(text: str, n_type: str):
return {"error": str(e)}
def save_draft_to_vault(markdown_content: str, filename: str = None):
- """Ruft den neuen Persistence-Service (WP-11) auf."""
try:
response = requests.post(
INGEST_SAVE_ENDPOINT,
json={"markdown_content": markdown_content, "filename": filename},
- timeout=60 # Indizierung kann dauern
+ timeout=60
)
response.raise_for_status()
return response.json()
@@ -225,7 +222,7 @@ def submit_feedback(query_id, node_id, score, comment=None):
def render_sidebar():
with st.sidebar:
st.title("🧠 mindnet")
- st.caption("v2.3.6 | WP-10b (Full)")
+ st.caption("v2.3.7 | Stable State")
mode = st.radio("Modus", ["💬 Chat", "📝 Manueller Editor"], index=0)
st.divider()
st.subheader("⚙️ Settings")
@@ -242,96 +239,119 @@ def render_sidebar():
def render_draft_editor(msg):
qid = msg.get('query_id', str(uuid.uuid4()))
key_base = f"draft_{qid}"
- body_key = f"{key_base}_txt_body"
- # --- CALLBACKS (Lösung für den State-Error) ---
- def _append_text(k, text):
- current = st.session_state.get(k, "")
- st.session_state[k] = f"{current}\n\n{text}"
- # Sync auch den generischen Key
- st.session_state[f"{key_base}_body"] = st.session_state[k]
+ # === STATE MANAGEMENT KEYS ===
+ # Wir nutzen getrennte Keys für Widget und Daten, um Streamlit's Bereinigung zu umgehen.
+ # Persistent Keys (bleiben erhalten auch beim Tab-Wechsel)
+ data_body_key = f"{key_base}_data_body"
+ data_meta_key = f"{key_base}_data_meta"
+ data_sugg_key = f"{key_base}_data_suggestions"
+
+ # Widget Keys (können sich ändern/neu gezeichnet werden)
+ widget_body_key = f"{key_base}_widget_body"
- def _remove_text(k, text):
- current = st.session_state.get(k, "")
- # Einfaches Replace (könnte man robuster machen)
- st.session_state[k] = current.replace(text, "").strip()
- st.session_state[f"{key_base}_body"] = st.session_state[k]
-
- # 1. Init (Nur beim allerersten Laden)
+ # --- 1. INIT STATE (Einmalig pro Nachricht) ---
if f"{key_base}_init" not in st.session_state:
meta, body = parse_markdown_draft(msg["content"])
- st.session_state[f"{key_base}_type"] = meta.get("type", "default")
- st.session_state[f"{key_base}_title"] = meta.get("title", "")
- tags_raw = meta.get("tags", [])
- st.session_state[f"{key_base}_tags"] = ", ".join(tags_raw) if isinstance(tags_raw, list) else str(tags_raw)
+ # Defaults setzen
+ if "type" not in meta: meta["type"] = "default"
+ if "title" not in meta: meta["title"] = ""
+ if "tags" not in meta: meta["tags"] = []
- # Initialisiere beide Keys
- st.session_state[body_key] = body.strip()
- st.session_state[f"{key_base}_body"] = body.strip()
-
- st.session_state[f"{key_base}_meta"] = meta
- st.session_state[f"{key_base}_suggestions"] = []
+ # Tags Listen-Check
+ if isinstance(meta["tags"], list):
+ meta["tags_str"] = ", ".join(meta["tags"])
+ else:
+ meta["tags_str"] = str(meta.get("tags", ""))
+
+ # Persistent speichern
+ st.session_state[data_meta_key] = meta
+ st.session_state[data_body_key] = body.strip()
+ st.session_state[data_sugg_key] = []
st.session_state[f"{key_base}_init"] = True
- # 2. UI Layout
+ # --- HELPER CALLBACKS ---
+ # Sync Widget -> Data
+ def _sync_body():
+ st.session_state[data_body_key] = st.session_state[widget_body_key]
+
+ # Insert Text (Daten ändern)
+ def _insert_text(text_to_insert):
+ current = st.session_state[data_body_key]
+ st.session_state[data_body_key] = f"{current}\n\n{text_to_insert}"
+
+ def _remove_text(text_to_remove):
+ current = st.session_state[data_body_key]
+ st.session_state[data_body_key] = current.replace(text_to_remove, "").strip()
+
+ # --- 2. UI LAYOUT ---
st.markdown(f'
', unsafe_allow_html=True)
st.markdown("### 📝 Entwurf bearbeiten")
- # Metadata
+ # Load Data Reference
+ meta_ref = st.session_state[data_meta_key]
+
+ # Metadata Form
c1, c2 = st.columns([2, 1])
with c1:
- new_title = st.text_input("Titel", key=f"{key_base}_inp_title", value=st.session_state.get(f"{key_base}_title", ""))
+ new_title = st.text_input("Titel", key=f"{key_base}_wdg_title", value=meta_ref["title"])
+ meta_ref["title"] = new_title # Direct Sync
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")
+ curr_type = meta_ref["type"]
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_type = st.selectbox("Typ", known_types, index=known_types.index(curr_type), key=f"{key_base}_wdg_type")
+ meta_ref["type"] = new_type # Direct Sync
- new_tags = st.text_input("Tags (kommagetrennt)", key=f"{key_base}_inp_tags", value=st.session_state.get(f"{key_base}_tags", ""))
+ new_tags = st.text_input("Tags", key=f"{key_base}_wdg_tags", value=meta_ref.get("tags_str", ""))
+ meta_ref["tags_str"] = new_tags # Direct Sync
# Tabs
tab_edit, tab_intel, tab_view = st.tabs(["✏️ Inhalt", "🧠 Intelligence", "👁️ Vorschau"])
# --- TAB 1: EDITOR ---
with tab_edit:
- # Das Widget rendert HIER. Änderungen am State müssen VORHER (via Callback) passieren.
- current_body = st.text_area(
+ # Hier ist der Trick: Value kommt aus 'data_body_key',
+ # Änderungen triggern '_sync_body', der zurück in 'data_body_key' schreibt.
+ st.text_area(
"Body",
- key=body_key,
+ key=widget_body_key,
+ value=st.session_state[data_body_key],
+ on_change=_sync_body,
height=500,
label_visibility="collapsed"
)
- # Sync manueller Änderungen in den generischen Key
- 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"):
+ # 1. Alte Ergebnisse löschen für Feedback
+ st.session_state[data_sugg_key] = []
+
with st.spinner("Analysiere..."):
- text_to_analyze = st.session_state[body_key]
- analysis = analyze_draft_text(text_to_analyze, new_type)
+ # Aktuellen Text nehmen
+ text_to_analyze = st.session_state[data_body_key]
+ analysis = analyze_draft_text(text_to_analyze, meta_ref["type"])
if "error" in analysis:
st.error(f"Fehler: {analysis['error']}")
else:
suggestions = analysis.get("suggestions", [])
- st.session_state[f"{key_base}_suggestions"] = suggestions
+ st.session_state[data_sugg_key] = suggestions
if not suggestions:
st.warning("Keine Vorschläge gefunden.")
+ else:
+ st.success(f"{len(suggestions)} Vorschläge gefunden.")
- suggestions = st.session_state.get(f"{key_base}_suggestions", [])
+ suggestions = st.session_state[data_sugg_key]
if suggestions:
- st.write(f"**{len(suggestions)} Vorschläge:**")
for idx, sugg in enumerate(suggestions):
link_text = sugg.get('suggested_markdown', '')
+ is_inserted = link_text in st.session_state[data_body_key]
- # Prüfe ob Text vorhanden (Case Insensitive Check wäre besser, hier simpel)
- is_inserted = link_text in st.session_state[body_key]
-
- # 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"
@@ -343,35 +363,22 @@ def render_draft_editor(msg):
""", unsafe_allow_html=True)
- # Button Logik mit CALLBACKS (on_click)
if is_inserted:
- st.button(
- f"❌ Entfernen",
- key=f"del_{idx}_{key_base}",
- on_click=_remove_text, # Callback
- args=(body_key, link_text) # Argumente für Callback
- )
+ st.button("❌ Entfernen", key=f"del_{idx}_{key_base}", on_click=_remove_text, args=(link_text,))
else:
- st.button(
- f"➕ Einfügen",
- key=f"add_{idx}_{key_base}",
- on_click=_append_text, # Callback
- args=(body_key, link_text) # Argumente für Callback
- )
+ st.button("➕ Einfügen", key=f"add_{idx}_{key_base}", on_click=_insert_text, args=(link_text,))
# --- TAB 3: PREVIEW & SAVE ---
- final_tags_list = [t.strip() for t in new_tags.split(",") if t.strip()]
+ # Final Assembly
+ final_tags_list = [t.strip() for t in meta_ref["tags_str"].split(",") if t.strip()]
final_meta = {
"id": "generated_on_save",
- "type": new_type,
- "title": new_title,
+ "type": meta_ref["type"],
+ "title": meta_ref["title"],
"status": "draft",
"tags": final_tags_list
}
-
- # Nimm immer den aktuellsten Text aus dem Widget-State
- final_body_content = st.session_state[body_key]
- final_doc = build_markdown_doc(final_meta, final_body_content)
+ final_doc = build_markdown_doc(final_meta, st.session_state[data_body_key])
with tab_view:
st.markdown('', unsafe_allow_html=True)
@@ -380,18 +387,15 @@ def render_draft_editor(msg):
st.markdown("---")
- # Save Action
b1, b2 = st.columns([1, 1])
with b1:
if st.button("💾 Speichern & Indizieren", type="primary", key=f"{key_base}_save"):
with st.spinner("Speichere im Vault..."):
- safe_title = re.sub(r'[^a-zA-Z0-9]', '-', new_title).lower()[:30]
+ safe_title = re.sub(r'[^a-zA-Z0-9]', '-', meta_ref["title"]).lower()[:30]
if not safe_title: safe_title = "draft"
fname = f"{datetime.now().strftime('%Y%m%d')}-{safe_title}.md"
- # Speichern mit aktuellstem Inhalt
result = save_draft_to_vault(final_doc, filename=fname)
-
if "error" in result:
st.error(f"Fehler: {result['error']}")
else:
@@ -463,11 +467,11 @@ def render_chat_interface(top_k, explain):
st.rerun()
def render_manual_editor():
- # Wir nutzen eine Fake-Message, um die render_draft_editor Logik wiederzuverwenden
- # Aber mit leeren Defaults
+ # Wir nutzen dieselbe Logik wie beim Interview, aber mit einem "leeren" Mock-Objekt
+ # Wichtig: Feste Query-ID für Manuellen Modus, damit der State persistent bleibt
mock_msg = {
- "content": "---\ntype: default\nstatus: draft\ntitle: Neue Notiz\ntags: []\n---\n# Titel\n",
- "query_id": "manual_mode_v2" # Feste ID für manuellen Modus
+ "content": "---\ntype: concept\ntitle: Neue Notiz\nstatus: draft\ntags: []\n---\n# Titel\n",
+ "query_id": "manual_editor_fixed_v1"
}
render_draft_editor(mock_msg)