layout verbesserung

This commit is contained in:
Lars 2025-12-14 11:27:43 +01:00
parent 537a2883bf
commit f1bcbb1543
2 changed files with 47 additions and 80 deletions

View File

@ -11,10 +11,12 @@ from ui_api import save_draft_to_vault, analyze_draft_text, send_chat_message, s
from ui_config import HISTORY_FILE, COLLECTION_PREFIX, GRAPH_COLORS from ui_config import HISTORY_FILE, COLLECTION_PREFIX, GRAPH_COLORS
def render_sidebar(): def render_sidebar():
"""
Rendert die Sidebar mit Modus-Auswahl und Verlauf.
"""
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()
@ -24,12 +26,10 @@ def render_sidebar():
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):
@ -67,7 +67,6 @@ def render_draft_editor(msg):
st.session_state[f"{key_base}_init"] = True st.session_state[f"{key_base}_init"] = True
# --- STATE 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]
@ -100,7 +99,6 @@ def render_draft_editor(msg):
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)
@ -112,7 +110,6 @@ 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:
@ -136,7 +133,6 @@ 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, "")
@ -144,7 +140,6 @@ def render_draft_editor(msg):
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"
@ -161,7 +156,6 @@ def render_draft_editor(msg):
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()]
@ -174,7 +168,6 @@ def render_draft_editor(msg):
} }
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()
@ -188,7 +181,6 @@ def render_draft_editor(msg):
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"):
@ -218,23 +210,19 @@ def render_chat_interface(top_k, explain):
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})"):
@ -242,25 +230,21 @@ def render_chat_interface(top_k, explain):
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..."):
@ -296,12 +280,11 @@ def render_graph_explorer(graph_service):
if "graph_center_id" not in st.session_state: if "graph_center_id" not in st.session_state:
st.session_state.graph_center_id = None st.session_state.graph_center_id = None
col_ctrl, col_graph = st.columns([1, 4]) # Graph bekommt mehr Platz col_ctrl, col_graph = st.columns([1, 4])
with col_ctrl: with col_ctrl:
st.subheader("Fokus") st.subheader("Fokus")
# 1. Suchfeld mit Autocomplete-Logik
search_term = st.text_input("Suche Notiz", placeholder="Titel eingeben...") search_term = st.text_input("Suche Notiz", placeholder="Titel eingeben...")
options = {} options = {}
@ -315,7 +298,6 @@ def render_graph_explorer(graph_service):
) )
options = {h.payload['title']: h.payload['note_id'] for h in hits} options = {h.payload['title']: h.payload['note_id'] for h in hits}
# 2. Auswahlliste
if options: if options:
selected_title = st.selectbox("Ergebnisse:", list(options.keys())) selected_title = st.selectbox("Ergebnisse:", list(options.keys()))
if st.button("Laden", use_container_width=True): if st.button("Laden", use_container_width=True):
@ -323,18 +305,14 @@ def render_graph_explorer(graph_service):
st.rerun() st.rerun()
st.divider() st.divider()
# 3. Legende (Top Typen)
st.caption("Legende (Wichtigste 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]: for k, v in list(GRAPH_COLORS.items())[:8]:
st.markdown(f"<span style='color:{v}'>●</span> {k}", unsafe_allow_html=True) st.markdown(f"<span style='color:{v}'>●</span> {k}", unsafe_allow_html=True)
st.caption("Weitere Farben siehe `ui_config.py`") st.caption("Weitere Farben siehe `ui_config.py`")
st.divider() st.divider()
# 4. Tiefe Steuerung
depth_val = st.slider("Tiefe (Tier)", 1, 3, 2, help="Level 1 = Nachbarn, Level 2 = Nachbarn der Nachbarn") depth_val = st.slider("Tiefe (Tier)", 1, 3, 2, help="Level 1 = Nachbarn, Level 2 = Nachbarn der Nachbarn")
st.info("💡 Tipp: Ein Klick auf einen Knoten zentriert die Ansicht neu.")
with col_graph: with col_graph:
center_id = st.session_state.graph_center_id center_id = st.session_state.graph_center_id
@ -342,41 +320,45 @@ def render_graph_explorer(graph_service):
if center_id: if center_id:
with st.spinner(f"Lade Graph für {center_id} (Tiefe {depth_val})..."): 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) nodes, edges = graph_service.get_ego_graph(center_id, depth=depth_val)
if not nodes: if not nodes:
st.warning("Keine Daten gefunden. Vielleicht existiert die Notiz nicht mehr?") st.warning("Keine Daten gefunden. Vielleicht existiert die Notiz nicht mehr?")
else: else:
# CONFIG: Abstand und Physik optimiert für Lesbarkeit # CONFIG: ForceAtlas2Based für maximale Entzerrung
config = Config( config = Config(
width=1000, width=1000,
height=750, height=800,
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: # Solver Wechsel: ForceAtlas2Based ist besser für Entzerrung
gravity=-4000, # Starke Abstoßung (Minus-Wert) solver="forceAtlas2Based",
central_gravity=0.3,# Zieht Nodes leicht zur Mitte forceAtlas2Based={
spring_length=250, # Längere Kanten für bessere Lesbarkeit "theta": 0.5,
spring_strength=0.05, "gravitationalConstant": -100, # Starke Abstoßung
damping=0.09 "centralGravity": 0.005, # Sehr schwacher Zug zur Mitte (verhindert Klumpen)
"springConstant": 0.08,
"springLength": 150, # Längere Kanten
"damping": 0.4,
"avoidOverlap": 1 # Versucht aktiv, Überlappungen zu vermeiden
},
stabilization={
"enabled": True,
"iterations": 1000
}
) )
st.caption(f"Zentrum: **{center_id}** | Knoten: {len(nodes)} | Kanten: {len(edges)}") 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)
# NAVIGATION LOGIK # NAVIGATION LOGIK
# Wenn ein Node geklickt wurde UND es nicht der aktuelle Center ist -> Navigation
if return_value and return_value != center_id: if return_value and return_value != center_id:
st.session_state.graph_center_id = return_value st.session_state.graph_center_id = return_value
st.rerun() # Refresh mit neuem Center st.rerun()
else: else:
st.info("👈 Wähle links eine Notiz, um den Graphen zu starten.") st.info("👈 Wähle links eine Notiz, um den Graphen zu starten.")

View File

@ -15,9 +15,7 @@ class GraphExplorerService:
nodes_dict = {} nodes_dict = {}
unique_edges = {} unique_edges = {}
# --- LEVEL 1: Center & direkte Nachbarn --- # 1. Center Note laden
# 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, level=0) self._add_node_to_dict(nodes_dict, center_note, level=0)
@ -34,23 +32,19 @@ class GraphExplorerService:
if src_id: level_1_ids.add(src_id) if src_id: level_1_ids.add(src_id)
if tgt_id: level_1_ids.add(tgt_id) if tgt_id: level_1_ids.add(tgt_id)
# --- LEVEL 2: Nachbarn der Nachbarn --- # Level 2 Suche (begrenzt, um Chaos zu vermeiden)
if depth > 1 and level_1_ids: if depth > 1 and len(level_1_ids) > 1 and len(level_1_ids) < 40: # Limit für Performance
# Wir nehmen alle IDs aus Level 1 (außer Center, das haben wir schon)
l1_subset = list(level_1_ids - {center_note_id}) l1_subset = list(level_1_ids - {center_note_id})
if l1_subset: if l1_subset:
l2_edges = self._find_connected_edges_batch(l1_subset) l2_edges = self._find_connected_edges_batch(l1_subset)
for edge_data in l2_edges: for edge_data in l2_edges:
self._process_edge(edge_data, nodes_dict, unique_edges, current_depth=2) self._process_edge(edge_data, nodes_dict, unique_edges, current_depth=2)
# --- GRAPH CONSTRUCTION --- # Graphen 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']
# Dynamische Farbe holen
color = get_edge_color(kind) color = get_edge_color(kind)
is_smart = (prov != "explicit" and prov != "rule") is_smart = (prov != "explicit" and prov != "rule")
@ -62,8 +56,8 @@ 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): def _find_connected_edges(self, note_ids, note_title=None):
"""Findet In- und Outgoing Edges für eine Liste von Note-IDs.""" """Findet In- und Outgoing Edges."""
# 1. Chunks zu diesen Notes finden # Chunks finden
scroll_filter = models.Filter( scroll_filter = models.Filter(
must=[models.FieldCondition(key="note_id", match=models.MatchAny(any=note_ids))] must=[models.FieldCondition(key="note_id", match=models.MatchAny(any=note_ids))]
) )
@ -74,25 +68,29 @@ class GraphExplorerService:
results = [] results = []
# Outgoing (Source is Chunk) # --- OUTGOING SEARCH (Quelle = Chunk ODER Note) ---
if chunk_ids: # FIX: Wir suchen jetzt auch nach der note_id als source_id, falls Edges direkt an der Note hängen
source_candidates = chunk_ids + note_ids
if source_candidates:
out_f = models.Filter(must=[ out_f = models.Filter(must=[
models.FieldCondition(key="source_id", match=models.MatchAny(any=chunk_ids)), models.FieldCondition(key="source_id", match=models.MatchAny(any=source_candidates)),
# FIX: MatchExcept mit **kwargs nutzen wegen 'except' Keyword # FIX: Pydantic "except" keyword workaround
models.FieldCondition(key="kind", match=models.MatchExcept(**{"except": 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) res_out, _ = self.client.scroll(self.edges_col, scroll_filter=out_f, limit=100, with_payload=True)
results.extend(res_out) results.extend(res_out)
# Incoming (Target is Chunk OR Title OR NoteID) # --- INCOMING SEARCH (Ziel = Chunk ODER Title ODER Note) ---
shoulds = [] shoulds = []
if chunk_ids: shoulds.append(models.FieldCondition(key="target_id", match=models.MatchAny(any=chunk_ids))) 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))) if note_title: shoulds.append(models.FieldCondition(key="target_id", match=models.MatchValue(value=note_title)))
# Target = Note ID
shoulds.append(models.FieldCondition(key="target_id", match=models.MatchAny(any=note_ids))) shoulds.append(models.FieldCondition(key="target_id", match=models.MatchAny(any=note_ids)))
if shoulds: if shoulds:
in_f = models.Filter( in_f = models.Filter(
# FIX: MatchExcept mit **kwargs nutzen wegen 'except' Keyword # FIX: Pydantic "except" keyword workaround
must=[models.FieldCondition(key="kind", match=models.MatchExcept(**{"except": SYSTEM_EDGES}))], must=[models.FieldCondition(key="kind", match=models.MatchExcept(**{"except": SYSTEM_EDGES}))],
should=shoulds should=shoulds
) )
@ -102,7 +100,7 @@ class GraphExplorerService:
return results return results
def _find_connected_edges_batch(self, note_ids): def _find_connected_edges_batch(self, note_ids):
"""Batch-Suche für Level 2 (nur ausgehend und eingehend auf Note-Ebene).""" """Batch-Suche für Level 2."""
return self._find_connected_edges(note_ids) return self._find_connected_edges(note_ids)
def _process_edge(self, record, nodes_dict, unique_edges, current_depth): def _process_edge(self, record, nodes_dict, unique_edges, current_depth):
@ -112,7 +110,6 @@ class GraphExplorerService:
kind = payload.get("kind") kind = payload.get("kind")
provenance = payload.get("provenance", "explicit") provenance = payload.get("provenance", "explicit")
# Resolve
src_note = self._resolve_note_from_ref(src_ref) src_note = self._resolve_note_from_ref(src_ref)
tgt_note = self._resolve_note_from_ref(tgt_ref) tgt_note = self._resolve_note_from_ref(tgt_ref)
@ -121,15 +118,12 @@ class GraphExplorerService:
tgt_id = tgt_note['note_id'] tgt_id = tgt_note['note_id']
if src_id != tgt_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, src_note, level=current_depth)
self._add_node_to_dict(nodes_dict, tgt_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) key = (src_id, tgt_id)
existing = unique_edges.get(key) existing = unique_edges.get(key)
# Update logic: Explicit > Smart
should_update = True should_update = True
is_current_explicit = (provenance in ["explicit", "rule"]) is_current_explicit = (provenance in ["explicit", "rule"])
if existing: if existing:
@ -184,31 +178,22 @@ class GraphExplorerService:
def _add_node_to_dict(self, node_dict, note_payload, level=1): 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"])
# Größe & Label basierend auf Level # Size Adjustment für Hierarchie
if level == 0: if level == 0: size = 45
size = 40 elif level == 1: size = 25
label_prefix = "" else: size = 15
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=f"{label_prefix}{note_payload.get('title', nid)}", label=note_payload.get('title', nid),
size=size, size=size,
color=color, color=color,
shape="dot" if level > 0 else "diamond", shape="dot" if level > 0 else "diamond",
title=f"Type: {ntype}\nLevel: {level}\nTags: {note_payload.get('tags')}", title=f"Type: {ntype}\nTags: {note_payload.get('tags')}",
font={'color': 'black', 'face': 'arial', 'size': 14 if level < 2 else 10} font={'color': 'black', 'face': 'arial', 'size': 14 if level < 2 else 0}
) )