@bolloon/bolloon-agent 0.1.24 → 0.1.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/web/client.js CHANGED
@@ -23,6 +23,7 @@ let eventSources = new Map(); // channelId -> EventSource
23
23
  let currentChannelId = null;
24
24
  let currentAgentId = '';
25
25
  let channels = [];
26
+ let remoteChannels = []; // v3: 远端 channel UI 元数据 (按 peer 分组)
26
27
  let isSidebarCollapsed = false;
27
28
  let reconnectAttempts = new Map(); // channelId -> attempts
28
29
  let reconnectTimers = new Map(); // channelId -> timer
@@ -623,6 +624,11 @@ async function selectChannel(channelId, targetSessionId = null) {
623
624
  currentChannelId = channelId;
624
625
  reconnectAttempts.set(channelId, 0);
625
626
 
627
+ // v3: 盾牌弹窗打开时, 切 channel 要刷列表 (tab 标题 + 已绑/未绑 分组)
628
+ if (typeof judgmentsModal !== 'undefined' && judgmentsModal && judgmentsModal.classList.contains('active')) {
629
+ if (typeof lastJudgmentsCache !== 'undefined') renderJudgments(lastJudgmentsCache);
630
+ }
631
+
626
632
  // 找到当前频道和 session
627
633
  const channel = channels.find(c => c.id === channelId);
628
634
  if (channel) {
@@ -1996,6 +2002,18 @@ let judgmentsLoaded = false;
1996
2002
  function showJudgmentsModal() {
1997
2003
  if (judgmentsModal) judgmentsModal.classList.add('active');
1998
2004
  if (!judgmentsLoaded) loadJudgments();
2005
+ else renderJudgments(lastJudgmentsCache); // 打开时按当前 channel / tab 重渲
2006
+ }
2007
+
2008
+ function switchJudgmentTab(tab) {
2009
+ currentJudgmentTab = tab;
2010
+ document.querySelectorAll('.judgment-tab').forEach(btn => {
2011
+ const active = btn.dataset.tab === tab;
2012
+ btn.classList.toggle('active', active);
2013
+ btn.style.borderBottomColor = active ? '#2563eb' : 'transparent';
2014
+ btn.style.color = active ? '#2563eb' : '#6b7280';
2015
+ });
2016
+ renderJudgments(lastJudgmentsCache);
1999
2017
  }
2000
2018
 
2001
2019
  function hideJudgmentsModal() {
@@ -2008,16 +2026,83 @@ function escapeHtml(s) {
2008
2026
  }[c]));
2009
2027
  }
2010
2028
 
2029
+ let currentJudgmentTab = 'channel'; // 'channel' | 'global'
2030
+ let lastJudgmentsCache = []; // 最近一次 loadJudgments 拿到的原始列表, 切 tab / 切 channel 时复用
2031
+
2032
+ /**
2033
+ * v3 重做: 渲染判断力列表 (受 tab + 当前 channel 影响)
2034
+ * tab = 'channel': 拆为"已绑定" + "未绑定"两组, 每条带 + / × 按钮
2035
+ * tab = 'global': 全部 judgment 列表, 无 + / × 按钮
2036
+ * 如果没选 channel, 'channel' tab 自动显示提示 + 全部 judgment
2037
+ */
2011
2038
  function renderJudgments(items) {
2012
2039
  if (!judgmentsList) return;
2013
- if (!items || items.length === 0) {
2040
+ const all = items || [];
2041
+ const titleEl = document.getElementById('judgments-list-title');
2042
+ const chNameEl = document.getElementById('judgments-tab-channel-name');
2043
+ const currentCh = currentChannelId
2044
+ ? channels.find(c => c.id === currentChannelId)
2045
+ : null;
2046
+
2047
+ if (chNameEl) {
2048
+ chNameEl.textContent = currentCh ? `(${currentCh.name})` : '(未选)';
2049
+ }
2050
+
2051
+ if (all.length === 0) {
2014
2052
  judgmentsList.innerHTML = '<div class="task-empty">还没有判断, 在上面记录第一条吧</div>';
2053
+ if (titleEl) titleEl.textContent = '本 channel 的判断力';
2054
+ return;
2055
+ }
2056
+
2057
+ if (currentJudgmentTab === 'global') {
2058
+ // 全局 tab: 全部 judgment, 简单列表
2059
+ if (titleEl) titleEl.textContent = `全局判断力 (${all.length} 条)`;
2060
+ judgmentsList.innerHTML = renderJudgmentItems(all, { showBindToggle: false });
2061
+ return;
2062
+ }
2063
+
2064
+ // channel tab: 必须有 channel
2065
+ if (!currentCh) {
2066
+ if (titleEl) titleEl.textContent = '本 channel 的判断力';
2067
+ judgmentsList.innerHTML = `
2068
+ <div style="padding:24px 12px;text-align:center;color:#6b7280;font-size:13px;">
2069
+ 请先在左侧选中一个 channel,<br>然后这里会显示已绑定和可加入的判断力。
2070
+ </div>
2071
+ `;
2015
2072
  return;
2016
2073
  }
2017
- const html = items.map(j => {
2074
+
2075
+ const boundIds = new Set(
2076
+ Array.isArray(currentCh.bound_judgment_ids) ? currentCh.bound_judgment_ids : []
2077
+ );
2078
+ const bound = all.filter(j => boundIds.has(j.id));
2079
+ const unbound = all.filter(j => !boundIds.has(j.id));
2080
+
2081
+ if (titleEl) titleEl.textContent = `${currentCh.name} 的判断力 (已绑 ${bound.length} / 共 ${all.length})`;
2082
+
2083
+ let html = '';
2084
+ if (bound.length > 0) {
2085
+ html += `<div style="font-size:11px;color:#6b7280;text-transform:uppercase;letter-spacing:0.5px;padding:8px 4px 4px;">已绑定 (${bound.length})</div>`;
2086
+ html += renderJudgmentItems(bound, { showBindToggle: true, isBound: true });
2087
+ }
2088
+ if (unbound.length > 0) {
2089
+ html += `<div style="font-size:11px;color:#6b7280;text-transform:uppercase;letter-spacing:0.5px;padding:14px 4px 4px;">未绑定 (${unbound.length})</div>`;
2090
+ html += renderJudgmentItems(unbound, { showBindToggle: true, isBound: false });
2091
+ }
2092
+ judgmentsList.innerHTML = html;
2093
+ }
2094
+
2095
+ function renderJudgmentItems(items, opts) {
2096
+ const { showBindToggle, isBound } = opts || {};
2097
+ return items.map(j => {
2018
2098
  const reason = (j.reasons && j.reasons[0]) ? escapeHtml(j.reasons[0]) : '';
2019
2099
  const domain = (j.context && j.context.domain) ? escapeHtml(j.context.domain) : 'general';
2020
2100
  const stakes = (j.context && j.context.stakes) ? escapeHtml(j.context.stakes) : 'medium';
2101
+ const bindBtn = showBindToggle
2102
+ ? isBound
2103
+ ? `<button class="judgment-toggle-btn" data-id="${escapeHtml(j.id)}" data-action="unbind" title="从当前 channel 移除" style="background:none;border:1px solid #fca5a5;color:#b91c1c;padding:1px 8px;border-radius:3px;cursor:pointer;font-size:11px;">× 移除</button>`
2104
+ : `<button class="judgment-toggle-btn" data-id="${escapeHtml(j.id)}" data-action="bind" title="加进当前 channel" style="background:none;border:1px solid #6b7280;color:#6b7280;padding:1px 8px;border-radius:3px;cursor:pointer;font-size:11px;">+ 加入</button>`
2105
+ : '';
2021
2106
  return `
2022
2107
  <div class="task-item completed judgment-row"
2023
2108
  data-judgment-id="${escapeHtml(j.id)}"
@@ -2037,6 +2122,7 @@ function renderJudgments(items) {
2037
2122
  <div class="task-item-meta" style="color:#999;font-size:11px;margin-top:4px;display:flex;justify-content:space-between;align-items:center;">
2038
2123
  <span>${domain} · ${escapeHtml(j.timestamp)} · ${escapeHtml(j.id)}</span>
2039
2124
  <span style="display:flex;gap:4px;">
2125
+ ${bindBtn}
2040
2126
  <button class="judgment-edit-btn" data-id="${escapeHtml(j.id)}" title="编辑判断" style="background:none;border:1px solid #d1d5db;color:#374151;padding:1px 8px;border-radius:3px;cursor:pointer;font-size:11px;">编辑</button>
2041
2127
  <button class="judgment-del-btn" data-id="${escapeHtml(j.id)}" title="删除判断" style="background:none;border:1px solid #fca5a5;color:#b91c1c;padding:1px 8px;border-radius:3px;cursor:pointer;font-size:11px;">删除</button>
2042
2128
  </span>
@@ -2044,7 +2130,6 @@ function renderJudgments(items) {
2044
2130
  </div>
2045
2131
  `;
2046
2132
  }).join('');
2047
- judgmentsList.innerHTML = html;
2048
2133
  }
2049
2134
 
2050
2135
  async function loadJudgments() {
@@ -2053,7 +2138,8 @@ async function loadJudgments() {
2053
2138
  const res = await fetch('/api/judgments');
2054
2139
  if (!res.ok) throw new Error('HTTP ' + res.status);
2055
2140
  const data = await res.json();
2056
- renderJudgments(data.judgments);
2141
+ lastJudgmentsCache = data.judgments || [];
2142
+ renderJudgments(lastJudgmentsCache);
2057
2143
  if (judgmentsBadge) {
2058
2144
  if (data.count > 0) {
2059
2145
  judgmentsBadge.textContent = data.count;
@@ -2068,11 +2154,46 @@ async function loadJudgments() {
2068
2154
  }
2069
2155
  }
2070
2156
 
2157
+ /** 把 judgment id 加进 / 移出当前 channel.bound_judgment_ids, 然后刷新两边 UI */
2158
+ async function toggleChannelJudgment(judgmentId, action) {
2159
+ if (!currentChannelId) {
2160
+ showJudgmentError('请先选中一个 channel');
2161
+ return;
2162
+ }
2163
+ const ch = channels.find(c => c.id === currentChannelId);
2164
+ if (!ch) return;
2165
+ const set = new Set(Array.isArray(ch.bound_judgment_ids) ? ch.bound_judgment_ids : []);
2166
+ if (action === 'bind') set.add(judgmentId);
2167
+ else set.delete(judgmentId);
2168
+ const next = Array.from(set);
2169
+ try {
2170
+ const res = await fetch(`/channels/${currentChannelId}`, {
2171
+ method: 'PATCH',
2172
+ headers: { 'Content-Type': 'application/json' },
2173
+ body: JSON.stringify({ bound_judgment_ids: next })
2174
+ });
2175
+ if (!res.ok) {
2176
+ const err = await res.json().catch(() => ({}));
2177
+ throw new Error(err.error || `HTTP ${res.status}`);
2178
+ }
2179
+ const updated = await res.json();
2180
+ const idx = channels.findIndex(c => c.id === currentChannelId);
2181
+ if (idx >= 0) channels[idx] = updated;
2182
+ // 弹窗开着就刷新, 关着就跳过
2183
+ if (judgmentsModal && judgmentsModal.classList.contains('active')) {
2184
+ renderJudgments(lastJudgmentsCache);
2185
+ }
2186
+ } catch (err) {
2187
+ showJudgmentError('绑定失败: ' + err.message);
2188
+ }
2189
+ }
2190
+
2071
2191
  // 列表内编辑/删除 + 拖拽 — 事件委托
2072
2192
  if (judgmentsList) {
2073
2193
  judgmentsList.addEventListener('click', async (e) => {
2074
2194
  const editBtn = e.target.closest && e.target.closest('.judgment-edit-btn');
2075
2195
  const delBtn = e.target.closest && e.target.closest('.judgment-del-btn');
2196
+ const toggleBtn = e.target.closest && e.target.closest('.judgment-toggle-btn');
2076
2197
  if (editBtn) {
2077
2198
  const id = editBtn.getAttribute('data-id');
2078
2199
  await editJudgment(id);
@@ -2087,9 +2208,18 @@ if (judgmentsList) {
2087
2208
  } catch (err) {
2088
2209
  showJudgmentError('删除失败: ' + err.message);
2089
2210
  }
2211
+ } else if (toggleBtn) {
2212
+ const id = toggleBtn.getAttribute('data-id');
2213
+ const action = toggleBtn.getAttribute('data-action');
2214
+ await toggleChannelJudgment(id, action);
2090
2215
  }
2091
2216
  });
2092
2217
 
2218
+ // tab 切换
2219
+ document.querySelectorAll('.judgment-tab').forEach(btn => {
2220
+ btn.addEventListener('click', () => switchJudgmentTab(btn.dataset.tab));
2221
+ });
2222
+
2093
2223
  // 拖拽: 每条 judgment 是 drag source, dataTransfer 装 decision text
2094
2224
  judgmentsList.addEventListener('dragstart', (e) => {
2095
2225
  const row = e.target.closest && e.target.closest('.judgment-row');
@@ -2361,6 +2491,163 @@ loadJudgments();
2361
2491
  // 后台定期刷新 (与 modal 打开/关闭无关, 任何时候都保持徽章新鲜)
2362
2492
  setInterval(loadJudgments, 10000);
2363
2493
 
2494
+ // ============================================================================
2495
+ // v3: 远端智能体 (从 P2P 连接的 peer 拉取的 channel UI 元数据)
2496
+ // ============================================================================
2497
+
2498
+ async function loadRemoteChannels() {
2499
+ try {
2500
+ const res = await fetch('/api/remote-channels');
2501
+ if (!res.ok) return;
2502
+ const data = await res.json();
2503
+ remoteChannels = Array.isArray(data.peers) ? data.peers : [];
2504
+ renderRemoteChannels();
2505
+ } catch (err) {
2506
+ console.error('[v3] loadRemoteChannels 失败:', err);
2507
+ }
2508
+ }
2509
+
2510
+ function renderRemoteChannels() {
2511
+ const list = document.getElementById('remote-channel-list');
2512
+ if (!list) return;
2513
+ if (remoteChannels.length === 0) {
2514
+ list.innerHTML = '<li style="color:var(--text-muted);font-size:11px;padding:8px 4px;text-align:center;">(暂无, 点 ↻ 刷新)</li>';
2515
+ return;
2516
+ }
2517
+ const html = remoteChannels.map(p => {
2518
+ const peerShort = p.peerId.substring(0, 12) + '…';
2519
+ return `
2520
+ <li class="remote-peer-group" style="margin-bottom:8px;">
2521
+ <div style="font-size:10px;color:var(--text-muted);padding:2px 4px;display:flex;align-items:center;gap:4px;">
2522
+ <span>🌐</span><span title="${escapeHtml(p.peerId)}">${escapeHtml(peerShort)}</span>
2523
+ <span>·</span>
2524
+ <span>${p.channels.length} 个</span>
2525
+ </div>
2526
+ ${p.channels.map(c => `
2527
+ <div class="remote-channel-row" data-peer-id="${escapeHtml(p.peerId)}" data-channel-id="${escapeHtml(c.id)}"
2528
+ style="display:flex;align-items:center;gap:6px;padding:4px 6px;cursor:pointer;border-radius:4px;font-size:12px;color:var(--text);">
2529
+ <span>🤖</span>
2530
+ <span style="flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;" title="${escapeHtml(c.name || '')}">${escapeHtml(c.name || '(未命名)')}</span>
2531
+ ${c.hasWallet ? '<span title="绑定钱包">⛓</span>' : ''}
2532
+ <span title="绑定判断力数" style="font-size:10px;color:var(--text-muted);">🧠${c.boundJudgmentCount || 0}</span>
2533
+ </div>
2534
+ `).join('')}
2535
+ </li>
2536
+ `;
2537
+ }).join('');
2538
+ list.innerHTML = html;
2539
+
2540
+ // 绑定点击 — 暂时只是 console.log + 提示, Phase 2 接 chat
2541
+ list.querySelectorAll('.remote-channel-row').forEach(row => {
2542
+ row.addEventListener('click', () => {
2543
+ const peerId = row.dataset.peerId;
2544
+ const channelId = row.dataset.channelId;
2545
+ console.log('[v3] 点击远端 channel:', peerId.substring(0,12), channelId);
2546
+ alert(`远端智能体点击 — Phase 2 实现 chat 调用\n\nPeer: ${peerId}\nChannel: ${channelId}`);
2547
+ });
2548
+ });
2549
+ }
2550
+
2551
+ const refreshRemoteBtn = document.getElementById('refresh-remote-agents-btn');
2552
+ if (refreshRemoteBtn) {
2553
+ refreshRemoteBtn.addEventListener('click', async (e) => {
2554
+ e.stopPropagation(); // 防止冒泡触发折叠
2555
+ refreshRemoteBtn.disabled = true;
2556
+ refreshRemoteBtn.textContent = '⏳';
2557
+ try {
2558
+ const res = await fetch('/api/remote-channels/refresh', { method: 'POST' });
2559
+ const data = await res.json();
2560
+ console.log('[v3] 刷新远端智能体:', data);
2561
+ setTimeout(loadRemoteChannels, 1500);
2562
+ } catch (err) {
2563
+ console.error('[v3] 刷新失败:', err);
2564
+ } finally {
2565
+ setTimeout(() => {
2566
+ refreshRemoteBtn.disabled = false;
2567
+ refreshRemoteBtn.textContent = '↻ 刷新';
2568
+ }, 2000);
2569
+ }
2570
+ });
2571
+ }
2572
+
2573
+ // 启动时拉一次 + 定期轮询 (SSE 接收 P2P reply 后也会更新)
2574
+ loadRemoteChannels();
2575
+ setInterval(loadRemoteChannels, 8000);
2576
+
2577
+ // ============ v3: 折叠 + 拖拽分隔线 ============
2578
+
2579
+ // 给本地/远端 section 加 flex 修饰类 (CSS variable 驱动比例)
2580
+ const localSection = document.querySelector('.sidebar-section'); // 第一个 section = 本地 channel
2581
+ const remoteSection = document.getElementById('remote-agents-section');
2582
+ if (localSection) localSection.classList.add('local-flex');
2583
+ if (remoteSection) remoteSection.classList.add('remote-flex');
2584
+
2585
+ // 折叠: 点 header 切换 collapsed 类
2586
+ const remoteHeader = document.getElementById('remote-agents-header');
2587
+ if (remoteHeader && remoteSection) {
2588
+ remoteHeader.addEventListener('click', (e) => {
2589
+ // 阻止刷新按钮的事件冒泡在 refreshRemoteBtn 里已处理
2590
+ remoteSection.classList.toggle('collapsed');
2591
+ });
2592
+ }
2593
+
2594
+ // 拖拽分隔线: 鼠标按下开始拖, mousemove 改 --local-flex / --remote-flex, mouseup 结束
2595
+ const splitHandle = document.getElementById('sidebar-split-handle');
2596
+ if (splitHandle && localSection && remoteSection) {
2597
+ // 初始化等分
2598
+ const updateFlexVars = (localRatio, remoteRatio) => {
2599
+ localSection.style.setProperty('--local-flex', String(localRatio));
2600
+ remoteSection.style.setProperty('--remote-flex', String(remoteRatio));
2601
+ };
2602
+ updateFlexVars(1, 1);
2603
+
2604
+ let isDragging = false;
2605
+ let dragStartY = 0;
2606
+ let startLocalFlex = 1;
2607
+ let startRemoteFlex = 1;
2608
+ let sidebarHeight = 0;
2609
+
2610
+ splitHandle.addEventListener('mousedown', (e) => {
2611
+ isDragging = true;
2612
+ splitHandle.classList.add('dragging');
2613
+ dragStartY = e.clientY;
2614
+ // 读当前 CSS variable 拿真实 flex 值
2615
+ const lf = parseFloat(getComputedStyle(localSection).getPropertyValue('--local-flex')) || 1;
2616
+ const rf = parseFloat(getComputedStyle(remoteSection).getPropertyValue('--remote-flex')) || 1;
2617
+ startLocalFlex = lf;
2618
+ startRemoteFlex = rf;
2619
+ // 父容器可用高度 = sidebar-section 总和 (本地+远端+handle)
2620
+ const sidebar = document.querySelector('.sidebar');
2621
+ if (sidebar) sidebarHeight = sidebar.clientHeight;
2622
+ e.preventDefault();
2623
+ document.body.style.cursor = 'ns-resize';
2624
+ });
2625
+
2626
+ document.addEventListener('mousemove', (e) => {
2627
+ if (!isDragging) return;
2628
+ const deltaY = e.clientY - dragStartY;
2629
+ if (sidebarHeight <= 0) return;
2630
+ // deltaY 正 = 鼠标下移 = 拉大本地 / 缩小远端
2631
+ // 转换: 1 像素 ≈ sidebarHeight 中 0.005 的比例
2632
+ const deltaRatio = deltaY / sidebarHeight * 4; // 4 倍灵敏
2633
+ let newLocal = Math.max(0.1, startLocalFlex + deltaRatio);
2634
+ let newRemote = Math.max(0.1, startRemoteFlex - deltaRatio);
2635
+ updateFlexVars(newLocal, newRemote);
2636
+ });
2637
+
2638
+ document.addEventListener('mouseup', () => {
2639
+ if (!isDragging) return;
2640
+ isDragging = false;
2641
+ splitHandle.classList.remove('dragging');
2642
+ document.body.style.cursor = '';
2643
+ });
2644
+
2645
+ // 双击分隔线 = 重置为等分
2646
+ splitHandle.addEventListener('dblclick', () => {
2647
+ updateFlexVars(1, 1);
2648
+ });
2649
+ }
2650
+
2364
2651
  if (taskModal) {
2365
2652
  taskModal.addEventListener('click', (e) => {
2366
2653
  if (e.target === taskModal) {
@@ -2831,7 +3118,10 @@ if (agentAddConfirmBtn) {
2831
3118
  const res = await fetch(`/channels/${channelId}`, {
2832
3119
  method: 'PATCH',
2833
3120
  headers: { 'Content-Type': 'application/json' },
2834
- body: JSON.stringify({ walletAddress: walletAddress || null, autoInvokeTools })
3121
+ body: JSON.stringify({
3122
+ walletAddress: walletAddress || null,
3123
+ autoInvokeTools
3124
+ })
2835
3125
  });
2836
3126
  if (!res.ok) throw new Error('update failed');
2837
3127
  const updated = await res.json();
@@ -2846,4 +3136,3 @@ if (agentAddConfirmBtn) {
2846
3136
  }
2847
3137
  });
2848
3138
  }
2849
-
@@ -64,6 +64,25 @@
64
64
  <ul class="channel-list" id="channel-list"></ul>
65
65
  </div>
66
66
 
67
+ <!-- v3: 本地/远端分隔线 — 可拖拽改变两边高度 -->
68
+ <div id="sidebar-split-handle" title="拖动调整上方/下方高度">
69
+ <div class="split-handle-grip"></div>
70
+ </div>
71
+
72
+ <!-- v3: 远端智能体 — 从 P2P 连接的 peer 拉取的 channel UI 元数据 -->
73
+ <div class="sidebar-section" id="remote-agents-section">
74
+ <div class="section-header" id="remote-agents-header" style="display:flex;align-items:center;justify-content:space-between;margin-bottom:8px;cursor:pointer;user-select:none;">
75
+ <span style="display:flex;align-items:center;gap:6px;">
76
+ <span id="remote-agents-toggle" style="font-size:10px;color:var(--text-muted);transition:transform 0.2s;">▼</span>
77
+ <span class="section-title" style="font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.5px;color:var(--text-muted);">远端智能体</span>
78
+ </span>
79
+ <button id="refresh-remote-agents-btn" title="从已连接 P2P peer 拉取" style="background:none;border:1px solid var(--border);color:var(--text-secondary);cursor:pointer;padding:2px 6px;border-radius:4px;font-size:11px;line-height:1;">↻ 刷新</button>
80
+ </div>
81
+ <ul class="channel-list" id="remote-channel-list" style="list-style:none;padding:0;margin:0;">
82
+ <li style="color:var(--text-muted);font-size:11px;padding:8px 4px;text-align:center;">(暂无, 点 ↻ 刷新)</li>
83
+ </ul>
84
+ </div>
85
+
67
86
  <div class="sidebar-footer">
68
87
  <div class="agent-status">
69
88
  <span class="status-dot"></span>
@@ -236,8 +255,19 @@
236
255
  <input type="file" id="judgment-import-file" accept=".json,.yaml,.yml,.md,.txt,.html,.htm" style="display:none">
237
256
  </div>
238
257
  <div id="judgment-error" class="form-info" style="display:none;color:#b91c1c;"></div>
239
- <h3 style="margin-top:24px;font-size:14px;font-weight:600;display:flex;align-items:center;gap:12px;flex-wrap:wrap;">
240
- <span>已记录的判断</span>
258
+ <!-- v3 重做: tab 切换 — 默认显示当前 channel 的 judgment 上下文, 切换后才是全部 -->
259
+ <div id="judgments-tabs" style="display:flex;border-bottom:1px solid #e5e7eb;margin-top:20px;">
260
+ <button class="judgment-tab active" data-tab="channel"
261
+ style="flex:1;padding:10px 12px;background:none;border:none;border-bottom:2px solid #2563eb;color:#2563eb;font-size:13px;font-weight:600;cursor:pointer;">
262
+ 本 channel <span id="judgments-tab-channel-name" style="font-weight:normal;color:#6b7280;"></span>
263
+ </button>
264
+ <button class="judgment-tab" data-tab="global"
265
+ style="flex:1;padding:10px 12px;background:none;border:none;border-bottom:2px solid transparent;color:#6b7280;font-size:13px;font-weight:600;cursor:pointer;">
266
+ 全局
267
+ </button>
268
+ </div>
269
+ <h3 style="margin-top:16px;font-size:14px;font-weight:600;display:flex;align-items:center;gap:12px;flex-wrap:wrap;">
270
+ <span id="judgments-list-title">本 channel 的判断力</span>
241
271
  <span style="display:flex;align-items:center;gap:6px;margin-left:auto;font-size:12px;font-weight:normal;">
242
272
  <label style="display:flex;align-items:center;gap:4px;cursor:pointer;color:#6b7280;">
243
273
  <input type="checkbox" id="judgment-select-all" style="cursor:pointer;"> 全选