neue Funkktionen für Graphendarstellung
This commit is contained in:
parent
93b8bc48e4
commit
72a3988e49
|
|
@ -5,41 +5,50 @@ from datetime import datetime
|
||||||
from streamlit_agraph import agraph, Config
|
from streamlit_agraph import agraph, Config
|
||||||
from qdrant_client import models
|
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_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_api import save_draft_to_vault, analyze_draft_text, send_chat_message, submit_feedback
|
||||||
from ui_config import HISTORY_FILE, COLLECTION_PREFIX
|
from ui_config import HISTORY_FILE, COLLECTION_PREFIX, GRAPH_COLORS
|
||||||
|
|
||||||
def render_sidebar():
|
def render_sidebar():
|
||||||
with st.sidebar:
|
with st.sidebar:
|
||||||
st.title("🧠 mindnet")
|
st.title("🧠 mindnet")
|
||||||
st.caption("v2.6 | WP-19 Graph View")
|
st.caption("v2.6 | WP-19 Graph View")
|
||||||
|
# Modus-Auswahl
|
||||||
mode = st.radio("Modus", ["💬 Chat", "📝 Manueller Editor", "🕸️ Graph Explorer"], index=0)
|
mode = st.radio("Modus", ["💬 Chat", "📝 Manueller Editor", "🕸️ Graph Explorer"], index=0)
|
||||||
|
|
||||||
st.divider()
|
st.divider()
|
||||||
st.subheader("⚙️ Settings")
|
st.subheader("⚙️ Settings")
|
||||||
top_k = st.slider("Quellen (Top-K)", 1, 10, 5)
|
top_k = st.slider("Quellen (Top-K)", 1, 10, 5)
|
||||||
explain = st.toggle("Explanation Layer", True)
|
explain = st.toggle("Explanation Layer", True)
|
||||||
|
|
||||||
st.divider()
|
st.divider()
|
||||||
st.subheader("🕒 Verlauf")
|
st.subheader("🕒 Verlauf")
|
||||||
|
# Historie laden
|
||||||
for q in load_history_from_logs(HISTORY_FILE, 8):
|
for q in load_history_from_logs(HISTORY_FILE, 8):
|
||||||
if st.button(f"🔎 {q[:25]}...", key=f"hist_{q}", use_container_width=True):
|
if st.button(f"🔎 {q[:25]}...", key=f"hist_{q}", use_container_width=True):
|
||||||
st.session_state.messages.append({"role": "user", "content": q})
|
st.session_state.messages.append({"role": "user", "content": q})
|
||||||
st.rerun()
|
st.rerun()
|
||||||
|
|
||||||
return mode, top_k, explain
|
return mode, top_k, explain
|
||||||
|
|
||||||
def render_draft_editor(msg):
|
def render_draft_editor(msg):
|
||||||
|
"""
|
||||||
|
Rendert den Editor für Drafts (genutzt im Chat bei INTERVIEW Intent oder im manuellen Modus).
|
||||||
|
"""
|
||||||
if "query_id" not in msg or not msg["query_id"]:
|
if "query_id" not in msg or not msg["query_id"]:
|
||||||
msg["query_id"] = str(uuid.uuid4())
|
msg["query_id"] = str(uuid.uuid4())
|
||||||
|
|
||||||
qid = msg["query_id"]
|
qid = msg["query_id"]
|
||||||
key_base = f"draft_{qid}"
|
key_base = f"draft_{qid}"
|
||||||
|
|
||||||
# State Keys
|
# State Keys für Persistenz
|
||||||
data_meta_key = f"{key_base}_data_meta"
|
data_meta_key = f"{key_base}_data_meta"
|
||||||
data_sugg_key = f"{key_base}_data_suggestions"
|
data_sugg_key = f"{key_base}_data_suggestions"
|
||||||
widget_body_key = f"{key_base}_widget_body"
|
widget_body_key = f"{key_base}_widget_body"
|
||||||
data_body_key = f"{key_base}_data_body"
|
data_body_key = f"{key_base}_data_body"
|
||||||
|
|
||||||
# INIT STATE
|
# --- INIT STATE ---
|
||||||
if f"{key_base}_init" not in st.session_state:
|
if f"{key_base}_init" not in st.session_state:
|
||||||
meta, body = parse_markdown_draft(msg["content"])
|
meta, body = parse_markdown_draft(msg["content"])
|
||||||
if "type" not in meta: meta["type"] = "default"
|
if "type" not in meta: meta["type"] = "default"
|
||||||
|
|
@ -50,16 +59,19 @@ def render_draft_editor(msg):
|
||||||
st.session_state[data_meta_key] = meta
|
st.session_state[data_meta_key] = meta
|
||||||
st.session_state[data_sugg_key] = []
|
st.session_state[data_sugg_key] = []
|
||||||
st.session_state[data_body_key] = body.strip()
|
st.session_state[data_body_key] = body.strip()
|
||||||
|
|
||||||
|
# Widget States initialisieren
|
||||||
st.session_state[f"{key_base}_wdg_title"] = meta["title"]
|
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_type"] = meta["type"]
|
||||||
st.session_state[f"{key_base}_wdg_tags"] = meta["tags_str"]
|
st.session_state[f"{key_base}_wdg_tags"] = meta["tags_str"]
|
||||||
st.session_state[f"{key_base}_init"] = True
|
st.session_state[f"{key_base}_init"] = True
|
||||||
|
|
||||||
# RESURRECTION
|
# --- STATE RESURRECTION ---
|
||||||
|
# Falls Streamlit rerunt, stellen wir sicher, dass der Body nicht verloren geht
|
||||||
if widget_body_key not in st.session_state and data_body_key in st.session_state:
|
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]
|
st.session_state[widget_body_key] = st.session_state[data_body_key]
|
||||||
|
|
||||||
# CALLBACKS
|
# --- CALLBACKS ---
|
||||||
def _sync_meta():
|
def _sync_meta():
|
||||||
meta = st.session_state[data_meta_key]
|
meta = st.session_state[data_meta_key]
|
||||||
meta["title"] = st.session_state.get(f"{key_base}_wdg_title", "")
|
meta["title"] = st.session_state.get(f"{key_base}_wdg_title", "")
|
||||||
|
|
@ -82,11 +94,13 @@ def render_draft_editor(msg):
|
||||||
st.session_state[widget_body_key] = new_text
|
st.session_state[widget_body_key] = new_text
|
||||||
st.session_state[data_body_key] = new_text
|
st.session_state[data_body_key] = new_text
|
||||||
|
|
||||||
# UI LAYOUT
|
# --- UI LAYOUT ---
|
||||||
st.markdown(f'<div class="draft-box">', unsafe_allow_html=True)
|
st.markdown(f'<div class="draft-box">', unsafe_allow_html=True)
|
||||||
st.markdown("### 📝 Entwurf bearbeiten")
|
st.markdown("### 📝 Entwurf bearbeiten")
|
||||||
|
|
||||||
meta_ref = st.session_state[data_meta_key]
|
meta_ref = st.session_state[data_meta_key]
|
||||||
|
|
||||||
|
# Metadaten Zeile
|
||||||
c1, c2 = st.columns([2, 1])
|
c1, c2 = st.columns([2, 1])
|
||||||
with c1:
|
with c1:
|
||||||
st.text_input("Titel", key=f"{key_base}_wdg_title", on_change=_sync_meta)
|
st.text_input("Titel", key=f"{key_base}_wdg_title", on_change=_sync_meta)
|
||||||
|
|
@ -98,6 +112,7 @@ def render_draft_editor(msg):
|
||||||
|
|
||||||
st.text_input("Tags", key=f"{key_base}_wdg_tags", on_change=_sync_meta)
|
st.text_input("Tags", key=f"{key_base}_wdg_tags", on_change=_sync_meta)
|
||||||
|
|
||||||
|
# Tabs für Bearbeitung
|
||||||
tab_edit, tab_intel, tab_view = st.tabs(["✏️ Inhalt", "🧠 Intelligence", "👁️ Vorschau"])
|
tab_edit, tab_intel, tab_view = st.tabs(["✏️ Inhalt", "🧠 Intelligence", "👁️ Vorschau"])
|
||||||
|
|
||||||
with tab_edit:
|
with tab_edit:
|
||||||
|
|
@ -105,6 +120,7 @@ def render_draft_editor(msg):
|
||||||
|
|
||||||
with tab_intel:
|
with tab_intel:
|
||||||
st.info("Klicke auf 'Analysieren', um Verknüpfungen für den AKTUELLEN Text zu finden.")
|
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"):
|
if st.button("🔍 Analyse starten", key=f"{key_base}_analyze"):
|
||||||
st.session_state[data_sugg_key] = []
|
st.session_state[data_sugg_key] = []
|
||||||
text_to_analyze = st.session_state.get(widget_body_key, st.session_state.get(data_body_key, ""))
|
text_to_analyze = st.session_state.get(widget_body_key, st.session_state.get(data_body_key, ""))
|
||||||
|
|
@ -120,14 +136,18 @@ def render_draft_editor(msg):
|
||||||
if not suggestions: st.warning("Keine Vorschläge gefunden.")
|
if not suggestions: st.warning("Keine Vorschläge gefunden.")
|
||||||
else: st.success(f"{len(suggestions)} Vorschläge gefunden.")
|
else: st.success(f"{len(suggestions)} Vorschläge gefunden.")
|
||||||
|
|
||||||
|
# Vorschläge anzeigen
|
||||||
suggestions = st.session_state[data_sugg_key]
|
suggestions = st.session_state[data_sugg_key]
|
||||||
if suggestions:
|
if suggestions:
|
||||||
current_text_state = st.session_state.get(widget_body_key, "")
|
current_text_state = st.session_state.get(widget_body_key, "")
|
||||||
for idx, sugg in enumerate(suggestions):
|
for idx, sugg in enumerate(suggestions):
|
||||||
link_text = sugg.get('suggested_markdown', '')
|
link_text = sugg.get('suggested_markdown', '')
|
||||||
is_inserted = link_text in current_text_state
|
is_inserted = link_text in current_text_state
|
||||||
|
|
||||||
|
# Styling je nach Status
|
||||||
bg_color = "#e6fffa" if is_inserted else "#ffffff"
|
bg_color = "#e6fffa" if is_inserted else "#ffffff"
|
||||||
border = "3px solid #28a745" if is_inserted else "3px solid #1a73e8"
|
border = "3px solid #28a745" if is_inserted else "3px solid #1a73e8"
|
||||||
|
|
||||||
st.markdown(f"""
|
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);">
|
<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>
|
<b>{sugg.get('target_title')}</b> <small>({sugg.get('type')})</small><br>
|
||||||
|
|
@ -135,13 +155,16 @@ def render_draft_editor(msg):
|
||||||
<code>{link_text}</code>
|
<code>{link_text}</code>
|
||||||
</div>
|
</div>
|
||||||
""", unsafe_allow_html=True)
|
""", unsafe_allow_html=True)
|
||||||
|
|
||||||
if is_inserted:
|
if is_inserted:
|
||||||
st.button("❌ Entfernen", key=f"del_{idx}_{key_base}", on_click=_remove_text, args=(link_text,))
|
st.button("❌ Entfernen", key=f"del_{idx}_{key_base}", on_click=_remove_text, args=(link_text,))
|
||||||
else:
|
else:
|
||||||
st.button("➕ Einfügen", key=f"add_{idx}_{key_base}", on_click=_insert_text, args=(link_text,))
|
st.button("➕ Einfügen", key=f"add_{idx}_{key_base}", on_click=_insert_text, args=(link_text,))
|
||||||
|
|
||||||
|
# Dokument zusammenbauen für Speicherung/Vorschau
|
||||||
final_tags_str = st.session_state.get(f"{key_base}_wdg_tags", "")
|
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_tags = [t.strip() for t in final_tags_str.split(",") if t.strip()]
|
||||||
|
|
||||||
final_meta = {
|
final_meta = {
|
||||||
"id": "generated_on_save",
|
"id": "generated_on_save",
|
||||||
"type": st.session_state.get(f"{key_base}_wdg_type", "default"),
|
"type": st.session_state.get(f"{key_base}_wdg_type", "default"),
|
||||||
|
|
@ -150,6 +173,8 @@ def render_draft_editor(msg):
|
||||||
"tags": final_tags
|
"tags": final_tags
|
||||||
}
|
}
|
||||||
final_body = st.session_state.get(widget_body_key, st.session_state[data_body_key])
|
final_body = st.session_state.get(widget_body_key, st.session_state[data_body_key])
|
||||||
|
|
||||||
|
# Fallback Title aus H1
|
||||||
if not final_meta["title"]:
|
if not final_meta["title"]:
|
||||||
h1_match = re.search(r"^#\s+(.*)$", final_body, re.MULTILINE)
|
h1_match = re.search(r"^#\s+(.*)$", final_body, re.MULTILINE)
|
||||||
if h1_match: final_meta["title"] = h1_match.group(1).strip()
|
if h1_match: final_meta["title"] = h1_match.group(1).strip()
|
||||||
|
|
@ -162,6 +187,8 @@ def render_draft_editor(msg):
|
||||||
st.markdown('</div>', unsafe_allow_html=True)
|
st.markdown('</div>', unsafe_allow_html=True)
|
||||||
|
|
||||||
st.markdown("---")
|
st.markdown("---")
|
||||||
|
|
||||||
|
# Footer Buttons
|
||||||
b1, b2 = st.columns([1, 1])
|
b1, b2 = st.columns([1, 1])
|
||||||
with b1:
|
with b1:
|
||||||
if st.button("💾 Speichern & Indizieren", type="primary", key=f"{key_base}_save"):
|
if st.button("💾 Speichern & Indizieren", type="primary", key=f"{key_base}_save"):
|
||||||
|
|
@ -172,6 +199,7 @@ def render_draft_editor(msg):
|
||||||
raw_title = clean_body[:40] if clean_body else "draft"
|
raw_title = clean_body[:40] if clean_body else "draft"
|
||||||
safe_title = slugify(raw_title)[:60] or "draft"
|
safe_title = slugify(raw_title)[:60] or "draft"
|
||||||
fname = f"{datetime.now().strftime('%Y%m%d')}-{safe_title}.md"
|
fname = f"{datetime.now().strftime('%Y%m%d')}-{safe_title}.md"
|
||||||
|
|
||||||
result = save_draft_to_vault(final_doc, filename=fname)
|
result = save_draft_to_vault(final_doc, filename=fname)
|
||||||
if "error" in result: st.error(f"Fehler: {result['error']}")
|
if "error" in result: st.error(f"Fehler: {result['error']}")
|
||||||
else:
|
else:
|
||||||
|
|
@ -180,46 +208,59 @@ def render_draft_editor(msg):
|
||||||
with b2:
|
with b2:
|
||||||
if st.button("📋 Code anzeigen", key=f"{key_base}_btn_copy"):
|
if st.button("📋 Code anzeigen", key=f"{key_base}_btn_copy"):
|
||||||
st.code(final_doc, language="markdown")
|
st.code(final_doc, language="markdown")
|
||||||
|
|
||||||
st.markdown("</div>", unsafe_allow_html=True)
|
st.markdown("</div>", unsafe_allow_html=True)
|
||||||
|
|
||||||
def render_chat_interface(top_k, explain):
|
def render_chat_interface(top_k, explain):
|
||||||
|
"""
|
||||||
|
Rendert den Chat-Verlauf und das Eingabefeld.
|
||||||
|
"""
|
||||||
for idx, msg in enumerate(st.session_state.messages):
|
for idx, msg in enumerate(st.session_state.messages):
|
||||||
with st.chat_message(msg["role"]):
|
with st.chat_message(msg["role"]):
|
||||||
if msg["role"] == "assistant":
|
if msg["role"] == "assistant":
|
||||||
|
# Intent Badge
|
||||||
intent = msg.get("intent", "UNKNOWN")
|
intent = msg.get("intent", "UNKNOWN")
|
||||||
src = msg.get("intent_source", "?")
|
src = msg.get("intent_source", "?")
|
||||||
icon = {"EMPATHY":"❤️", "DECISION":"⚖️", "CODING":"💻", "FACT":"📚", "INTERVIEW":"📝"}.get(intent, "🧠")
|
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)
|
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)
|
||||||
|
|
||||||
|
# Debug Info
|
||||||
with st.expander("🐞 Debug Raw Payload", expanded=False):
|
with st.expander("🐞 Debug Raw Payload", expanded=False):
|
||||||
st.json(msg)
|
st.json(msg)
|
||||||
|
|
||||||
|
# Special Renderers
|
||||||
if intent == "INTERVIEW":
|
if intent == "INTERVIEW":
|
||||||
render_draft_editor(msg)
|
render_draft_editor(msg)
|
||||||
else:
|
else:
|
||||||
st.markdown(msg["content"])
|
st.markdown(msg["content"])
|
||||||
|
|
||||||
|
# Quellen-Anzeige
|
||||||
if "sources" in msg and msg["sources"]:
|
if "sources" in msg and msg["sources"]:
|
||||||
for hit in msg["sources"]:
|
for hit in msg["sources"]:
|
||||||
with st.expander(f"📄 {hit.get('note_id', '?')} ({hit.get('total_score', 0):.2f})"):
|
with st.expander(f"📄 {hit.get('note_id', '?')} ({hit.get('total_score', 0):.2f})"):
|
||||||
st.markdown(f"_{hit.get('source', {}).get('text', '')[:300]}..._")
|
st.markdown(f"_{hit.get('source', {}).get('text', '')[:300]}..._")
|
||||||
if hit.get('explanation'):
|
if hit.get('explanation'):
|
||||||
st.caption(f"Grund: {hit['explanation']['reasons'][0]['message']}")
|
st.caption(f"Grund: {hit['explanation']['reasons'][0]['message']}")
|
||||||
|
|
||||||
|
# Source Feedback
|
||||||
def _cb(qid=msg.get("query_id"), nid=hit.get('node_id')):
|
def _cb(qid=msg.get("query_id"), nid=hit.get('node_id')):
|
||||||
val = st.session_state.get(f"fb_src_{qid}_{nid}")
|
val = st.session_state.get(f"fb_src_{qid}_{nid}")
|
||||||
if val is not None: submit_feedback(qid, nid, val+1)
|
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)
|
st.feedback("faces", key=f"fb_src_{msg.get('query_id')}_{hit.get('node_id')}", on_change=_cb)
|
||||||
|
|
||||||
|
# Global Feedback
|
||||||
if "query_id" in msg:
|
if "query_id" in msg:
|
||||||
qid = msg["query_id"]
|
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))
|
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:
|
else:
|
||||||
st.markdown(msg["content"])
|
st.markdown(msg["content"])
|
||||||
|
|
||||||
|
# Chat Input
|
||||||
if prompt := st.chat_input("Frage Mindnet..."):
|
if prompt := st.chat_input("Frage Mindnet..."):
|
||||||
st.session_state.messages.append({"role": "user", "content": prompt})
|
st.session_state.messages.append({"role": "user", "content": prompt})
|
||||||
st.rerun()
|
st.rerun()
|
||||||
|
|
||||||
|
# Antwort generieren (falls User zuletzt gefragt hat)
|
||||||
if len(st.session_state.messages) > 0 and st.session_state.messages[-1]["role"] == "user":
|
if len(st.session_state.messages) > 0 and st.session_state.messages[-1]["role"] == "user":
|
||||||
with st.chat_message("assistant"):
|
with st.chat_message("assistant"):
|
||||||
with st.spinner("Thinking..."):
|
with st.spinner("Thinking..."):
|
||||||
|
|
@ -238,6 +279,7 @@ def render_chat_interface(top_k, explain):
|
||||||
st.rerun()
|
st.rerun()
|
||||||
|
|
||||||
def render_manual_editor():
|
def render_manual_editor():
|
||||||
|
"""Rendert einen leeren Editor für manuelle Eingaben."""
|
||||||
mock_msg = {
|
mock_msg = {
|
||||||
"content": "---\ntype: concept\ntitle: Neue Notiz\nstatus: draft\ntags: []\n---\n# Titel\n",
|
"content": "---\ntype: concept\ntitle: Neue Notiz\nstatus: draft\ntags: []\n---\n# Titel\n",
|
||||||
"query_id": "manual_mode_v2"
|
"query_id": "manual_mode_v2"
|
||||||
|
|
@ -245,15 +287,24 @@ def render_manual_editor():
|
||||||
render_draft_editor(mock_msg)
|
render_draft_editor(mock_msg)
|
||||||
|
|
||||||
def render_graph_explorer(graph_service):
|
def render_graph_explorer(graph_service):
|
||||||
st.header("🕸️ Graph Explorer (WP-19)")
|
"""
|
||||||
|
Rendert den erweiterten Graph Explorer (WP-19).
|
||||||
|
"""
|
||||||
|
st.header("🕸️ Graph Explorer")
|
||||||
|
|
||||||
col_ctrl, col_graph = st.columns([1, 3])
|
# State Management für Graph Navigation
|
||||||
|
if "graph_center_id" not in st.session_state:
|
||||||
|
st.session_state.graph_center_id = None
|
||||||
|
|
||||||
|
col_ctrl, col_graph = st.columns([1, 4]) # Graph bekommt mehr Platz
|
||||||
|
|
||||||
with col_ctrl:
|
with col_ctrl:
|
||||||
st.subheader("Fokus setzen")
|
st.subheader("Fokus")
|
||||||
search_term = st.text_input("Suche Notiz (Titel)", placeholder="z.B. Project Alpha")
|
|
||||||
selected_note_id = None
|
|
||||||
|
|
||||||
|
# 1. Suchfeld mit Autocomplete-Logik
|
||||||
|
search_term = st.text_input("Suche Notiz", placeholder="Titel eingeben...")
|
||||||
|
|
||||||
|
options = {}
|
||||||
if search_term:
|
if search_term:
|
||||||
hits, _ = graph_service.client.scroll(
|
hits, _ = graph_service.client.scroll(
|
||||||
collection_name=f"{COLLECTION_PREFIX}_notes",
|
collection_name=f"{COLLECTION_PREFIX}_notes",
|
||||||
|
|
@ -263,43 +314,69 @@ def render_graph_explorer(graph_service):
|
||||||
limit=10
|
limit=10
|
||||||
)
|
)
|
||||||
options = {h.payload['title']: h.payload['note_id'] for h in hits}
|
options = {h.payload['title']: h.payload['note_id'] for h in hits}
|
||||||
if options:
|
|
||||||
selected_title = st.selectbox("Wähle Notiz:", list(options.keys()))
|
|
||||||
selected_note_id = options[selected_title]
|
|
||||||
else:
|
|
||||||
st.warning("Keine Notiz gefunden.")
|
|
||||||
|
|
||||||
st.markdown("---")
|
# 2. Auswahlliste
|
||||||
st.markdown("**Legende:**")
|
if options:
|
||||||
st.markdown(f"🔴 **Blocker** (Risk/Block)")
|
selected_title = st.selectbox("Ergebnisse:", list(options.keys()))
|
||||||
st.markdown(f"🔵 **Konzept/Struktur**")
|
if st.button("Laden", use_container_width=True):
|
||||||
st.markdown(f"🟣 **Entscheidung**")
|
st.session_state.graph_center_id = options[selected_title]
|
||||||
st.markdown(f"🟢 **Beitrag**")
|
st.rerun()
|
||||||
st.markdown(f"--- **Solid**: Explicit Link")
|
|
||||||
st.markdown(f"- - **Dashed**: Smart/AI Link")
|
st.divider()
|
||||||
|
|
||||||
|
# 3. Legende (Top Typen)
|
||||||
|
st.caption("Legende (Wichtigste Typen)")
|
||||||
|
# Wir zeigen nur die ersten 6 Farben an, um die UI nicht zu sprengen
|
||||||
|
for k, v in list(GRAPH_COLORS.items())[:8]:
|
||||||
|
st.markdown(f"<span style='color:{v}'>●</span> {k}", unsafe_allow_html=True)
|
||||||
|
st.caption("Weitere Farben siehe `ui_config.py`")
|
||||||
|
|
||||||
|
st.divider()
|
||||||
|
|
||||||
|
# 4. Tiefe Steuerung
|
||||||
|
depth_val = st.slider("Tiefe (Tier)", 1, 3, 2, help="Level 1 = Nachbarn, Level 2 = Nachbarn der Nachbarn")
|
||||||
|
|
||||||
with col_graph:
|
with col_graph:
|
||||||
if selected_note_id:
|
center_id = st.session_state.graph_center_id
|
||||||
with st.spinner(f"Lade Graph für {selected_note_id}..."):
|
|
||||||
nodes, edges = graph_service.get_ego_graph(selected_note_id)
|
if center_id:
|
||||||
|
with st.spinner(f"Lade Graph für {center_id} (Tiefe {depth_val})..."):
|
||||||
|
|
||||||
|
# Daten laden (mit Tiefe)
|
||||||
|
nodes, edges = graph_service.get_ego_graph(center_id, depth=depth_val)
|
||||||
|
|
||||||
if not nodes:
|
if not nodes:
|
||||||
st.error("Knoten konnte nicht geladen werden.")
|
st.warning("Keine Daten gefunden. Vielleicht existiert die Notiz nicht mehr?")
|
||||||
else:
|
else:
|
||||||
|
# CONFIG: Abstand und Physik optimiert für Lesbarkeit
|
||||||
config = Config(
|
config = Config(
|
||||||
width=900,
|
width=1000,
|
||||||
height=700,
|
height=750,
|
||||||
directed=True,
|
directed=True,
|
||||||
physics=True,
|
physics=True,
|
||||||
hierarchical=False,
|
hierarchical=False,
|
||||||
|
# Erweiterte Physik-Einstellungen
|
||||||
|
key="graph_view",
|
||||||
nodeHighlightBehavior=True,
|
nodeHighlightBehavior=True,
|
||||||
highlightColor="#F7A7A6",
|
highlightColor="#F7A7A6",
|
||||||
collapsible=False
|
collapsible=False,
|
||||||
|
# Tuning für Abstand:
|
||||||
|
gravity=-4000, # Starke Abstoßung (Minus-Wert)
|
||||||
|
central_gravity=0.3,# Zieht Nodes leicht zur Mitte
|
||||||
|
spring_length=250, # Längere Kanten für bessere Lesbarkeit
|
||||||
|
spring_strength=0.05,
|
||||||
|
damping=0.09
|
||||||
)
|
)
|
||||||
st.caption(f"Graph zeigt {len(nodes)} Knoten und {len(edges)} Kanten.")
|
|
||||||
|
st.caption(f"Zentrum: **{center_id}** | Knoten: {len(nodes)} | Kanten: {len(edges)}")
|
||||||
|
|
||||||
|
# Interaktion: Agraph gibt die ID des geklickten Nodes zurück
|
||||||
return_value = agraph(nodes=nodes, edges=edges, config=config)
|
return_value = agraph(nodes=nodes, edges=edges, config=config)
|
||||||
|
|
||||||
if return_value:
|
# NAVIGATION LOGIK
|
||||||
st.info(f"Auswahl: {return_value}")
|
# Wenn ein Node geklickt wurde UND es nicht der aktuelle Center ist -> Navigation
|
||||||
|
if return_value and return_value != center_id:
|
||||||
|
st.session_state.graph_center_id = return_value
|
||||||
|
st.rerun() # Refresh mit neuem Center
|
||||||
else:
|
else:
|
||||||
st.info("👈 Bitte wähle links eine Notiz aus, um den Graphen zu starten.")
|
st.info("👈 Wähle links eine Notiz, um den Graphen zu starten.")
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import os
|
import os
|
||||||
|
import hashlib
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
@ -23,27 +24,56 @@ timeout_setting = os.getenv("MINDNET_API_TIMEOUT") or os.getenv("MINDNET_LLM_TIM
|
||||||
API_TIMEOUT = float(timeout_setting) if timeout_setting else 300.0
|
API_TIMEOUT = float(timeout_setting) if timeout_setting else 300.0
|
||||||
|
|
||||||
# --- STYLING CONSTANTS ---
|
# --- STYLING CONSTANTS ---
|
||||||
|
|
||||||
|
# Basierend auf types.yaml
|
||||||
GRAPH_COLORS = {
|
GRAPH_COLORS = {
|
||||||
"project": "#ff9f43", # Orange
|
# Kerntypen
|
||||||
"concept": "#54a0ff", # Blau
|
"experience": "#feca57", # Gelb/Orange
|
||||||
"decision": "#5f27cd", # Lila
|
"project": "#ff9f43", # Dunkleres Orange
|
||||||
"risk": "#ff6b6b", # Rot
|
"decision": "#5f27cd", # Lila
|
||||||
"person": "#1dd1a1", # Grün
|
|
||||||
"experience": "#feca57",# Gelb
|
# Persönlichkeit
|
||||||
"value": "#00d2d3", # Cyan
|
"value": "#00d2d3", # Cyan
|
||||||
"goal": "#ff9ff3", # Pink
|
"principle": "#0abde3", # Dunkles Cyan
|
||||||
"default": "#8395a7" # Grau
|
"belief": "#48dbfb", # Helles Blau
|
||||||
|
"profile": "#1dd1a1", # Grün
|
||||||
|
|
||||||
|
# Strategie & Risiko
|
||||||
|
"goal": "#ff9ff3", # Pink
|
||||||
|
"risk": "#ff6b6b", # Rot
|
||||||
|
|
||||||
|
# Basis
|
||||||
|
"concept": "#54a0ff", # Blau
|
||||||
|
"task": "#8395a7", # Grau-Blau
|
||||||
|
"journal": "#c8d6e5", # Hellgrau
|
||||||
|
"source": "#576574", # Dunkelgrau
|
||||||
|
"glossary": "#222f3e", # Sehr dunkel
|
||||||
|
|
||||||
|
"default": "#8395a7" # Fallback
|
||||||
}
|
}
|
||||||
|
|
||||||
EDGE_COLORS = {
|
# System-Kanten, die wir NICHT im Graphen sehen wollen, um Rauschen zu reduzieren
|
||||||
"depends_on": "#ff6b6b", # Rot (Blocker)
|
SYSTEM_EDGES = ["prev", "next", "belongs_to"]
|
||||||
"blocks": "#ee5253", # Dunkelrot
|
|
||||||
"caused_by": "#ff9ff3", # Pink
|
def get_edge_color(kind: str) -> str:
|
||||||
"related_to": "#c8d6e5", # Hellgrau
|
"""Generiert eine deterministische Farbe basierend auf dem Edge-Typ."""
|
||||||
"similar_to": "#c8d6e5", # Hellgrau
|
if not kind: return "#bdc3c7"
|
||||||
"next": "#54a0ff", # Blau
|
|
||||||
"derived_from": "#ff9ff3", # Pink
|
# Einige feste Farben für wichtige semantische Typen
|
||||||
"references": "#bdc3c7", # Grau
|
fixed_colors = {
|
||||||
"belongs_to": "#2e86de", # Dunkelblau
|
"depends_on": "#ff6b6b", # Rot (Blocker/Abhängigkeit)
|
||||||
"contributes_to": "#1dd1a1" # Grün (Neu!)
|
"blocks": "#ee5253", # Dunkelrot
|
||||||
}
|
"caused_by": "#ff9ff3", # Pink
|
||||||
|
"related_to": "#c8d6e5", # Hellgrau (Hintergrund)
|
||||||
|
"references": "#bdc3c7", # Grau
|
||||||
|
"derived_from": "#1dd1a1" # Grün
|
||||||
|
}
|
||||||
|
|
||||||
|
if kind in fixed_colors:
|
||||||
|
return fixed_colors[kind]
|
||||||
|
|
||||||
|
# Fallback: Hash-basierte Farbe für dynamische Typen
|
||||||
|
# Wir nutzen einen Pastell-Generator, damit es nicht zu grell wird
|
||||||
|
hash_obj = hashlib.md5(kind.encode())
|
||||||
|
hue = int(hash_obj.hexdigest(), 16) % 360
|
||||||
|
return f"hsl({hue}, 60%, 50%)"
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
from qdrant_client import QdrantClient, models
|
from qdrant_client import QdrantClient, models
|
||||||
from streamlit_agraph import Node, Edge
|
from streamlit_agraph import Node, Edge
|
||||||
from ui_config import GRAPH_COLORS, EDGE_COLORS
|
from ui_config import GRAPH_COLORS, get_edge_color, SYSTEM_EDGES
|
||||||
|
|
||||||
class GraphExplorerService:
|
class GraphExplorerService:
|
||||||
def __init__(self, url, api_key=None, prefix="mindnet"):
|
def __init__(self, url, api_key=None, prefix="mindnet"):
|
||||||
|
|
@ -11,100 +11,53 @@ class GraphExplorerService:
|
||||||
self.edges_col = f"{prefix}_edges"
|
self.edges_col = f"{prefix}_edges"
|
||||||
self._note_cache = {}
|
self._note_cache = {}
|
||||||
|
|
||||||
def get_ego_graph(self, center_note_id: str):
|
def get_ego_graph(self, center_note_id: str, depth=2):
|
||||||
nodes_dict = {}
|
nodes_dict = {}
|
||||||
unique_edges = {}
|
unique_edges = {}
|
||||||
|
|
||||||
# 1. Center Note laden
|
# --- LEVEL 1: Center & direkte Nachbarn ---
|
||||||
|
|
||||||
|
# 1. Center Note
|
||||||
center_note = self._fetch_note_cached(center_note_id)
|
center_note = self._fetch_note_cached(center_note_id)
|
||||||
if not center_note: return [], []
|
if not center_note: return [], []
|
||||||
self._add_node_to_dict(nodes_dict, center_note, is_center=True)
|
self._add_node_to_dict(nodes_dict, center_note, level=0)
|
||||||
|
|
||||||
center_title = center_note.get("title")
|
# Wir sammeln IDs für Level 2 Suche
|
||||||
|
level_1_ids = {center_note_id}
|
||||||
|
|
||||||
# 2. Chunks der Center Note finden
|
# Suche Kanten für Center
|
||||||
scroll_filter = models.Filter(
|
l1_edges = self._find_connected_edges([center_note_id], center_note.get("title"))
|
||||||
must=[models.FieldCondition(key="note_id", match=models.MatchValue(value=center_note_id))]
|
|
||||||
)
|
|
||||||
chunks, _ = self.client.scroll(
|
|
||||||
collection_name=self.chunks_col, scroll_filter=scroll_filter, limit=100, with_payload=True
|
|
||||||
)
|
|
||||||
center_chunk_ids = [c.id for c in chunks]
|
|
||||||
|
|
||||||
raw_edges = []
|
# Verarbeite L1 Kanten
|
||||||
|
for edge_data in l1_edges:
|
||||||
|
src_id, tgt_id = self._process_edge(edge_data, nodes_dict, unique_edges, current_depth=1)
|
||||||
|
if src_id: level_1_ids.add(src_id)
|
||||||
|
if tgt_id: level_1_ids.add(tgt_id)
|
||||||
|
|
||||||
# 3. OUTGOING EDGES: Source = einer meiner Chunks
|
# --- LEVEL 2: Nachbarn der Nachbarn ---
|
||||||
if center_chunk_ids:
|
if depth > 1 and level_1_ids:
|
||||||
out_filter = models.Filter(
|
# Wir suchen Kanten, bei denen Source oder Target einer der L1 Nodes ist
|
||||||
must=[models.FieldCondition(key="source_id", match=models.MatchAny(any=center_chunk_ids))]
|
# Wichtig: Wir filtern System-Edges schon in der Query oder Python, um Traffic zu sparen
|
||||||
)
|
|
||||||
res_out, _ = self.client.scroll(
|
|
||||||
collection_name=self.edges_col, scroll_filter=out_filter, limit=100, with_payload=True
|
|
||||||
)
|
|
||||||
raw_edges.extend(res_out)
|
|
||||||
|
|
||||||
# 4. INCOMING EDGES: Target = Chunk, Titel oder Note-ID
|
# Um die Performance zu wahren, limitieren wir die L2 Suche auf die IDs, die wir schon haben (als Source)
|
||||||
must_conditions = []
|
# Das ist ein "Ego-Network" Ansatz.
|
||||||
if center_chunk_ids:
|
|
||||||
must_conditions.append(models.FieldCondition(key="target_id", match=models.MatchAny(any=center_chunk_ids)))
|
|
||||||
if center_title:
|
|
||||||
must_conditions.append(models.FieldCondition(key="target_id", match=models.MatchValue(value=center_title)))
|
|
||||||
|
|
||||||
# FIX: Auch exakte Note-ID als Target prüfen
|
# Wir nehmen alle IDs aus Level 1 (außer Center, das haben wir schon)
|
||||||
must_conditions.append(models.FieldCondition(key="target_id", match=models.MatchValue(value=center_note_id)))
|
l1_subset = list(level_1_ids - {center_note_id})
|
||||||
|
|
||||||
if must_conditions:
|
if l1_subset:
|
||||||
in_filter = models.Filter(should=must_conditions) # 'should' = OR
|
l2_edges = self._find_connected_edges_batch(l1_subset)
|
||||||
res_in, _ = self.client.scroll(
|
for edge_data in l2_edges:
|
||||||
collection_name=self.edges_col, scroll_filter=in_filter, limit=100, with_payload=True
|
self._process_edge(edge_data, nodes_dict, unique_edges, current_depth=2)
|
||||||
)
|
|
||||||
raw_edges.extend(res_in)
|
|
||||||
|
|
||||||
# 5. Verarbeitung & Auflösung
|
# --- GRAPH CONSTRUCTION ---
|
||||||
for record in raw_edges:
|
|
||||||
payload = record.payload
|
|
||||||
|
|
||||||
src_ref = payload.get("source_id")
|
|
||||||
tgt_ref = payload.get("target_id")
|
|
||||||
kind = payload.get("kind", "related_to")
|
|
||||||
provenance = payload.get("provenance", "explicit")
|
|
||||||
|
|
||||||
src_note = self._resolve_note_from_ref(src_ref)
|
|
||||||
tgt_note = self._resolve_note_from_ref(tgt_ref)
|
|
||||||
|
|
||||||
if src_note and tgt_note:
|
|
||||||
src_id = src_note['note_id']
|
|
||||||
tgt_id = tgt_note['note_id']
|
|
||||||
|
|
||||||
# Keine Self-Loops und valide Verbindung
|
|
||||||
if src_id != tgt_id:
|
|
||||||
self._add_node_to_dict(nodes_dict, src_note)
|
|
||||||
self._add_node_to_dict(nodes_dict, tgt_note)
|
|
||||||
|
|
||||||
key = (src_id, tgt_id)
|
|
||||||
existing = unique_edges.get(key)
|
|
||||||
|
|
||||||
# Deduplizierung: Explizite Kanten überschreiben Smart Edges
|
|
||||||
is_current_explicit = (provenance == "explicit" or provenance == "rule")
|
|
||||||
should_update = True
|
|
||||||
|
|
||||||
if existing:
|
|
||||||
is_existing_explicit = (existing['provenance'] == "explicit" or existing['provenance'] == "rule")
|
|
||||||
if is_existing_explicit and not is_current_explicit:
|
|
||||||
should_update = False
|
|
||||||
|
|
||||||
if should_update:
|
|
||||||
unique_edges[key] = {
|
|
||||||
"source": src_id, "target": tgt_id, "kind": kind, "provenance": provenance
|
|
||||||
}
|
|
||||||
|
|
||||||
# 6. Agraph Objekte bauen
|
|
||||||
final_edges = []
|
final_edges = []
|
||||||
for (src, tgt), data in unique_edges.items():
|
for (src, tgt), data in unique_edges.items():
|
||||||
kind = data['kind']
|
kind = data['kind']
|
||||||
prov = data['provenance']
|
prov = data['provenance']
|
||||||
|
|
||||||
color = EDGE_COLORS.get(kind, "#bdc3c7")
|
# Dynamische Farbe holen
|
||||||
|
color = get_edge_color(kind)
|
||||||
is_smart = (prov != "explicit" and prov != "rule")
|
is_smart = (prov != "explicit" and prov != "rule")
|
||||||
|
|
||||||
final_edges.append(Edge(
|
final_edges.append(Edge(
|
||||||
|
|
@ -114,9 +67,92 @@ class GraphExplorerService:
|
||||||
|
|
||||||
return list(nodes_dict.values()), final_edges
|
return list(nodes_dict.values()), final_edges
|
||||||
|
|
||||||
|
def _find_connected_edges(self, note_ids, note_title=None):
|
||||||
|
"""Findet In- und Outgoing Edges für eine Liste von Note-IDs."""
|
||||||
|
# 1. Chunks zu diesen Notes finden
|
||||||
|
scroll_filter = models.Filter(
|
||||||
|
must=[models.FieldCondition(key="note_id", match=models.MatchAny(any=note_ids))]
|
||||||
|
)
|
||||||
|
chunks, _ = self.client.scroll(
|
||||||
|
collection_name=self.chunks_col, scroll_filter=scroll_filter, limit=200
|
||||||
|
)
|
||||||
|
chunk_ids = [c.id for c in chunks]
|
||||||
|
|
||||||
|
results = []
|
||||||
|
|
||||||
|
# Outgoing (Source is Chunk)
|
||||||
|
if chunk_ids:
|
||||||
|
out_f = models.Filter(must=[
|
||||||
|
models.FieldCondition(key="source_id", match=models.MatchAny(any=chunk_ids)),
|
||||||
|
# Filter System Edges
|
||||||
|
models.FieldCondition(key="kind", match=models.MatchExcept(except_=SYSTEM_EDGES))
|
||||||
|
])
|
||||||
|
res_out, _ = self.client.scroll(self.edges_col, scroll_filter=out_f, limit=100, with_payload=True)
|
||||||
|
results.extend(res_out)
|
||||||
|
|
||||||
|
# Incoming (Target is Chunk OR Title OR NoteID)
|
||||||
|
shoulds = []
|
||||||
|
if chunk_ids: shoulds.append(models.FieldCondition(key="target_id", match=models.MatchAny(any=chunk_ids)))
|
||||||
|
if note_title: shoulds.append(models.FieldCondition(key="target_id", match=models.MatchValue(value=note_title)))
|
||||||
|
shoulds.append(models.FieldCondition(key="target_id", match=models.MatchAny(any=note_ids)))
|
||||||
|
|
||||||
|
if shoulds:
|
||||||
|
in_f = models.Filter(
|
||||||
|
must=[models.FieldCondition(key="kind", match=models.MatchExcept(except_=SYSTEM_EDGES))],
|
||||||
|
should=shoulds
|
||||||
|
)
|
||||||
|
res_in, _ = self.client.scroll(self.edges_col, scroll_filter=in_f, limit=100, with_payload=True)
|
||||||
|
results.extend(res_in)
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
def _find_connected_edges_batch(self, note_ids):
|
||||||
|
"""Batch-Suche für Level 2 (nur ausgehend und eingehend auf Note-Ebene, keine Title-Suche für Performance)."""
|
||||||
|
# Vereinfachte Suche: Wir suchen Kanten, die direkt mit den note_ids (oder deren Chunks) zu tun haben
|
||||||
|
# Um Performance zu sparen, machen wir hier einen simpleren Lookup, wenn möglich.
|
||||||
|
return self._find_connected_edges(note_ids)
|
||||||
|
|
||||||
|
def _process_edge(self, record, nodes_dict, unique_edges, current_depth):
|
||||||
|
payload = record.payload
|
||||||
|
src_ref = payload.get("source_id")
|
||||||
|
tgt_ref = payload.get("target_id")
|
||||||
|
kind = payload.get("kind")
|
||||||
|
provenance = payload.get("provenance", "explicit")
|
||||||
|
|
||||||
|
# Resolve
|
||||||
|
src_note = self._resolve_note_from_ref(src_ref)
|
||||||
|
tgt_note = self._resolve_note_from_ref(tgt_ref)
|
||||||
|
|
||||||
|
if src_note and tgt_note:
|
||||||
|
src_id = src_note['note_id']
|
||||||
|
tgt_id = tgt_note['note_id']
|
||||||
|
|
||||||
|
if src_id != tgt_id:
|
||||||
|
# Add Nodes
|
||||||
|
self._add_node_to_dict(nodes_dict, src_note, level=current_depth)
|
||||||
|
self._add_node_to_dict(nodes_dict, tgt_note, level=current_depth)
|
||||||
|
|
||||||
|
# Add Edge (Deduplication Logic)
|
||||||
|
key = (src_id, tgt_id)
|
||||||
|
existing = unique_edges.get(key)
|
||||||
|
|
||||||
|
# Update logic: Explicit > Smart
|
||||||
|
should_update = True
|
||||||
|
is_current_explicit = (provenance in ["explicit", "rule"])
|
||||||
|
if existing:
|
||||||
|
is_existing_explicit = (existing['provenance'] in ["explicit", "rule"])
|
||||||
|
if is_existing_explicit and not is_current_explicit:
|
||||||
|
should_update = False
|
||||||
|
|
||||||
|
if should_update:
|
||||||
|
unique_edges[key] = {
|
||||||
|
"source": src_id, "target": tgt_id, "kind": kind, "provenance": provenance
|
||||||
|
}
|
||||||
|
return src_id, tgt_id
|
||||||
|
return None, None
|
||||||
|
|
||||||
def _fetch_note_cached(self, note_id):
|
def _fetch_note_cached(self, note_id):
|
||||||
if note_id in self._note_cache: return self._note_cache[note_id]
|
if note_id in self._note_cache: return self._note_cache[note_id]
|
||||||
|
|
||||||
res, _ = self.client.scroll(
|
res, _ = self.client.scroll(
|
||||||
collection_name=self.notes_col,
|
collection_name=self.notes_col,
|
||||||
scroll_filter=models.Filter(must=[models.FieldCondition(key="note_id", match=models.MatchValue(value=note_id))]),
|
scroll_filter=models.Filter(must=[models.FieldCondition(key="note_id", match=models.MatchValue(value=note_id))]),
|
||||||
|
|
@ -129,53 +165,57 @@ class GraphExplorerService:
|
||||||
|
|
||||||
def _resolve_note_from_ref(self, ref_str):
|
def _resolve_note_from_ref(self, ref_str):
|
||||||
if not ref_str: return None
|
if not ref_str: return None
|
||||||
|
# ... (Logik identisch zu vorher, hier gekürzt für Übersicht)
|
||||||
# Fall A: Chunk ID (Format: note_id#cXX)
|
# Fall A: Chunk ID / Section
|
||||||
if "#" in ref_str:
|
if "#" in ref_str:
|
||||||
# Versuch 1: Echte Chunk ID in DB
|
|
||||||
try:
|
try:
|
||||||
res = self.client.retrieve(collection_name=self.chunks_col, ids=[ref_str], with_payload=True)
|
res = self.client.retrieve(self.chunks_col, ids=[ref_str], with_payload=True)
|
||||||
if res:
|
if res: return self._fetch_note_cached(res[0].payload.get("note_id"))
|
||||||
parent_id = res[0].payload.get("note_id")
|
|
||||||
return self._fetch_note_cached(parent_id)
|
|
||||||
except: pass
|
except: pass
|
||||||
|
|
||||||
# Versuch 2: Section Link (note-id#Header) -> Hash abschneiden
|
|
||||||
possible_note_id = ref_str.split("#")[0]
|
possible_note_id = ref_str.split("#")[0]
|
||||||
note_by_id = self._fetch_note_cached(possible_note_id)
|
if self._fetch_note_cached(possible_note_id): return self._fetch_note_cached(possible_note_id)
|
||||||
if note_by_id: return note_by_id
|
|
||||||
|
|
||||||
# Fall B: Es ist direkt die Note ID
|
# Fall B: Note ID
|
||||||
note_by_id = self._fetch_note_cached(ref_str)
|
if self._fetch_note_cached(ref_str): return self._fetch_note_cached(ref_str)
|
||||||
if note_by_id: return note_by_id
|
|
||||||
|
|
||||||
# Fall C: Es ist der Titel (Wikilink)
|
# Fall C: Titel
|
||||||
res, _ = self.client.scroll(
|
res, _ = self.client.scroll(
|
||||||
collection_name=self.notes_col,
|
collection_name=self.notes_col,
|
||||||
scroll_filter=models.Filter(must=[models.FieldCondition(key="title", match=models.MatchValue(value=ref_str))]),
|
scroll_filter=models.Filter(must=[models.FieldCondition(key="title", match=models.MatchValue(value=ref_str))]),
|
||||||
limit=1, with_payload=True
|
limit=1, with_payload=True
|
||||||
)
|
)
|
||||||
if res:
|
if res:
|
||||||
p = res[0].payload
|
self._note_cache[res[0].payload['note_id']] = res[0].payload
|
||||||
self._note_cache[p['note_id']] = p
|
return res[0].payload
|
||||||
return p
|
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _add_node_to_dict(self, node_dict, note_payload, is_center=False):
|
def _add_node_to_dict(self, node_dict, note_payload, level=1):
|
||||||
nid = note_payload.get("note_id")
|
nid = note_payload.get("note_id")
|
||||||
|
|
||||||
|
# Wenn Node schon da ist, aber wir finden ihn auf einem "höheren" Level (näher am Zentrum), updaten wir ihn nicht zwingend,
|
||||||
|
# außer wir wollen visuelle Eigenschaften ändern.
|
||||||
if nid in node_dict: return
|
if nid in node_dict: return
|
||||||
|
|
||||||
ntype = note_payload.get("type", "default")
|
ntype = note_payload.get("type", "default")
|
||||||
color = GRAPH_COLORS.get(ntype, GRAPH_COLORS["default"])
|
color = GRAPH_COLORS.get(ntype, GRAPH_COLORS["default"])
|
||||||
size = 35 if is_center else 20
|
|
||||||
|
# Größe & Label basierend auf Level
|
||||||
|
if level == 0:
|
||||||
|
size = 40
|
||||||
|
label_prefix = ""
|
||||||
|
elif level == 1:
|
||||||
|
size = 25
|
||||||
|
label_prefix = ""
|
||||||
|
else:
|
||||||
|
size = 15 # Level 2 kleiner
|
||||||
|
label_prefix = ""
|
||||||
|
|
||||||
node_dict[nid] = Node(
|
node_dict[nid] = Node(
|
||||||
id=nid,
|
id=nid,
|
||||||
label=note_payload.get("title", nid),
|
label=f"{label_prefix}{note_payload.get('title', nid)}",
|
||||||
size=size,
|
size=size,
|
||||||
color=color,
|
color=color,
|
||||||
shape="dot" if not is_center else "diamond",
|
shape="dot" if level > 0 else "diamond",
|
||||||
title=f"Type: {ntype}\nTags: {note_payload.get('tags')}",
|
title=f"Type: {ntype}\nLevel: {level}\nTags: {note_payload.get('tags')}",
|
||||||
font={'color': 'black'}
|
font={'color': 'black', 'face': 'arial', 'size': 14 if level < 2 else 10}
|
||||||
)
|
)
|
||||||
Loading…
Reference in New Issue
Block a user