mindnet/app/frontend/ui.py
2025-12-10 17:43:03 +01:00

304 lines
12 KiB
Python

import streamlit as st
import requests
import uuid
import os
import json
import re
import yaml
from datetime import datetime
from pathlib import Path
from dotenv import load_dotenv
# --- CONFIGURATION ---
load_dotenv()
API_BASE_URL = os.getenv("MINDNET_API_URL", "http://localhost:8002")
CHAT_ENDPOINT = f"{API_BASE_URL}/chat"
FEEDBACK_ENDPOINT = f"{API_BASE_URL}/feedback"
HISTORY_FILE = Path("data/logs/search_history.jsonl")
# Timeout Strategy
timeout_setting = os.getenv("MINDNET_API_TIMEOUT") or os.getenv("MINDNET_LLM_TIMEOUT")
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")
# --- CSS STYLING ---
st.markdown("""
<style>
.block-container { padding-top: 2rem; max_width: 1000px; margin: auto; }
.intent-badge {
background-color: #e8f0fe; color: #1a73e8;
padding: 4px 10px; border-radius: 12px;
font-size: 0.8rem; font-weight: 600;
border: 1px solid #d2e3fc; display: inline-block; margin-bottom: 0.5rem;
}
.draft-box {
border: 1px solid #d0d7de;
border-radius: 6px;
padding: 16px;
background-color: #f6f8fa;
margin-top: 10px;
margin-bottom: 10px;
}
.preview-box {
border: 1px solid #e0e0e0;
border-radius: 6px;
padding: 24px;
background-color: white;
font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif;
}
.debug-info {
font-size: 0.7rem;
color: #888;
margin-bottom: 5px;
}
</style>
""", unsafe_allow_html=True)
# --- SESSION STATE ---
if "messages" not in st.session_state: st.session_state.messages = []
if "user_id" not in st.session_state: st.session_state.user_id = str(uuid.uuid4())
# --- HELPER FUNCTIONS ---
def parse_markdown_draft(full_text):
"""
Versucht extrem tolerant, Frontmatter und Body zu trennen.
"""
clean_text = full_text
# 1. Versuch: Codeblock isolieren
pattern_block = r"```(?:markdown|md)?\s*(.*?)\s*```"
match_block = re.search(pattern_block, full_text, re.DOTALL | re.IGNORECASE)
if match_block:
clean_text = match_block.group(1).strip()
# 2. Versuch: Frontmatter finden (--- YAML ---)
# Verbesserter Regex: Sucht nach dem ersten Vorkommen von --- am Zeilenanfang
pattern_fm = r"(-{3,})\s*(.*?)\s*\1\s*(.*)"
match_fm = re.search(pattern_fm, clean_text, re.DOTALL)
meta = {}
body = clean_text
if match_fm:
yaml_str = match_fm.group(2)
body_content = match_fm.group(3)
try:
parsed = yaml.safe_load(yaml_str)
if isinstance(parsed, dict):
meta = parsed
body = body_content.strip()
except Exception:
pass # YAML kaputt -> alles als Body behandeln
return meta, body
def build_markdown_doc(meta, body):
if "id" not in meta or meta["id"] == "generated_on_save":
meta["id"] = f"{datetime.now().strftime('%Y%m%d')}-{meta.get('type', 'note')}-{uuid.uuid4().hex[:6]}"
meta["updated"] = datetime.now().strftime("%Y-%m-%d")
try:
yaml_str = yaml.dump(meta, default_flow_style=None, sort_keys=False, allow_unicode=True).strip()
except:
yaml_str = "error: generating_yaml"
return f"---\n{yaml_str}\n---\n\n{body}"
def load_history_from_logs(limit=10):
queries = []
if HISTORY_FILE.exists():
try:
with open(HISTORY_FILE, "r", encoding="utf-8") as f:
lines = f.readlines()
for line in reversed(lines):
try:
entry = json.loads(line)
q = entry.get("query_text")
if q and q not in queries:
queries.append(q)
if len(queries) >= limit: break
except: continue
except: pass
return queries
def send_chat_message(message: str, top_k: int, explain: bool):
try:
response = requests.post(
CHAT_ENDPOINT,
json={"message": message, "top_k": top_k, "explain": explain},
timeout=API_TIMEOUT
)
response.raise_for_status()
return response.json()
except Exception as e:
return {"error": str(e)}
def submit_feedback(query_id, node_id, score, comment=None):
try:
requests.post(FEEDBACK_ENDPOINT, json={"query_id": query_id, "node_id": node_id, "score": score, "comment": comment}, timeout=2)
st.toast(f"Feedback ({score}) gesendet!")
except: pass
# --- UI COMPONENTS ---
def render_sidebar():
with st.sidebar:
st.title("🧠 mindnet")
st.caption("DEBUG MODE ACTIVATED")
mode = st.radio("Modus", ["💬 Chat", "📝 Manueller Editor"], index=0)
st.divider()
st.subheader("⚙️ Settings")
top_k = st.slider("Quellen (Top-K)", 1, 10, 5)
explain = st.toggle("Explanation Layer", True)
st.divider()
st.subheader("🕒 Verlauf")
for q in load_history_from_logs(8):
if st.button(f"🔎 {q[:25]}...", key=f"hist_{q}", use_container_width=True):
st.session_state.messages.append({"role": "user", "content": q})
st.rerun()
return mode, top_k, explain
def render_draft_editor(msg):
qid = msg.get('query_id', str(uuid.uuid4()))
key_base = f"draft_{qid}"
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")
tags_raw = meta.get("tags", [])
st.session_state[f"{key_base}_tags"] = ", ".join(tags_raw) if isinstance(tags_raw, list) else str(tags_raw)
st.session_state[f"{key_base}_body"] = body.strip()
st.session_state[f"{key_base}_init"] = True
st.markdown(f'<div class="draft-box">', unsafe_allow_html=True)
st.markdown("### 📝 Entwurf bearbeiten")
# Metadata Controls
c1, c2 = st.columns([1, 2])
with c1:
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")
with c2:
new_tags = st.text_input("Tags", value=st.session_state.get(f"{key_base}_tags", ""), key=f"{key_base}_inp_tags")
# Editor / Preview Tabs
tab_edit, tab_view = st.tabs(["✏️ Editor", "👁️ Vorschau"])
with tab_edit:
new_body = st.text_area(
"Inhalt",
value=st.session_state.get(f"{key_base}_body", ""),
height=500,
key=f"{key_base}_txt_body",
label_visibility="collapsed"
)
# Live Reassembly
final_tags_list = [t.strip() for t in new_tags.split(",") if t.strip()]
final_meta = {"id": "generated_on_save", "type": new_type, "status": "draft", "tags": final_tags_list}
final_doc = build_markdown_doc(final_meta, new_body)
with tab_view:
st.markdown('<div class="preview-box">', unsafe_allow_html=True)
st.markdown(final_doc)
st.markdown('</div>', unsafe_allow_html=True)
st.markdown("---")
# Actions
b1, b2 = st.columns([1, 1])
with b1:
st.download_button("💾 Download .md", data=final_doc, file_name=f"draft_{new_type}.md", mime="text/markdown")
with b2:
if st.button("📋 Code Copy", key=f"{key_base}_btn_copy"):
st.code(final_doc, language="markdown")
st.markdown("</div>", unsafe_allow_html=True)
def render_chat_interface(top_k, explain):
for idx, msg in enumerate(st.session_state.messages):
with st.chat_message(msg["role"]):
if msg["role"] == "assistant":
# Meta Info
intent = msg.get("intent", "UNKNOWN")
src = msg.get("intent_source", "?")
icon = {"EMPATHY":"❤️", "DECISION":"⚖️", "CODING":"💻", "FACT":"📚", "INTERVIEW":"📝"}.get(intent, "🧠")
st.markdown(f'<div class="intent-badge">{icon} Intent: {intent} <span style="opacity:0.6; font-size:0.8em">({src})</span></div>', unsafe_allow_html=True)
# --- WICHTIG: DEBUGGING JETZT GANZ OBEN ---
with st.expander("🐞 Debug Raw Payload", expanded=False):
st.text("Hier siehst du, was das Backend wirklich geschickt hat:")
st.json(msg)
# --- CONTENT LOGIC ---
if intent == "INTERVIEW":
render_draft_editor(msg)
else:
st.markdown(msg["content"])
# Sources & Feedback
if "sources" in msg and msg["sources"]:
for hit in msg["sources"]:
with st.expander(f"📄 {hit.get('note_id', '?')} ({hit.get('total_score', 0):.2f})"):
st.markdown(f"_{hit.get('source', {}).get('text', '')[:300]}..._")
if hit.get('explanation'):
st.caption(f"Grund: {hit['explanation']['reasons'][0]['message']}")
def _cb(qid=msg.get("query_id"), nid=hit.get('node_id')):
val = st.session_state.get(f"fb_src_{qid}_{nid}")
if val is not None: submit_feedback(qid, nid, val+1)
st.feedback("faces", key=f"fb_src_{msg.get('query_id')}_{hit.get('node_id')}", on_change=_cb)
if "query_id" in msg:
qid = msg["query_id"]
st.feedback("stars", key=f"fb_glob_{qid}", on_change=lambda: submit_feedback(qid, "generated_answer", st.session_state[f"fb_glob_{qid}"]+1))
else:
st.markdown(msg["content"])
# Input Logic
if prompt := st.chat_input("Frage Mindnet..."):
st.session_state.messages.append({"role": "user", "content": prompt})
st.rerun()
if len(st.session_state.messages) > 0 and st.session_state.messages[-1]["role"] == "user":
with st.chat_message("assistant"):
with st.spinner("Thinking..."):
resp = send_chat_message(st.session_state.messages[-1]["content"], top_k, explain)
if "error" in resp:
st.error(resp["error"])
else:
st.session_state.messages.append({
"role": "assistant",
"content": resp.get("answer"),
"intent": resp.get("intent", "FACT"),
"intent_source": resp.get("intent_source", "Unknown"),
"sources": resp.get("sources", []),
"query_id": resp.get("query_id")
})
st.rerun()
def render_manual_editor():
st.header("📝 Manueller Editor")
c1, c2 = st.columns([1, 2])
n_type = c1.selectbox("Typ", ["concept", "project", "decision", "experience", "value", "goal"])
tags = c2.text_input("Tags")
body = st.text_area("Inhalt", height=400, placeholder="# Titel\n\nText...")
if st.button("Code anzeigen"):
meta = {"type": n_type, "status": "draft", "tags": [t.strip() for t in tags.split(",")]}
st.code(build_markdown_doc(meta, body), language="markdown")
mode, top_k, explain = render_sidebar()
if mode == "💬 Chat":
render_chat_interface(top_k, explain)
else:
render_manual_editor()