OverviewFilesCommitsIssuesDocsReleasesPeers

godot-keet commits / 96da1b3

Graph algo selector + cluster labels from MetadataBase
- keet_graph.gd: compute/get_clusters/get_edges accept algo Dictionary - graph_panel: OptionButton with 4 presets (members/topics/category/combined) - graph_canvas: draw cluster labels at centroids using cluster color - room_list_panel: show MetadataBase label instead of "Cluster (N rooms)" - graph_panel.tscn: add AlgoBtn to header Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
96da1b369f08b651a703dd83c9dc6c1f9307a79e
by plankatron <fleeky@gmail.com> · 2026-02-23T02:42:26.000Z

Changed files (5) +110 -14

addons/keet_social/keet_graph.gd +16 -6
……@@ -12,9 +12,13 @@ var client: KeetUdsClient
1212
1313## Compute full graph (cached 60s, use force=true to recompute).
1414## Result: { clusters: Array, edges: Array, suggestions: Array }
15func compute(force: bool = false, callback: Callable = Callable()) -> void:
15## algo: optional Dictionary with {features, threshold, minSize} for MI clustering.
16func compute(force: bool = false, algo: Dictionary = {}, callback: Callable = Callable()) -> void:
1617 _require_client()
17 client.call_rpc("graph.compute", {"force": force}, func(result, err):
18 var params: Dictionary = {"force": force}
19 if not algo.is_empty():
20 params["algo"] = algo
21 client.call_rpc("graph.compute", params, func(result, err):
1822 if err != null:
1923 error_occurred.emit("compute: %s" % str(err))
2024 return
……@@ -26,9 +30,12 @@ func compute(force: bool = false, callback: Callable = Callable()) -> void:
2630
2731
2832## Get clusters from last computation (uses cache).
29func get_clusters(callback: Callable = Callable()) -> void:
33func get_clusters(algo: Dictionary = {}, callback: Callable = Callable()) -> void:
3034 _require_client()
31 client.call_rpc("graph.clusters", {}, func(result, err):
35 var params: Dictionary = {}
36 if not algo.is_empty():
37 params["algo"] = algo
38 client.call_rpc("graph.clusters", params, func(result, err):
3239 if err != null:
3340 error_occurred.emit("get_clusters: %s" % str(err))
3441 return
……@@ -40,9 +47,12 @@ func get_clusters(callback: Callable = Callable()) -> void:
4047
4148
4249## Get MI edges from last computation (uses cache).
43func get_edges(callback: Callable = Callable()) -> void:
50func get_edges(algo: Dictionary = {}, callback: Callable = Callable()) -> void:
4451 _require_client()
45 client.call_rpc("graph.edges", {}, func(result, err):
52 var params: Dictionary = {}
53 if not algo.is_empty():
54 params["algo"] = algo
55 client.call_rpc("graph.edges", params, func(result, err):
4656 if err != null:
4757 error_occurred.emit("get_edges: %s" % str(err))
4858 return
scenes/graph_panel.tscn +5 -0
……@@ -20,6 +20,11 @@ size_flags_horizontal = 3
2020text = "Graph"
2121horizontal_alignment = 1
2222
23[node name="AlgoBtn" type="OptionButton" parent="VBox/Header"]
24unique_name_in_owner = true
25layout_mode = 2
26tooltip_text = "Clustering algorithm"
27
2328[node name="RefreshBtn" type="Button" parent="VBox/Header"]
2429unique_name_in_owner = true
2530layout_mode = 2
scripts/graph_canvas.gd +36 -1
……@@ -11,6 +11,7 @@ var _edges: Array = [] # [{from, to, mi}]
1111var _positions: Dictionary = {} # id -> Vector2
1212var _cluster_map: Dictionary = {} # id -> cluster_id
1313var _title_map: Dictionary = {} # id -> String
14var _cluster_labels: Dictionary = {} # cluster_index -> {label, topics}
1415
1516## Interaction state
1617var _hovered_node: String = ""
……@@ -50,11 +51,12 @@ func _ready() -> void:
5051 set_process(false)
5152
5253
53func set_graph_data(nodes: Array, edges: Array, clusters: Array) -> void:
54func set_graph_data(nodes: Array, edges: Array, clusters: Array, cluster_labels: Dictionary = {}) -> void:
5455 _nodes = nodes
5556 _edges = edges
5657 _cluster_map.clear()
5758 _title_map.clear()
59 _cluster_labels = cluster_labels
5860
5961 # Build cluster membership: room_id -> cluster_id
6062 for ci: int in clusters.size():
……@@ -251,6 +253,39 @@ func _draw() -> void:
251253 if font_size >= 6:
252254 draw_string(_font, label_pos, title, HORIZONTAL_ALIGNMENT_LEFT, -1, font_size, Color(0.8, 0.8, 0.8))
253255
256 # Draw cluster labels at centroids
257 if not _cluster_labels.is_empty():
258 # Collect node positions per cluster
259 var cluster_positions: Dictionary = {} # cluster_index -> Array[Vector2]
260 for nid: String in _cluster_map.keys():
261 var ci: int = _cluster_map[nid]
262 if not _positions.has(nid):
263 continue
264 if not cluster_positions.has(ci):
265 cluster_positions[ci] = []
266 cluster_positions[ci].append(_positions[nid])
267
268 for ci: int in cluster_positions.keys():
269 if not _cluster_labels.has(ci):
270 continue
271 var meta: Dictionary = _cluster_labels[ci]
272 var label_text: String = str(meta.get("label", ""))
273 if label_text == "":
274 continue
275 # Compute centroid
276 var pts: Array = cluster_positions[ci]
277 var centroid := Vector2.ZERO
278 for p: Vector2 in pts:
279 centroid += p
280 centroid /= float(pts.size())
281 var screen_pos: Vector2 = _world_to_screen(centroid)
282 var color: Color = CLUSTER_COLORS[ci % CLUSTER_COLORS.size()]
283 var font_size: int = int(14.0 * _zoom)
284 if font_size >= 8:
285 # Draw label above the centroid
286 var lbl_pos: Vector2 = screen_pos - Vector2(label_text.length() * 3.5 * _zoom, 20.0 * _zoom)
287 draw_string(_font, lbl_pos, label_text, HORIZONTAL_ALIGNMENT_LEFT, -1, font_size, color.lightened(0.4))
288
254289 # Tooltip for hovered node
255290 if _hovered_node != "" and _positions.has(_hovered_node):
256291 var pos: Vector2 = _world_to_screen(_positions[_hovered_node])
scripts/graph_panel.gd +35 -5
……@@ -4,14 +4,23 @@ extends PanelContainer
44## Only refreshes on manual button press or initial load.
55
66@onready var title_label: Label = %Title
7@onready var algo_btn: OptionButton = %AlgoBtn
78@onready var refresh_btn: Button = %RefreshBtn
89@onready var graph_canvas: Control = %GraphCanvas
910
11const ALGO_PRESETS: Array[Dictionary] = [
12 {"name": "members-only", "features": [{"type": "members", "weight": 1.0}], "threshold": 0.5, "minSize": 2},
13 {"name": "topics-only", "features": [{"type": "topics", "weight": 1.0}], "threshold": 0.3, "minSize": 2},
14 {"name": "category-only", "features": [{"type": "category", "weight": 1.0}], "threshold": 0.1, "minSize": 2},
15 {"name": "combined", "features": [{"type": "members", "weight": 1.0}, {"type": "topics", "weight": 0.5}], "threshold": 0.4, "minSize": 2},
16]
17
1018var _clusters: Array = []
1119var _edges: Array = []
1220var _rooms: Array = [] # [{id, title}]
1321var _data_ready: int = 0 # bitmask: 1=clusters, 2=edges, 4=rooms
1422var _compute_timeout: SceneTreeTimer = null
23var _selected_algo: Dictionary = {} # current algo preset (empty = daemon default)
1524
1625
1726func _ready() -> void:
……@@ -23,6 +32,13 @@ func _ready() -> void:
2332 refresh_btn.pressed.connect(_on_refresh)
2433 graph_canvas.room_clicked.connect(_on_room_clicked)
2534
35 # Populate algo selector
36 algo_btn.add_item("Default", 0)
37 for i: int in ALGO_PRESETS.size():
38 algo_btn.add_item(ALGO_PRESETS[i]["name"], i + 1)
39 algo_btn.selected = 0
40 algo_btn.item_selected.connect(_on_algo_selected)
41
2642 title_label.text = "Graph"
2743
2844 # Initial fetch
……@@ -32,8 +48,8 @@ func _ready() -> void:
3248func _fetch_all() -> void:
3349 _data_ready = 0
3450 title_label.text = "Graph (loading...)"
35 print("[graph] fetching graph data...")
36 KeetService.graph_mgr.compute(true)
51 print("[graph] fetching graph data... algo=%s" % str(_selected_algo.get("name", "default")))
52 KeetService.graph_mgr.compute(true, _selected_algo)
3753
3854 # Timeout: if graph.compute hangs, render rooms without edges after 8s
3955 if _compute_timeout != null:
……@@ -95,18 +111,24 @@ func _try_render() -> void:
95111 for r: Dictionary in _rooms:
96112 nodes.append({"id": r["id"], "title": r["title"]})
97113
98 # Build cluster arrays (daemon returns [{rooms: [id, id, ...], ...}])
114 # Build cluster arrays and extract labels (daemon returns [{rooms, label, topics, ...}])
99115 var cluster_arrays: Array = []
116 var cluster_labels: Dictionary = {} # index -> {label, topics}
100117 for cluster: Variant in _clusters:
101118 if cluster is Dictionary:
102119 var room_ids: Variant = cluster.get("rooms", [])
103120 if room_ids is Array:
121 var ci: int = cluster_arrays.size()
104122 cluster_arrays.append(room_ids)
123 var label: String = str(cluster.get("label", ""))
124 var topics: Variant = cluster.get("topics", [])
125 if label != "":
126 cluster_labels[ci] = {"label": label, "topics": topics}
105127 elif cluster is Array:
106128 cluster_arrays.append(cluster)
107129
108 print("[graph] rendering: %d nodes, %d edges, %d clusters" % [nodes.size(), _edges.size(), cluster_arrays.size()])
109 graph_canvas.set_graph_data(nodes, _edges, cluster_arrays)
130 print("[graph] rendering: %d nodes, %d edges, %d clusters, %d labels" % [nodes.size(), _edges.size(), cluster_arrays.size(), cluster_labels.size()])
131 graph_canvas.set_graph_data(nodes, _edges, cluster_arrays, cluster_labels)
110132
111133
112134func _on_graph_error(msg: String) -> void:
……@@ -119,6 +141,14 @@ func _on_graph_error(msg: String) -> void:
119141 _try_render()
120142
121143
144func _on_algo_selected(index: int) -> void:
145 if index == 0:
146 _selected_algo = {}
147 else:
148 _selected_algo = ALGO_PRESETS[index - 1]
149 _fetch_all()
150
151
122152func _on_refresh() -> void:
123153 _fetch_all()
124154
scripts/room_list_panel.gd +18 -2
……@@ -50,6 +50,9 @@ var _joining_rooms: Dictionary = {} # roomId -> true, tracks rooms being joined
5050var _topic_results: Array = []
5151var _topic_searching: bool = false
5252
53## Cluster metadata from graph (cluster_id_str -> {label, topics})
54var _cluster_meta_cache: Dictionary = {}
55
5356## Style constants
5457const COLOR_SELECTED := Color(0.25, 0.35, 0.55)
5558const COLOR_HOVER := Color(0.2, 0.25, 0.35)
……@@ -121,7 +124,7 @@ func _on_group_changed(group_id: String) -> void:
121124 if _group_rooms_key != group_id:
122125 _group_rooms.clear()
123126 _group_rooms_key = group_id
124 KeetService.graph_mgr.get_clusters(func(clusters):
127 KeetService.graph_mgr.get_clusters({}, func(clusters):
125128 if _active_group != group_id:
126129 return # stale
127130 for c: Variant in clusters:
……@@ -131,6 +134,13 @@ func _on_group_changed(group_id: String) -> void:
131134 _group_rooms.clear()
132135 for rid: Variant in c.get("rooms", []):
133136 _group_rooms.append({"roomId": str(rid)})
137 # Cache cluster label/topics for display
138 var label: String = str(c.get("label", ""))
139 var topics: Variant = c.get("topics", [])
140 if label != "":
141 _cluster_meta_cache[group_id] = {"label": label, "topics": topics}
142 # Update title now that we have metadata
143 title_label.text = _get_group_display_name(group_id)
134144 _build_joined_group_rooms()
135145 return
136146 )
……@@ -235,8 +245,14 @@ func _on_title_gui_input(event: InputEvent) -> void:
235245
236246
237247func _get_group_display_name(group_id: String) -> String:
238 # Cluster groups
248 # Cluster groups — prefer MetadataBase label, fall back to room count
239249 if group_id.begins_with("cluster:"):
250 if _cluster_meta_cache.has(group_id):
251 var meta: Dictionary = _cluster_meta_cache[group_id]
252 var label: String = str(meta.get("label", ""))
253 if label != "":
254 var n: int = _group_rooms.size()
255 return "%s (%d)" % [label, n] if n > 0 else label
240256 var n: int = _group_rooms.size()
241257 return "Cluster (%d rooms)" % n if n > 0 else "Cluster"
242258 # Registry room — check cached data, then registries array