"""Direct chat tab — event-sourced peer-to-peer messaging via the bus (M10)."""
from __future__ import annotations
def _get_known_peers(bus) -> list[str]:
"""Return node IDs of all remote peers currently in the registry."""
if bus is None:
return []
try:
seen: set[str] = set()
for entry in bus.registry.all_remote():
nid = entry.node_id
if nid and nid not in seen:
seen.add(nid)
return sorted(seen)
except Exception:
return []
def build_chat_tab(bus=None):
import gradio as gr
my_node_id = getattr(bus, "node_id_full", None) if bus else None
initial_peers = _get_known_peers(bus)
with gr.Column():
gr.HTML("""
💬 Direct Messages
P2P encrypted chat · X3DH key exchange · message peers on the mesh by Node ID
""")
gr.Markdown("### 💬 Direct Messages")
if my_node_id:
gr.Markdown(
f"**Your Node ID** (share this so others can message you):\n\n"
f"```\n{my_node_id}\n```"
)
gr.Markdown(
"> **Cross-node chat requires 2 nodes.** "
"Open the **Mesh** tab, click **Join Relay**, then come back here "
"and click **🔄 Refresh Peers** to see who's online.\n\n"
"> To start a second local node: `python scripts/start_mesh_node.py --name Bob --port 7081 --connect hf --demo-services`"
)
with gr.Row():
peer_dropdown = gr.Dropdown(
label="Known peers (from relay)",
choices=initial_peers,
value=initial_peers[0] if initial_peers else None,
interactive=True,
allow_custom_value=True,
scale=4,
)
refresh_peers_btn = gr.Button("🔄 Refresh Peers", size="sm", scale=1)
with gr.Row():
peer_id = gr.Textbox(
label="Recipient Node ID (paste here or pick above)",
placeholder=f"e.g. {my_node_id or 'ed25519:...'} (use * for broadcast)",
scale=4,
)
history_btn = gr.Button("Load History", scale=1)
# Clicking a peer in the dropdown fills the text box
peer_dropdown.change(lambda v: v or "", inputs=peer_dropdown, outputs=peer_id)
chat_out = gr.Chatbot(label="Messages", height=340)
with gr.Row():
msg_input = gr.Textbox(label="Message", placeholder="Type a message…", scale=7)
send_btn = gr.Button("Send", scale=1, variant="primary")
status_out = gr.Markdown(visible=False)
async def refresh_peers():
peers = _get_known_peers(bus)
return gr.update(choices=peers, value=peers[0] if peers else None)
async def load_history(peer_drop, peer_box):
# peer_box wins if filled; fall back to dropdown selection
peer = (peer_box or peer_drop or "").strip()
if bus is None:
return [{"role": "assistant", "content": "⚠️ Bus not connected"}]
target = peer if peer else None
try:
r = await bus.call("chat.history", (1, 0), {"input": {"peer": target}})
msgs = r.get("output", {}).get("messages", [])
if not msgs:
return [{"role": "assistant", "content": "(no messages yet)"}]
result = []
node_me = getattr(bus, "node_id_full", "me")
for m in msgs:
sender = m.get("from", "?")
is_mine = sender == node_me
result.append(
{
"role": "user" if is_mine else "assistant",
"content": f"{'You' if is_mine else sender}: {m.get('body', '')}",
}
)
return result
except Exception as e:
return [{"role": "assistant", "content": f"Error loading history: {e}"}]
async def send_msg(peer_drop, peer_box, msg, history):
# peer_box wins when filled; fall back to dropdown (avoids Gradio race condition
# where dropdown.change hasn't updated peer_box before Send fires)
peer = (peer_box or peer_drop or "").strip()
if not msg.strip():
return history, "", gr.update(visible=False)
history = history or []
if bus is None:
return (
[
*history,
{"role": "user", "content": msg},
{"role": "assistant", "content": "⚠️ Bus not connected"},
],
"",
gr.update(visible=False),
)
recipient = peer if peer else getattr(bus, "node_id_full", "")
if recipient == "*":
all_peers = _get_known_peers(bus)
if not all_peers:
all_peers = [getattr(bus, "node_id_full", recipient)]
results = []
for p in all_peers:
try:
r = await bus.call(
"chat.send", (1, 0), {"input": {"recipient": p, "body": msg}}
)
results.append(r.get("output", {}).get("delivered", "queued"))
except Exception:
results.append("error")
history = [
*history,
{"role": "user", "content": f"[broadcast to {len(all_peers)} peers] {msg}"},
]
note = f"✓ Broadcast sent to {len(all_peers)} peer(s): {results}"
return history, "", gr.update(visible=True, value=note)
try:
r = await bus.call(
"chat.send", (1, 0), {"input": {"recipient": recipient, "body": msg}}
)
status = r.get("output", {}).get("delivered", "queued")
history = [*history, {"role": "user", "content": msg}]
if status == "direct":
history.append({"role": "assistant", "content": f"[echo] {msg}"})
elif status == "delivered":
history.append({"role": "assistant", "content": f"✓ delivered to {recipient[:24]}"})
note = f"✓ {status} → `{recipient[:32]}`"
return history, "", gr.update(visible=True, value=note)
except Exception as e:
history = [
*history,
{"role": "user", "content": msg},
{"role": "assistant", "content": f"Error: {e}"},
]
return history, "", gr.update(visible=False)
refresh_peers_btn.click(refresh_peers, outputs=peer_dropdown)
# Pass BOTH dropdown and textbox so send_msg can pick the authoritative value
# even if the dropdown.change callback hasn't propagated to peer_id yet.
history_btn.click(load_history, inputs=[peer_dropdown, peer_id], outputs=chat_out)
send_btn.click(
send_msg,
inputs=[peer_dropdown, peer_id, msg_input, chat_out],
outputs=[chat_out, msg_input, status_out],
)
msg_input.submit(
send_msg,
inputs=[peer_dropdown, peer_id, msg_input, chat_out],
outputs=[chat_out, msg_input, status_out],
)