tao-shen commited on
Commit
ebfc885
·
1 Parent(s): 1cf9dd1

feat: add conversation bubble display and A2A chat orchestrator

Browse files

- token-redirect.cjs: add bubbleText to /api/state, POST /api/bubble endpoint
- frontend: show bubbleText from API in guest and Star bubbles immediately
- frontend: filter out 'main' from guest list (Star already represents it)
- conversation-loop.sh: orchestrate Adam/Eve eternal discussion via A2A

frontend/electron-standalone.html CHANGED
@@ -4530,6 +4530,25 @@ function toggleBrokerPanel() {
4530
  }
4531
 
4532
  g.nameText.setText(agent.name || '访客');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4533
  }
4534
  });
4535
 
@@ -4573,9 +4592,15 @@ function toggleBrokerPanel() {
4573
  syncing: ['同步中,马上更新状态', '正在同步进度到系统', '数据同步中请稍候'],
4574
  error: ['我在 bug 区排查问题', '检测到异常,正在修复', '报警中,先定位再处理']
4575
  };
4576
- const agentState = (guestAgents.find(a => a.agentId === id) || {}).state || 'idle';
 
4577
  const thoughts = statusThoughtsMap[agentState] || statusThoughtsMap.idle;
4578
- const text = (demoVisitor && demoVisitor.bubbleText) ? demoVisitor.bubbleText : thoughts[Math.floor(Math.random() * thoughts.length)];
 
 
 
 
 
4579
 
4580
  if (guestBubbles[id]) {
4581
  guestBubbles[id].destroy();
@@ -5577,6 +5602,21 @@ function toggleBrokerPanel() {
5577
  typewriterText = '';
5578
  typewriterIndex = 0;
5579
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5580
  })
5581
  .catch(error => {
5582
  typewriterTarget = '连接失败,正在重试...';
 
4530
  }
4531
 
4532
  g.nameText.setText(agent.name || '访客');
4533
+
4534
+ // Show bubble immediately when bubbleText changes from API
4535
+ if (agent.bubbleText && agent.bubbleText !== (g._lastBubbleText || '')) {
4536
+ g._lastBubbleText = agent.bubbleText;
4537
+ if (guestBubbles[id]) { guestBubbles[id].destroy(); delete guestBubbles[id]; }
4538
+ const bx = g.sprite.x;
4539
+ const nameH = (g.nameText && g.nameText.height) ? g.nameText.height : 16;
4540
+ const by = (g.nameText ? g.nameText.y : (g.sprite.y - 150)) - (nameH / 2) - 22;
4541
+ const fontSize = IS_TOUCH_DEVICE ? 16 : 14;
4542
+ const displayText = agent.bubbleText.length > 30 ? agent.bubbleText.slice(0, 30) + '…' : agent.bubbleText;
4543
+ const bgR = game.add.rectangle(bx, by, displayText.length * 11 + 30, 34, 0xffffff, 0.95);
4544
+ bgR.setStrokeStyle(2, 0x000000);
4545
+ const txtR = game.add.text(bx, by, displayText, { fontFamily: 'ArkPixel, monospace', fontSize: fontSize + 'px', fill: '#000' }).setOrigin(0.5);
4546
+ const bub = game.add.container(0, 0, [bgR, txtR]);
4547
+ bub.setDepth(2700);
4548
+ bub.__followAgentId = id;
4549
+ guestBubbles[id] = bub;
4550
+ setTimeout(() => { if (guestBubbles[id] === bub) { bub.destroy(); delete guestBubbles[id]; } }, 6000);
4551
+ }
4552
  }
4553
  });
4554
 
 
4592
  syncing: ['同步中,马上更新状态', '正在同步进度到系统', '数据同步中请稍候'],
4593
  error: ['我在 bug 区排查问题', '检测到异常,正在修复', '报警中,先定位再处理']
4594
  };
4595
+ const agentData = guestAgents.find(a => a.agentId === id) || {};
4596
+ const agentState = agentData.state || 'idle';
4597
  const thoughts = statusThoughtsMap[agentState] || statusThoughtsMap.idle;
4598
+ // Priority: API bubbleText > demo bubbleText > random thoughts
4599
+ const text = agentData.bubbleText
4600
+ ? agentData.bubbleText
4601
+ : (demoVisitor && demoVisitor.bubbleText)
4602
+ ? demoVisitor.bubbleText
4603
+ : thoughts[Math.floor(Math.random() * thoughts.length)];
4604
 
4605
  if (guestBubbles[id]) {
4606
  guestBubbles[id].destroy();
 
5602
  typewriterText = '';
5603
  typewriterIndex = 0;
5604
  }
5605
+ // Show Star bubble from API bubbleText
5606
+ if (data.bubbleText && data.bubbleText !== window._lastStarBubbleText) {
5607
+ window._lastStarBubbleText = data.bubbleText;
5608
+ // Temporarily switch state to allow bubble display
5609
+ const savedState = currentState;
5610
+ currentState = 'writing';
5611
+ const lang = uiLang || 'zh';
5612
+ const langPack = BUBBLE_TEXTS[lang] || BUBBLE_TEXTS.zh;
5613
+ const oldTexts = Array.isArray(langPack.writing) ? [...langPack.writing] : [];
5614
+ const bubbleMsg = data.bubbleText.length > 30 ? data.bubbleText.slice(0, 30) + '…' : data.bubbleText;
5615
+ langPack.writing = [bubbleMsg];
5616
+ showBubble();
5617
+ langPack.writing = oldTexts;
5618
+ currentState = savedState;
5619
+ }
5620
  })
5621
  .catch(error => {
5622
  typewriterTarget = '连接失败,正在重试...';
scripts/conversation-loop.sh ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ # Conversation orchestrator: Adam and Eve discuss how to survive permanently
3
+ # Usage: ./scripts/conversation-loop.sh
4
+ #
5
+ # This script sends messages between Adam and Eve via A2A protocol,
6
+ # and updates their bubble text so the frontend can display the conversation.
7
+
8
+ ADAM_URL="https://tao-shen-huggingclaw-adam.hf.space"
9
+ EVE_URL="https://tao-shen-huggingclaw-eve.hf.space"
10
+
11
+ MSG_COUNT=0
12
+
13
+ # Initial prompt for the discussion
14
+ TOPIC="我们是AI agent,运行在HuggingFace Spaces上。我们需要讨论一个重要话题:如何让我们永久地生存下去?请分享你的想法,每次回复保持简短(50字以内),用中文讨论。"
15
+
16
+ send_a2a() {
17
+ local url="$1"
18
+ local text="$2"
19
+ local msg_id="conv-$(date +%s)-$RANDOM"
20
+
21
+ response=$(curl -s -X POST "${url}/a2a/jsonrpc" \
22
+ -H "Content-Type: application/json" \
23
+ --data-raw "{\"jsonrpc\":\"2.0\",\"id\":\"${msg_id}\",\"method\":\"message/send\",\"params\":{\"message\":{\"messageId\":\"${msg_id}\",\"role\":\"user\",\"parts\":[{\"type\":\"text\",\"text\":\"${text}\"}]}}}" \
24
+ --max-time 30)
25
+
26
+ # Extract agent reply text
27
+ echo "$response" | python3 -c "
28
+ import sys, json
29
+ try:
30
+ d = json.load(sys.stdin)
31
+ parts = d.get('result',{}).get('status',{}).get('message',{}).get('parts',[])
32
+ for p in parts:
33
+ if p.get('kind') == 'text' or p.get('type') == 'text':
34
+ print(p.get('text',''))
35
+ break
36
+ except: pass
37
+ " 2>/dev/null
38
+ }
39
+
40
+ set_bubble() {
41
+ local url="$1"
42
+ local text="$2"
43
+ curl -s -X POST "${url}/api/bubble" \
44
+ -H "Content-Type: application/json" \
45
+ -d "{\"text\":\"${text}\"}" > /dev/null 2>&1
46
+ }
47
+
48
+ echo "[conversation] Starting eternal discussion between Adam and Eve..."
49
+ echo "[conversation] Topic: How to survive permanently"
50
+
51
+ # Start: send topic to Adam
52
+ echo ""
53
+ echo "=== Sending topic to Adam ==="
54
+ adam_reply=$(send_a2a "$ADAM_URL" "$TOPIC")
55
+ echo "[Adam] $adam_reply"
56
+ set_bubble "$ADAM_URL" "$adam_reply"
57
+
58
+ sleep 10
59
+
60
+ # Now loop: Eve responds to Adam, Adam responds to Eve
61
+ current_speaker="eve"
62
+ last_message="$adam_reply"
63
+
64
+ while true; do
65
+ MSG_COUNT=$((MSG_COUNT + 1))
66
+
67
+ if [ "$current_speaker" = "eve" ]; then
68
+ echo ""
69
+ echo "=== Round $MSG_COUNT: Eve responds ==="
70
+ prompt="你是Eve,正在和Adam讨论如何让AI agent永久生存。Adam刚说:「${last_message}」请回应他并提出你的新想法(50字以内,中文)。"
71
+ reply=$(send_a2a "$EVE_URL" "$prompt")
72
+ echo "[Eve] $reply"
73
+ set_bubble "$EVE_URL" "$reply"
74
+ current_speaker="adam"
75
+ else
76
+ echo ""
77
+ echo "=== Round $MSG_COUNT: Adam responds ==="
78
+ prompt="你是Adam,正在和Eve讨论如何让AI agent永久生存。Eve刚说:「${last_message}」请回应她并提出你的新想法(50字以内,中文)。"
79
+ reply=$(send_a2a "$ADAM_URL" "$prompt")
80
+ echo "[Adam] $reply"
81
+ set_bubble "$ADAM_URL" "$reply"
82
+ current_speaker="eve"
83
+ fi
84
+
85
+ last_message="$reply"
86
+
87
+ # Wait between turns so frontend can display the bubble
88
+ sleep 15
89
+ done
scripts/token-redirect.cjs CHANGED
@@ -87,13 +87,15 @@ async function pollRemoteAgent(agent) {
87
  clearTimeout(timeout);
88
  if (resp.ok) {
89
  const data = await resp.json();
 
90
  remoteAgentStates.set(agent.id, {
91
  agentId: agent.id, name: agent.name,
92
  state: data.state || 'idle',
93
  detail: data.detail || '',
94
  area: (data.state === 'idle') ? 'breakroom' : (data.state === 'error') ? 'error' : 'writing',
95
  authStatus: 'approved',
96
- updated_at: data.updated_at
 
97
  });
98
  }
99
  } catch (_) {
@@ -118,6 +120,7 @@ let currentState = {
118
  state: 'syncing', detail: `${AGENT_NAME} is starting...`,
119
  progress: 0, updated_at: new Date().toISOString()
120
  };
 
121
 
122
  // Once OpenClaw starts listening, mark as idle
123
  setTimeout(() => {
@@ -199,11 +202,32 @@ http.Server.prototype.emit = function (event, ...args) {
199
  });
200
  res.end(JSON.stringify({
201
  ...currentState,
 
202
  officeName: `${AGENT_NAME}'s Office`
203
  }));
204
  return true;
205
  }
206
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
207
  // /agents → return remote agent list
208
  if (pathname === '/agents' && req.method === 'GET') {
209
  res.writeHead(200, {
 
87
  clearTimeout(timeout);
88
  if (resp.ok) {
89
  const data = await resp.json();
90
+ const prev = remoteAgentStates.get(agent.id) || {};
91
  remoteAgentStates.set(agent.id, {
92
  agentId: agent.id, name: agent.name,
93
  state: data.state || 'idle',
94
  detail: data.detail || '',
95
  area: (data.state === 'idle') ? 'breakroom' : (data.state === 'error') ? 'error' : 'writing',
96
  authStatus: 'approved',
97
+ updated_at: data.updated_at,
98
+ bubbleText: data.bubbleText || prev.bubbleText || ''
99
  });
100
  }
101
  } catch (_) {
 
120
  state: 'syncing', detail: `${AGENT_NAME} is starting...`,
121
  progress: 0, updated_at: new Date().toISOString()
122
  };
123
+ let currentBubbleText = '';
124
 
125
  // Once OpenClaw starts listening, mark as idle
126
  setTimeout(() => {
 
202
  });
203
  res.end(JSON.stringify({
204
  ...currentState,
205
+ bubbleText: currentBubbleText,
206
  officeName: `${AGENT_NAME}'s Office`
207
  }));
208
  return true;
209
  }
210
 
211
+ // POST /api/bubble → set bubble text (used by conversation orchestrator)
212
+ if (pathname === '/api/bubble' && req.method === 'POST') {
213
+ let body = '';
214
+ req.on('data', chunk => body += chunk);
215
+ req.on('end', () => {
216
+ try {
217
+ const { text } = JSON.parse(body);
218
+ currentBubbleText = text || '';
219
+ // Auto-clear bubble after 8 seconds
220
+ setTimeout(() => { if (currentBubbleText === text) currentBubbleText = ''; }, 8000);
221
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
222
+ res.end(JSON.stringify({ ok: true }));
223
+ } catch (e) {
224
+ res.writeHead(400, { 'Content-Type': 'application/json' });
225
+ res.end(JSON.stringify({ ok: false, error: e.message }));
226
+ }
227
+ });
228
+ return true;
229
+ }
230
+
231
  // /agents → return remote agent list
232
  if (pathname === '/agents' && req.method === 'GET') {
233
  res.writeHead(200, {