WP19 #10
|
|
@ -1,55 +1,37 @@
|
|||
import streamlit as st
|
||||
import uuid
|
||||
|
||||
# --- CONFIG & STYLING ---
|
||||
st.set_page_config(page_title="mindnet v2.6", page_icon="🧠", layout="wide")
|
||||
st.markdown("""
|
||||
<style>
|
||||
.block-container { padding-top: 2rem; max_width: 1200px; 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: 10px 0; }
|
||||
.preview-box { border: 1px solid #e0e0e0; border-radius: 6px; padding: 24px; background-color: white; }
|
||||
</style>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
# --- MODULE IMPORTS ---
|
||||
try:
|
||||
from ui_config import QDRANT_URL, QDRANT_KEY, COLLECTION_PREFIX
|
||||
from ui_graph_service import GraphExplorerService
|
||||
from ui_components import render_sidebar, render_chat_interface, render_manual_editor, render_graph_explorer
|
||||
|
||||
# Neue modulare Komponenten
|
||||
from ui_sidebar import render_sidebar
|
||||
from ui_chat import render_chat_interface
|
||||
from ui_editor import render_manual_editor
|
||||
from ui_graph import render_graph_explorer
|
||||
|
||||
except ImportError as e:
|
||||
st.error(f"Import Error: {e}. Bitte stelle sicher, dass alle UI-Dateien im selben Ordner liegen.")
|
||||
st.error(f"Import Error: {e}. Bitte stelle sicher, dass alle UI-Dateien im Ordner liegen.")
|
||||
st.stop()
|
||||
|
||||
# --- PAGE SETUP ---
|
||||
st.set_page_config(page_title="mindnet v2.6", page_icon="🧠", layout="wide")
|
||||
|
||||
# --- CSS STYLING ---
|
||||
st.markdown("""
|
||||
<style>
|
||||
.block-container { padding-top: 2rem; max_width: 1200px; 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;
|
||||
}
|
||||
</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())
|
||||
|
||||
# --- SERVICE INIT ---
|
||||
# Initialisiert den Graph Service einmalig
|
||||
graph_service = GraphExplorerService(QDRANT_URL, QDRANT_KEY, COLLECTION_PREFIX)
|
||||
|
||||
# --- MAIN ROUTING ---
|
||||
|
|
|
|||
37
app/frontend/ui_callbacks.py
Normal file
37
app/frontend/ui_callbacks.py
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import streamlit as st
|
||||
from ui_utils import build_markdown_doc
|
||||
|
||||
def switch_to_editor_callback(note_payload):
|
||||
"""
|
||||
Callback für den 'Bearbeiten'-Button im Graphen.
|
||||
Bereitet den Session-State vor, damit der Editor im Update-Modus startet.
|
||||
"""
|
||||
# 1. Inhalt extrahieren (Fulltext bevorzugt, sonst Fallback)
|
||||
content = note_payload.get('fulltext', '')
|
||||
if not content:
|
||||
content = build_markdown_doc(note_payload, "Inhalt konnte nicht geladen werden (nur Metadaten verfügbar).")
|
||||
|
||||
# 2. Single Source of Truth: 'path' Feld (Absoluter Pfad)
|
||||
origin_fname = note_payload.get('path')
|
||||
|
||||
# Fallback: Falls 'path' leer ist (Legacy Daten)
|
||||
if not origin_fname:
|
||||
origin_fname = note_payload.get('file_path') or note_payload.get('filename')
|
||||
|
||||
# Notfall-Fallback: Konstruktion aus ID
|
||||
if not origin_fname and 'note_id' in note_payload:
|
||||
origin_fname = f"{note_payload['note_id']}.md"
|
||||
|
||||
# 3. Message in den Chat-Verlauf injecten
|
||||
# Diese Nachricht dient als Datencontainer für den Editor im "Manuellen Modus"
|
||||
st.session_state.messages.append({
|
||||
"role": "assistant",
|
||||
"intent": "INTERVIEW",
|
||||
"content": content,
|
||||
"query_id": f"edit_{note_payload['note_id']}",
|
||||
"origin_filename": origin_fname,
|
||||
"origin_note_id": note_payload['note_id']
|
||||
})
|
||||
|
||||
# 4. Modus umschalten (erzwingt Wechsel zum Editor-Tab beim nächsten Re-Run)
|
||||
st.session_state["sidebar_mode_selection"] = "📝 Manueller Editor"
|
||||
56
app/frontend/ui_chat.py
Normal file
56
app/frontend/ui_chat.py
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
import streamlit as st
|
||||
from ui_api import send_chat_message, submit_feedback
|
||||
from ui_editor import render_draft_editor
|
||||
|
||||
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":
|
||||
intent = msg.get("intent", "UNKNOWN")
|
||||
st.markdown(f'<div class="intent-badge">Intent: {intent}</div>', unsafe_allow_html=True)
|
||||
|
||||
with st.expander("🐞 Payload", expanded=False):
|
||||
st.json(msg)
|
||||
|
||||
if intent == "INTERVIEW":
|
||||
render_draft_editor(msg)
|
||||
else:
|
||||
st.markdown(msg["content"])
|
||||
|
||||
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"])
|
||||
|
||||
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"),
|
||||
"sources": resp.get("sources", []),
|
||||
"query_id": resp.get("query_id")
|
||||
})
|
||||
st.rerun()
|
||||
|
|
@ -1,519 +0,0 @@
|
|||
import streamlit as st
|
||||
import uuid
|
||||
import re
|
||||
from datetime import datetime
|
||||
from streamlit_agraph import agraph, Config
|
||||
from qdrant_client import models
|
||||
|
||||
# Importe aus den anderen Modulen
|
||||
from ui_utils import parse_markdown_draft, build_markdown_doc, load_history_from_logs, slugify
|
||||
from ui_api import save_draft_to_vault, analyze_draft_text, send_chat_message, submit_feedback
|
||||
from ui_config import HISTORY_FILE, COLLECTION_PREFIX, GRAPH_COLORS
|
||||
|
||||
# --- CALLBACKS ---
|
||||
# Diese müssen zwingend VOR dem Aufruf definiert sein.
|
||||
|
||||
def switch_to_editor_callback(note_payload):
|
||||
"""
|
||||
Callback für den Edit-Button im Graphen.
|
||||
Lädt die Notiz in den Editor und setzt den Modus.
|
||||
Nutzt den 'path'-Parameter aus Qdrant als Single Source of Truth für Updates.
|
||||
"""
|
||||
# 1. Inhalt extrahieren (Fulltext oder Fallback)
|
||||
content = note_payload.get('fulltext', '')
|
||||
if not content:
|
||||
content = build_markdown_doc(note_payload, "Inhalt konnte nicht geladen werden (nur Metadaten verfügbar).")
|
||||
|
||||
# 2. Single Source of Truth Bestimmung (Dateipfad)
|
||||
# Priorität 1: Der absolute Pfad aus dem Ingest-Prozess ('path')
|
||||
origin_fname = note_payload.get('path')
|
||||
|
||||
# Priorität 2: 'file_path' oder 'filename' (Legacy Felder, Fallback)
|
||||
if not origin_fname:
|
||||
origin_fname = note_payload.get('file_path') or note_payload.get('filename')
|
||||
|
||||
# Priorität 3: Konstruktion aus ID (Notlösung, falls Metadaten unvollständig)
|
||||
if not origin_fname and 'note_id' in note_payload:
|
||||
# Annahme: Datei heißt {note_id}.md im Vault Root
|
||||
origin_fname = f"{note_payload['note_id']}.md"
|
||||
|
||||
# 3. Message in den Chat-Verlauf injecten (dient als Datencontainer für den Editor)
|
||||
# Wir fügen eine "künstliche" Assistant-Nachricht hinzu, die der Editor dann ausliest.
|
||||
st.session_state.messages.append({
|
||||
"role": "assistant",
|
||||
"intent": "INTERVIEW",
|
||||
"content": content,
|
||||
"query_id": f"edit_{note_payload['note_id']}",
|
||||
"origin_filename": origin_fname, # Pfad für Speicher-Logik
|
||||
"origin_note_id": note_payload['note_id']
|
||||
})
|
||||
|
||||
# 4. Modus umschalten
|
||||
# Wir setzen den Key des Radio-Buttons im Session State
|
||||
st.session_state["sidebar_mode_selection"] = "📝 Manueller Editor"
|
||||
|
||||
# --- UI RENDERER ---
|
||||
|
||||
def render_sidebar():
|
||||
"""
|
||||
Rendert die Seitenleiste mit Navigation und Einstellungen.
|
||||
"""
|
||||
with st.sidebar:
|
||||
st.title("🧠 mindnet")
|
||||
st.caption("v2.6 | WP-19 Graph View")
|
||||
|
||||
# Modus-Auswahl mit State-Key für programmatische Umschaltung
|
||||
if "sidebar_mode_selection" not in st.session_state:
|
||||
st.session_state["sidebar_mode_selection"] = "💬 Chat"
|
||||
|
||||
mode = st.radio(
|
||||
"Modus",
|
||||
["💬 Chat", "📝 Manueller Editor", "🕸️ Graph Explorer"],
|
||||
key="sidebar_mode_selection"
|
||||
)
|
||||
|
||||
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")
|
||||
# Suchhistorie laden
|
||||
for q in load_history_from_logs(HISTORY_FILE, 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):
|
||||
"""
|
||||
Der Markdown-Editor.
|
||||
Wird für neue Entwürfe UND für das Bearbeiten bestehender Notizen genutzt.
|
||||
"""
|
||||
# ID Generierung für Unique Keys
|
||||
if "query_id" not in msg or not msg["query_id"]:
|
||||
msg["query_id"] = str(uuid.uuid4())
|
||||
|
||||
qid = msg["query_id"]
|
||||
key_base = f"draft_{qid}"
|
||||
|
||||
# State Keys definieren
|
||||
data_meta_key = f"{key_base}_data_meta"
|
||||
data_sugg_key = f"{key_base}_data_suggestions"
|
||||
widget_body_key = f"{key_base}_widget_body"
|
||||
data_body_key = f"{key_base}_data_body"
|
||||
|
||||
# --- INITIALISIERUNG ---
|
||||
if f"{key_base}_init" not in st.session_state:
|
||||
# Markdown parsen
|
||||
meta, body = parse_markdown_draft(msg["content"])
|
||||
if "type" not in meta: meta["type"] = "default"
|
||||
if "title" not in meta: meta["title"] = ""
|
||||
tags = meta.get("tags", [])
|
||||
meta["tags_str"] = ", ".join(tags) if isinstance(tags, list) else str(tags)
|
||||
|
||||
# Daten in Session State schreiben
|
||||
st.session_state[data_meta_key] = meta
|
||||
st.session_state[data_sugg_key] = []
|
||||
st.session_state[data_body_key] = body.strip()
|
||||
|
||||
# Widget-Werte vorbelegen
|
||||
st.session_state[f"{key_base}_wdg_title"] = meta["title"]
|
||||
st.session_state[f"{key_base}_wdg_type"] = meta["type"]
|
||||
st.session_state[f"{key_base}_wdg_tags"] = meta["tags_str"]
|
||||
|
||||
# WICHTIG: Original-Pfad aus der Message übernehmen (für Update-Logik)
|
||||
st.session_state[f"{key_base}_origin_filename"] = msg.get("origin_filename")
|
||||
|
||||
st.session_state[f"{key_base}_init"] = True
|
||||
|
||||
# --- STATE RESURRECTION (falls Streamlit rerunt) ---
|
||||
if widget_body_key not in st.session_state and data_body_key in st.session_state:
|
||||
st.session_state[widget_body_key] = st.session_state[data_body_key]
|
||||
|
||||
# --- SYNC FUNKTIONEN ---
|
||||
def _sync_meta():
|
||||
meta = st.session_state[data_meta_key]
|
||||
meta["title"] = st.session_state.get(f"{key_base}_wdg_title", "")
|
||||
meta["type"] = st.session_state.get(f"{key_base}_wdg_type", "default")
|
||||
meta["tags_str"] = st.session_state.get(f"{key_base}_wdg_tags", "")
|
||||
st.session_state[data_meta_key] = meta
|
||||
|
||||
def _sync_body():
|
||||
st.session_state[data_body_key] = st.session_state[widget_body_key]
|
||||
|
||||
def _insert_text(text_to_insert):
|
||||
current = st.session_state.get(widget_body_key, "")
|
||||
new_text = f"{current}\n\n{text_to_insert}"
|
||||
st.session_state[widget_body_key] = new_text
|
||||
st.session_state[data_body_key] = new_text
|
||||
|
||||
def _remove_text(text_to_remove):
|
||||
current = st.session_state.get(widget_body_key, "")
|
||||
new_text = current.replace(text_to_remove, "").strip()
|
||||
st.session_state[widget_body_key] = new_text
|
||||
st.session_state[data_body_key] = new_text
|
||||
|
||||
# --- LAYOUT HEADER ---
|
||||
origin_fname = st.session_state.get(f"{key_base}_origin_filename")
|
||||
|
||||
if origin_fname:
|
||||
# Update Modus
|
||||
display_name = str(origin_fname).split("/")[-1] # Nur Dateiname anzeigen
|
||||
st.success(f"📂 **Datei-Modus**: `{origin_fname}`") # Voller Pfad zur Sicherheit
|
||||
st.markdown(f'<div class="draft-box" style="border-left: 5px solid #ff9f43;">', unsafe_allow_html=True)
|
||||
else:
|
||||
# Create Modus
|
||||
st.info("✨ **Erstell-Modus**: Neue Datei wird angelegt.")
|
||||
st.markdown(f'<div class="draft-box">', unsafe_allow_html=True)
|
||||
|
||||
st.markdown("### Editor")
|
||||
|
||||
# Metadaten-Editor
|
||||
meta_ref = st.session_state[data_meta_key]
|
||||
c1, c2 = st.columns([2, 1])
|
||||
with c1:
|
||||
st.text_input("Titel", key=f"{key_base}_wdg_title", on_change=_sync_meta)
|
||||
with c2:
|
||||
known_types = ["concept", "project", "decision", "experience", "journal", "value", "goal", "principle", "risk", "belief"]
|
||||
curr_type = st.session_state.get(f"{key_base}_wdg_type", meta_ref["type"])
|
||||
if curr_type not in known_types: known_types.append(curr_type)
|
||||
st.selectbox("Typ", known_types, key=f"{key_base}_wdg_type", on_change=_sync_meta)
|
||||
|
||||
st.text_input("Tags", key=f"{key_base}_wdg_tags", on_change=_sync_meta)
|
||||
|
||||
# Tabs
|
||||
tab_edit, tab_intel, tab_view = st.tabs(["✏️ Inhalt", "🧠 Intelligence", "👁️ Vorschau"])
|
||||
|
||||
with tab_edit:
|
||||
st.text_area("Body", key=widget_body_key, height=600, on_change=_sync_body, label_visibility="collapsed")
|
||||
|
||||
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"):
|
||||
st.session_state[data_sugg_key] = []
|
||||
text_to_analyze = st.session_state.get(widget_body_key, st.session_state.get(data_body_key, ""))
|
||||
current_doc_type = st.session_state.get(f"{key_base}_wdg_type", "concept")
|
||||
|
||||
with st.spinner("Analysiere..."):
|
||||
analysis = analyze_draft_text(text_to_analyze, current_doc_type)
|
||||
if "error" in analysis:
|
||||
st.error(f"Fehler: {analysis['error']}")
|
||||
else:
|
||||
suggestions = analysis.get("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.")
|
||||
|
||||
# Vorschläge rendern
|
||||
suggestions = st.session_state[data_sugg_key]
|
||||
if suggestions:
|
||||
current_text_state = st.session_state.get(widget_body_key, "")
|
||||
for idx, sugg in enumerate(suggestions):
|
||||
link_text = sugg.get('suggested_markdown', '')
|
||||
is_inserted = link_text in current_text_state
|
||||
|
||||
bg_color = "#e6fffa" if is_inserted else "#ffffff"
|
||||
border = "3px solid #28a745" if is_inserted else "3px solid #1a73e8"
|
||||
|
||||
st.markdown(f"""
|
||||
<div style="border-left: {border}; background-color: {bg_color}; padding: 10px; margin-bottom: 8px; border-radius: 4px; box-shadow: 0 1px 3px rgba(0,0,0,0.1);">
|
||||
<b>{sugg.get('target_title')}</b> <small>({sugg.get('type')})</small><br>
|
||||
<i>{sugg.get('reason')}</i><br>
|
||||
<code>{link_text}</code>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
if is_inserted:
|
||||
st.button("❌ Entfernen", key=f"del_{idx}_{key_base}", on_click=_remove_text, args=(link_text,))
|
||||
else:
|
||||
st.button("➕ Einfügen", key=f"add_{idx}_{key_base}", on_click=_insert_text, args=(link_text,))
|
||||
|
||||
# Dokument für Vorschau/Save bauen
|
||||
final_tags_str = st.session_state.get(f"{key_base}_wdg_tags", "")
|
||||
final_tags = [t.strip() for t in final_tags_str.split(",") if t.strip()]
|
||||
|
||||
final_meta = {
|
||||
"id": "generated_on_save",
|
||||
"type": st.session_state.get(f"{key_base}_wdg_type", "default"),
|
||||
"title": st.session_state.get(f"{key_base}_wdg_title", "").strip(),
|
||||
"status": "draft",
|
||||
"tags": final_tags
|
||||
}
|
||||
|
||||
# ID behalten wenn vorhanden (Wichtig für Source of Truth)
|
||||
if "origin_note_id" in msg:
|
||||
final_meta["id"] = msg["origin_note_id"]
|
||||
|
||||
final_body = st.session_state.get(widget_body_key, st.session_state[data_body_key])
|
||||
|
||||
# Fallback Titel aus H1
|
||||
if not final_meta["title"]:
|
||||
h1_match = re.search(r"^#\s+(.*)$", final_body, re.MULTILINE)
|
||||
if h1_match: final_meta["title"] = h1_match.group(1).strip()
|
||||
|
||||
final_doc = build_markdown_doc(final_meta, final_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("---")
|
||||
|
||||
# Footer Actions
|
||||
b1, b2 = st.columns([1, 1])
|
||||
with b1:
|
||||
# Label dynamisch
|
||||
save_label = "💾 Speichern (Überschreiben)" 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..."):
|
||||
|
||||
# ENTSCHEIDUNG: Update oder Neu?
|
||||
if origin_fname:
|
||||
# UPDATE: Wir nutzen den exakten Pfad aus Qdrant
|
||||
target_filename = origin_fname
|
||||
else:
|
||||
# NEU: Wir generieren einen Namen
|
||||
raw_title = final_meta.get("title", "draft")
|
||||
target_file = f"{datetime.now().strftime('%Y%m%d')}-{slugify(raw_title)[:60]}.md"
|
||||
target_filename = target_file
|
||||
|
||||
# Senden an API
|
||||
result = save_draft_to_vault(final_doc, filename=target_filename)
|
||||
|
||||
if "error" in result:
|
||||
st.error(f"Fehler: {result['error']}")
|
||||
else:
|
||||
st.success(f"Gespeichert: {result.get('file_path')}")
|
||||
st.balloons()
|
||||
|
||||
with b2:
|
||||
if st.button("📋 Code anzeigen", 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):
|
||||
"""
|
||||
Rendert das Chat-Interface.
|
||||
"""
|
||||
for idx, msg in enumerate(st.session_state.messages):
|
||||
with st.chat_message(msg["role"]):
|
||||
if msg["role"] == "assistant":
|
||||
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)
|
||||
|
||||
with st.expander("🐞 Debug Raw Payload", expanded=False):
|
||||
st.json(msg)
|
||||
|
||||
if intent == "INTERVIEW":
|
||||
render_draft_editor(msg)
|
||||
else:
|
||||
st.markdown(msg["content"])
|
||||
|
||||
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"])
|
||||
|
||||
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():
|
||||
"""
|
||||
Wrapper für den manuellen Editor-Modus (Startet mit leerem Template).
|
||||
"""
|
||||
mock_msg = {
|
||||
"content": "---\ntype: concept\ntitle: Neue Notiz\nstatus: draft\ntags: []\n---\n# Titel\n",
|
||||
"query_id": "manual_mode_v2"
|
||||
}
|
||||
render_draft_editor(mock_msg)
|
||||
|
||||
# --- GRAPH EXPLORER ---
|
||||
|
||||
def render_graph_explorer(graph_service):
|
||||
st.header("🕸️ Graph Explorer")
|
||||
|
||||
# Session State initialisieren
|
||||
if "graph_center_id" not in st.session_state: st.session_state.graph_center_id = None
|
||||
|
||||
# Defaults speichern für Persistenz
|
||||
st.session_state.setdefault("graph_depth", 2)
|
||||
st.session_state.setdefault("graph_show_labels", True)
|
||||
# Defaults angepasst für BarnesHut (Skalierung angepasst)
|
||||
st.session_state.setdefault("graph_spacing", 150)
|
||||
st.session_state.setdefault("graph_gravity", -3000)
|
||||
|
||||
col_ctrl, col_graph = st.columns([1, 4])
|
||||
|
||||
with col_ctrl:
|
||||
st.subheader("Fokus")
|
||||
|
||||
# Suche
|
||||
search_term = st.text_input("Suche Notiz", placeholder="Titel eingeben...")
|
||||
|
||||
options = {}
|
||||
if search_term:
|
||||
hits, _ = graph_service.client.scroll(
|
||||
collection_name=f"{COLLECTION_PREFIX}_notes",
|
||||
scroll_filter=models.Filter(must=[models.FieldCondition(key="title", match=models.MatchText(text=search_term))]),
|
||||
limit=10
|
||||
)
|
||||
options = {h.payload['title']: h.payload['note_id'] for h in hits}
|
||||
|
||||
if options:
|
||||
selected_title = st.selectbox("Ergebnisse:", list(options.keys()))
|
||||
if st.button("Laden", use_container_width=True):
|
||||
st.session_state.graph_center_id = options[selected_title]
|
||||
st.rerun()
|
||||
|
||||
st.divider()
|
||||
|
||||
# View Settings
|
||||
with st.expander("👁️ Ansicht & Layout", expanded=True):
|
||||
st.session_state.graph_depth = st.slider("Tiefe (Tier)", 1, 3, st.session_state.graph_depth)
|
||||
st.session_state.graph_show_labels = st.checkbox("Kanten-Beschriftung", st.session_state.graph_show_labels)
|
||||
|
||||
st.markdown("**Physik (BarnesHut)**")
|
||||
# ACHTUNG: BarnesHut reagiert anders. Spring Length ist wichtig.
|
||||
st.session_state.graph_spacing = st.slider("Federlänge (Abstand)", 50, 500, st.session_state.graph_spacing, help="Wie lang sollen die Verbindungen sein?")
|
||||
st.session_state.graph_gravity = st.slider("Abstoßung (Gravity)", -20000, -1000, st.session_state.graph_gravity, help="Wie stark sollen sich Knoten abstoßen?")
|
||||
|
||||
if st.button("Reset Layout"):
|
||||
st.session_state.graph_spacing = 150
|
||||
st.session_state.graph_gravity = -3000
|
||||
st.rerun()
|
||||
|
||||
st.divider()
|
||||
st.caption("Legende (Top Typen)")
|
||||
for k, v in list(GRAPH_COLORS.items())[:8]:
|
||||
st.markdown(f"<span style='color:{v}'>●</span> {k}", unsafe_allow_html=True)
|
||||
|
||||
with col_graph:
|
||||
center_id = st.session_state.graph_center_id
|
||||
|
||||
if center_id:
|
||||
# Container für Action Bar OBERHALB des Graphen (Layout Fix)
|
||||
action_container = st.container()
|
||||
|
||||
# Graph Laden
|
||||
with st.spinner(f"Lade Graph..."):
|
||||
# Daten laden (Cache wird genutzt)
|
||||
nodes, edges = graph_service.get_ego_graph(
|
||||
center_id,
|
||||
depth=st.session_state.graph_depth,
|
||||
show_labels=st.session_state.graph_show_labels
|
||||
)
|
||||
|
||||
# Fetch Note Data für Button & Debug
|
||||
note_data = graph_service._fetch_note_cached(center_id)
|
||||
|
||||
# --- ACTION BAR RENDEREN ---
|
||||
with action_container:
|
||||
c_act1, c_act2 = st.columns([3, 1])
|
||||
with c_act1:
|
||||
st.caption(f"Aktives Zentrum: **{center_id}**")
|
||||
with c_act2:
|
||||
if note_data:
|
||||
st.button("📝 Bearbeiten",
|
||||
use_container_width=True,
|
||||
on_click=switch_to_editor_callback,
|
||||
args=(note_data,))
|
||||
else:
|
||||
st.error("Daten nicht verfügbar")
|
||||
|
||||
# DATA INSPECTOR (Payload Debug)
|
||||
with st.expander("🕵️ Data Inspector (Payload Debug)", expanded=False):
|
||||
if note_data:
|
||||
st.json(note_data)
|
||||
if 'path' not in note_data:
|
||||
st.error("ACHTUNG: Feld 'path' fehlt im Qdrant-Payload! Update-Modus wird nicht funktionieren.")
|
||||
else:
|
||||
st.success(f"Pfad gefunden: {note_data['path']}")
|
||||
else:
|
||||
st.info("Keine Daten geladen.")
|
||||
|
||||
if not nodes:
|
||||
st.warning("Keine Daten gefunden.")
|
||||
else:
|
||||
# --- CONFIGURATION (BarnesHut) ---
|
||||
# Height-Trick für Re-Render (da key-Parameter nicht funktioniert)
|
||||
# Ändere Height minimal basierend auf Gravity
|
||||
dyn_height = 800 + (abs(st.session_state.graph_gravity) % 3)
|
||||
|
||||
config = Config(
|
||||
width=1000,
|
||||
height=dyn_height,
|
||||
directed=True,
|
||||
physics={
|
||||
"enabled": True,
|
||||
# BarnesHut ist der Standard und stabilste Solver für Agraph
|
||||
"solver": "barnesHut",
|
||||
"barnesHut": {
|
||||
"gravitationalConstant": st.session_state.graph_gravity,
|
||||
"centralGravity": 0.3,
|
||||
"springLength": st.session_state.graph_spacing,
|
||||
"springConstant": 0.04,
|
||||
"damping": 0.09,
|
||||
"avoidOverlap": 0.1
|
||||
},
|
||||
"stabilization": {"enabled": True, "iterations": 600}
|
||||
},
|
||||
hierarchical=False,
|
||||
nodeHighlightBehavior=True,
|
||||
highlightColor="#F7A7A6",
|
||||
collapsible=False
|
||||
)
|
||||
|
||||
return_value = agraph(nodes=nodes, edges=edges, config=config)
|
||||
|
||||
# Interaktions-Logik
|
||||
if return_value:
|
||||
if return_value != center_id:
|
||||
# Navigation: Neues Zentrum setzen
|
||||
st.session_state.graph_center_id = return_value
|
||||
st.rerun()
|
||||
else:
|
||||
# Klick auf das Zentrum selbst
|
||||
st.toast(f"Zentrum: {return_value}")
|
||||
|
||||
else:
|
||||
st.info("👈 Bitte wähle links eine Notiz aus.")
|
||||
189
app/frontend/ui_editor.py
Normal file
189
app/frontend/ui_editor.py
Normal file
|
|
@ -0,0 +1,189 @@
|
|||
import streamlit as st
|
||||
import uuid
|
||||
import re
|
||||
from datetime import datetime
|
||||
|
||||
from ui_utils import parse_markdown_draft, build_markdown_doc, slugify
|
||||
from ui_api import save_draft_to_vault, analyze_draft_text
|
||||
|
||||
def render_draft_editor(msg):
|
||||
"""
|
||||
Rendert den Markdown-Editor.
|
||||
Nutzt 'origin_filename' aus der Message, um zwischen Update und Neu zu unterscheiden.
|
||||
"""
|
||||
if "query_id" not in msg or not msg["query_id"]:
|
||||
msg["query_id"] = str(uuid.uuid4())
|
||||
|
||||
qid = msg["query_id"]
|
||||
key_base = f"draft_{qid}"
|
||||
|
||||
# State Keys
|
||||
data_meta_key = f"{key_base}_data_meta"
|
||||
data_sugg_key = f"{key_base}_data_suggestions"
|
||||
widget_body_key = f"{key_base}_widget_body"
|
||||
data_body_key = f"{key_base}_data_body"
|
||||
|
||||
# --- INIT STATE ---
|
||||
if f"{key_base}_init" not in st.session_state:
|
||||
meta, body = parse_markdown_draft(msg["content"])
|
||||
if "type" not in meta: meta["type"] = "default"
|
||||
if "title" not in meta: meta["title"] = ""
|
||||
tags = meta.get("tags", [])
|
||||
meta["tags_str"] = ", ".join(tags) if isinstance(tags, list) else str(tags)
|
||||
|
||||
st.session_state[data_meta_key] = meta
|
||||
st.session_state[data_sugg_key] = []
|
||||
st.session_state[data_body_key] = body.strip()
|
||||
|
||||
st.session_state[f"{key_base}_wdg_title"] = meta["title"]
|
||||
st.session_state[f"{key_base}_wdg_type"] = meta["type"]
|
||||
st.session_state[f"{key_base}_wdg_tags"] = meta["tags_str"]
|
||||
|
||||
# Pfad übernehmen (Source of Truth)
|
||||
st.session_state[f"{key_base}_origin_filename"] = msg.get("origin_filename")
|
||||
st.session_state[f"{key_base}_init"] = True
|
||||
|
||||
# --- RESURRECTION ---
|
||||
if widget_body_key not in st.session_state and data_body_key in st.session_state:
|
||||
st.session_state[widget_body_key] = st.session_state[data_body_key]
|
||||
|
||||
# --- SYNC HELPER ---
|
||||
def _sync_meta():
|
||||
meta = st.session_state[data_meta_key]
|
||||
meta["title"] = st.session_state.get(f"{key_base}_wdg_title", "")
|
||||
meta["type"] = st.session_state.get(f"{key_base}_wdg_type", "default")
|
||||
meta["tags_str"] = st.session_state.get(f"{key_base}_wdg_tags", "")
|
||||
st.session_state[data_meta_key] = meta
|
||||
|
||||
def _sync_body():
|
||||
st.session_state[data_body_key] = st.session_state[widget_body_key]
|
||||
|
||||
def _insert_text(t):
|
||||
st.session_state[widget_body_key] = f"{st.session_state.get(widget_body_key, '')}\n\n{t}"
|
||||
st.session_state[data_body_key] = st.session_state[widget_body_key]
|
||||
|
||||
def _remove_text(t):
|
||||
st.session_state[widget_body_key] = st.session_state.get(widget_body_key, '').replace(t, "").strip()
|
||||
st.session_state[data_body_key] = st.session_state[widget_body_key]
|
||||
|
||||
# --- UI LAYOUT ---
|
||||
|
||||
# Header Info
|
||||
origin_fname = st.session_state.get(f"{key_base}_origin_filename")
|
||||
if origin_fname:
|
||||
display_name = str(origin_fname).split("/")[-1]
|
||||
st.success(f"📂 **Update-Modus**: `{origin_fname}`")
|
||||
st.markdown(f'<div class="draft-box" style="border-left: 5px solid #ff9f43;">', unsafe_allow_html=True)
|
||||
else:
|
||||
st.info("✨ **Erstell-Modus**: Neue Datei wird angelegt.")
|
||||
st.markdown(f'<div class="draft-box">', unsafe_allow_html=True)
|
||||
|
||||
st.markdown("### Editor")
|
||||
|
||||
# Meta Felder
|
||||
meta_ref = st.session_state[data_meta_key]
|
||||
c1, c2 = st.columns([2, 1])
|
||||
with c1:
|
||||
st.text_input("Titel", key=f"{key_base}_wdg_title", on_change=_sync_meta)
|
||||
with c2:
|
||||
known_types = ["concept", "project", "decision", "experience", "journal", "value", "goal", "principle", "risk", "belief"]
|
||||
curr_type = st.session_state.get(f"{key_base}_wdg_type", meta_ref["type"])
|
||||
if curr_type not in known_types: known_types.append(curr_type)
|
||||
st.selectbox("Typ", known_types, key=f"{key_base}_wdg_type", on_change=_sync_meta)
|
||||
|
||||
st.text_input("Tags", key=f"{key_base}_wdg_tags", on_change=_sync_meta)
|
||||
|
||||
# Tabs
|
||||
tab_edit, tab_intel, tab_view = st.tabs(["✏️ Inhalt", "🧠 Intelligence", "👁️ Vorschau"])
|
||||
|
||||
with tab_edit:
|
||||
st.text_area("Body", key=widget_body_key, height=600, on_change=_sync_body, label_visibility="collapsed")
|
||||
|
||||
with tab_intel:
|
||||
st.info("Analysiert den Text auf Verknüpfungsmöglichkeiten.")
|
||||
if st.button("🔍 Analyse starten", key=f"{key_base}_analyze"):
|
||||
st.session_state[data_sugg_key] = []
|
||||
text_to_analyze = st.session_state.get(widget_body_key, st.session_state.get(data_body_key, ""))
|
||||
with st.spinner("Analysiere..."):
|
||||
analysis = analyze_draft_text(text_to_analyze, st.session_state.get(f"{key_base}_wdg_type", "concept"))
|
||||
if "error" in analysis:
|
||||
st.error(f"Fehler: {analysis['error']}")
|
||||
else:
|
||||
suggestions = analysis.get("suggestions", [])
|
||||
st.session_state[data_sugg_key] = suggestions
|
||||
if not suggestions: st.warning("Keine Vorschläge.")
|
||||
else: st.success(f"{len(suggestions)} Vorschläge gefunden.")
|
||||
|
||||
suggestions = st.session_state[data_sugg_key]
|
||||
if suggestions:
|
||||
current_text = st.session_state.get(widget_body_key, "")
|
||||
for idx, sugg in enumerate(suggestions):
|
||||
link_text = sugg.get('suggested_markdown', '')
|
||||
is_inserted = link_text in current_text
|
||||
bg_color = "#e6fffa" if is_inserted else "#ffffff"
|
||||
border = "3px solid #28a745" if is_inserted else "3px solid #1a73e8"
|
||||
st.markdown(f"<div style='border-left: {border}; background-color: {bg_color}; padding: 10px; margin-bottom: 8px;'><b>{sugg.get('target_title')}</b> <small>({sugg.get('type')})</small><br><i>{sugg.get('reason')}</i><br><code>{link_text}</code></div>", unsafe_allow_html=True)
|
||||
if is_inserted:
|
||||
st.button("❌ Entfernen", key=f"del_{idx}_{key_base}", on_click=_remove_text, args=(link_text,))
|
||||
else:
|
||||
st.button("➕ Einfügen", key=f"add_{idx}_{key_base}", on_click=_insert_text, args=(link_text,))
|
||||
|
||||
# Save Logic Preparation
|
||||
final_tags = [t.strip() for t in st.session_state.get(f"{key_base}_wdg_tags", "").split(",") if t.strip()]
|
||||
final_meta = {
|
||||
"id": "generated_on_save",
|
||||
"type": st.session_state.get(f"{key_base}_wdg_type", "default"),
|
||||
"title": st.session_state.get(f"{key_base}_wdg_title", "").strip(),
|
||||
"status": "draft",
|
||||
"tags": final_tags
|
||||
}
|
||||
if "origin_note_id" in msg:
|
||||
final_meta["id"] = msg["origin_note_id"]
|
||||
|
||||
final_body = st.session_state.get(widget_body_key, st.session_state[data_body_key])
|
||||
if not final_meta["title"]:
|
||||
h1_match = re.search(r"^#\s+(.*)$", final_body, re.MULTILINE)
|
||||
if h1_match: final_meta["title"] = h1_match.group(1).strip()
|
||||
|
||||
final_doc = build_markdown_doc(final_meta, final_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("---")
|
||||
|
||||
# Save Actions
|
||||
b1, b2 = st.columns([1, 1])
|
||||
with b1:
|
||||
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..."):
|
||||
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)
|
||||
if "error" in result:
|
||||
st.error(f"Fehler: {result['error']}")
|
||||
else:
|
||||
st.success(f"Gespeichert: {result.get('file_path')}")
|
||||
st.balloons()
|
||||
with b2:
|
||||
if st.button("📋 Code anzeigen", key=f"{key_base}_btn_copy"):
|
||||
st.code(final_doc, language="markdown")
|
||||
|
||||
st.markdown("</div>", unsafe_allow_html=True)
|
||||
|
||||
def render_manual_editor():
|
||||
mock_msg = {
|
||||
"content": "---\ntype: concept\ntitle: Neue Notiz\nstatus: draft\ntags: []\n---\n# Titel\n",
|
||||
"query_id": "manual_mode_v2"
|
||||
}
|
||||
render_draft_editor(mock_msg)
|
||||
126
app/frontend/ui_graph.py
Normal file
126
app/frontend/ui_graph.py
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
import streamlit as st
|
||||
from streamlit_agraph import agraph, Config
|
||||
from qdrant_client import models
|
||||
from ui_config import COLLECTION_PREFIX, GRAPH_COLORS
|
||||
from ui_callbacks import switch_to_editor_callback
|
||||
|
||||
def render_graph_explorer(graph_service):
|
||||
st.header("🕸️ Graph Explorer")
|
||||
|
||||
if "graph_center_id" not in st.session_state: st.session_state.graph_center_id = None
|
||||
|
||||
# Defaults
|
||||
st.session_state.setdefault("graph_depth", 2)
|
||||
st.session_state.setdefault("graph_show_labels", True)
|
||||
st.session_state.setdefault("graph_spacing", 150)
|
||||
st.session_state.setdefault("graph_gravity", -3000)
|
||||
|
||||
col_ctrl, col_graph = st.columns([1, 4])
|
||||
|
||||
with col_ctrl:
|
||||
st.subheader("Fokus")
|
||||
search_term = st.text_input("Suche Notiz", placeholder="Titel eingeben...")
|
||||
|
||||
if search_term:
|
||||
hits, _ = graph_service.client.scroll(
|
||||
collection_name=f"{COLLECTION_PREFIX}_notes",
|
||||
scroll_filter=models.Filter(must=[models.FieldCondition(key="title", match=models.MatchText(text=search_term))]),
|
||||
limit=10
|
||||
)
|
||||
options = {h.payload['title']: h.payload['note_id'] for h in hits}
|
||||
if options:
|
||||
selected_title = st.selectbox("Ergebnisse:", list(options.keys()))
|
||||
if st.button("Laden", use_container_width=True):
|
||||
st.session_state.graph_center_id = options[selected_title]
|
||||
st.rerun()
|
||||
|
||||
st.divider()
|
||||
with st.expander("👁️ Ansicht & Layout", expanded=True):
|
||||
st.session_state.graph_depth = st.slider("Tiefe (Tier)", 1, 3, st.session_state.graph_depth)
|
||||
st.session_state.graph_show_labels = st.checkbox("Kanten-Beschriftung", st.session_state.graph_show_labels)
|
||||
|
||||
st.markdown("**Physik (BarnesHut)**")
|
||||
st.session_state.graph_spacing = st.slider("Federlänge", 50, 500, st.session_state.graph_spacing)
|
||||
st.session_state.graph_gravity = st.slider("Abstoßung", -20000, -500, st.session_state.graph_gravity)
|
||||
|
||||
if st.button("Reset Layout"):
|
||||
st.session_state.graph_spacing = 150
|
||||
st.session_state.graph_gravity = -3000
|
||||
st.rerun()
|
||||
|
||||
st.divider()
|
||||
st.caption("Legende (Top Typen)")
|
||||
for k, v in list(GRAPH_COLORS.items())[:8]:
|
||||
st.markdown(f"<span style='color:{v}'>●</span> {k}", unsafe_allow_html=True)
|
||||
|
||||
with col_graph:
|
||||
center_id = st.session_state.graph_center_id
|
||||
|
||||
if center_id:
|
||||
# Action Container oben
|
||||
action_container = st.container()
|
||||
|
||||
with st.spinner(f"Lade Graph..."):
|
||||
nodes, edges = graph_service.get_ego_graph(
|
||||
center_id,
|
||||
depth=st.session_state.graph_depth,
|
||||
show_labels=st.session_state.graph_show_labels
|
||||
)
|
||||
note_data = graph_service._fetch_note_cached(center_id)
|
||||
|
||||
# Action Bar rendern
|
||||
with action_container:
|
||||
c1, c2 = st.columns([3, 1])
|
||||
with c1: st.caption(f"Aktives Zentrum: **{center_id}**")
|
||||
with c2:
|
||||
if note_data:
|
||||
st.button("📝 Bearbeiten", use_container_width=True, on_click=switch_to_editor_callback, args=(note_data,))
|
||||
else:
|
||||
st.error("Datenfehler")
|
||||
|
||||
with st.expander("🕵️ Data Inspector", expanded=False):
|
||||
if note_data:
|
||||
st.json(note_data)
|
||||
if 'path' in note_data: st.success(f"Pfad OK: {note_data['path']}")
|
||||
else: st.error("Pfad fehlt!")
|
||||
else: st.info("Leer.")
|
||||
|
||||
if not nodes:
|
||||
st.warning("Keine Daten gefunden.")
|
||||
else:
|
||||
# Physik Config für BarnesHut + Height-Trick
|
||||
dyn_height = 800 + (abs(st.session_state.graph_gravity) % 5)
|
||||
|
||||
config = Config(
|
||||
width=1000,
|
||||
height=dyn_height,
|
||||
directed=True,
|
||||
physics={
|
||||
"enabled": True,
|
||||
"solver": "barnesHut",
|
||||
"barnesHut": {
|
||||
"gravitationalConstant": st.session_state.graph_gravity,
|
||||
"centralGravity": 0.3,
|
||||
"springLength": st.session_state.graph_spacing,
|
||||
"springConstant": 0.04,
|
||||
"damping": 0.09,
|
||||
"avoidOverlap": 0.1
|
||||
},
|
||||
"stabilization": {"enabled": True, "iterations": 600}
|
||||
},
|
||||
hierarchical=False,
|
||||
nodeHighlightBehavior=True,
|
||||
highlightColor="#F7A7A6",
|
||||
collapsible=False
|
||||
)
|
||||
|
||||
return_value = agraph(nodes=nodes, edges=edges, config=config)
|
||||
|
||||
if return_value:
|
||||
if return_value != center_id:
|
||||
st.session_state.graph_center_id = return_value
|
||||
st.rerun()
|
||||
else:
|
||||
st.toast(f"Zentrum: {return_value}")
|
||||
else:
|
||||
st.info("👈 Bitte wähle links eine Notiz aus.")
|
||||
31
app/frontend/ui_sidebar.py
Normal file
31
app/frontend/ui_sidebar.py
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import streamlit as st
|
||||
from ui_utils import load_history_from_logs
|
||||
from ui_config import HISTORY_FILE
|
||||
|
||||
def render_sidebar():
|
||||
with st.sidebar:
|
||||
st.title("🧠 mindnet")
|
||||
st.caption("v2.6 | WP-19 Graph View")
|
||||
|
||||
if "sidebar_mode_selection" not in st.session_state:
|
||||
st.session_state["sidebar_mode_selection"] = "💬 Chat"
|
||||
|
||||
mode = st.radio(
|
||||
"Modus",
|
||||
["💬 Chat", "📝 Manueller Editor", "🕸️ Graph Explorer"],
|
||||
key="sidebar_mode_selection"
|
||||
)
|
||||
|
||||
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(HISTORY_FILE, 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
|
||||
Loading…
Reference in New Issue
Block a user