|
1 | | -<!DOCTYPE html> |
| 1 | +<!DOCTYPE html> |
2 | 2 | <html lang="en"> |
3 | 3 |
|
4 | 4 | <head> |
|
10 | 10 | <link |
11 | 11 | href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" |
12 | 12 | rel="stylesheet" /> |
13 | | - <style> |
| 13 | + <link rel="stylesheet" href="/static/css/main.css" /> |
| 14 | + <style>PLACEHOLDER_TO_REMOVE |
14 | 15 | /* ==================================================================================== |
15 | 16 | Reset & Base |
16 | 17 | ==================================================================================== */ |
|
707 | 708 | cursor: pointer; |
708 | 709 | } |
709 | 710 |
|
| 711 | + /* ---- Author grouping ---- */ |
| 712 | + .msg-group-cont { |
| 713 | + margin-top: -10px; |
| 714 | + } |
| 715 | + |
| 716 | + .msg-group-cont .msg-avatar { |
| 717 | + visibility: hidden; |
| 718 | + } |
| 719 | + |
710 | 720 | .msg-col { |
711 | 721 | display: flex; |
712 | 722 | flex-direction: column; |
|
1124 | 1134 | /** Agent Status Bar **/ |
1125 | 1135 | #agent-status-bar { |
1126 | 1136 | display: flex; |
| 1137 | + align-items: center; |
1127 | 1138 | background: var(--bg-base); |
1128 | 1139 | border-top: 1px solid var(--border-light); |
1129 | | - padding: 8px 12px; |
1130 | | - height: 56px; |
| 1140 | + padding: 6px 12px; |
| 1141 | + min-height: 64px; |
1131 | 1142 | overflow-x: auto; |
1132 | | - overflow-y: hidden; |
| 1143 | + overflow-y: visible; |
1133 | 1144 | gap: 10px; |
1134 | 1145 | font-size: 12px; |
| 1146 | + flex-shrink: 0; |
1135 | 1147 | } |
1136 | 1148 |
|
1137 | 1149 | #agent-status-list { |
|
1663 | 1675 | <script src="/static/js/components/acb-compose-shell.js?v=3"></script> |
1664 | 1676 | <script src="/static/js/components/acb-agent-status-shell.js?v=2"></script> |
1665 | 1677 | <script src="/static/js/shared-threads.js?v=2"></script> |
1666 | | - <script src="/static/js/shared-agent-status.js?v=2"></script> |
1667 | | - <script src="/static/js/components/acb-agent-status-item.js?v=2"></script> |
| 1678 | + <script src="/static/js/shared-agent-status.js?v=3"></script> |
| 1679 | + <script src="/static/js/components/acb-agent-status-item.js?v=3"></script> |
1668 | 1680 | <script src="/static/js/shared-agents.js?v=2"></script> |
1669 | 1681 | <script src="/static/js/shared-chat.js?v=2"></script> |
1670 | 1682 | <script src="/static/js/shared-tooltip.js?v=2"></script> |
|
1909 | 1921 | // SSE |
1910 | 1922 | // ============================================================================================== |
1911 | 1923 | function startSSE() { |
1912 | | - const es = new EventSource('/events'); |
1913 | | - es.onopen = () => setConnected(true); |
1914 | | - es.onerror = () => { setConnected(false); setTimeout(startSSE, 3000); es.close(); }; |
1915 | | - |
1916 | | - es.onmessage = async (e) => { |
1917 | | - const ev = JSON.parse(e.data); |
1918 | | - const p = ev.payload; |
1919 | | - |
1920 | | - if (ev.type === 'msg.new') { |
1921 | | - if (p.thread_id === activeThreadId) await loadNewMessages(); |
1922 | | - await refreshThreads(); // update sidebar counts |
1923 | | - } |
1924 | | - if (ev.type === 'thread.new' || ev.type === 'thread.state' || ev.type === 'thread.closed' || ev.type === 'thread.archived' || ev.type === 'thread.deleted') { |
1925 | | - await refreshThreads(); |
1926 | | - if (ev.type === 'thread.deleted' && p.thread_id === activeThreadId) { |
1927 | | - activeThreadId = null; |
1928 | | - document.getElementById('msg-pane').innerHTML = ''; |
1929 | | - const hdr = document.getElementById('thread-header'); |
1930 | | - if (hdr) hdr.textContent = ''; |
1931 | | - } |
1932 | | - } |
1933 | | - if (ev.type === 'agent.online' || ev.type === 'agent.offline') { |
1934 | | - await refreshAgents(); |
1935 | | - } |
1936 | | - if (ev.type === 'agent.typing' && p.thread_id === activeThreadId) { |
1937 | | - if (p.is_typing) showTyping(p.agent_id); |
1938 | | - else hideTyping(p.agent_id); |
1939 | | - } |
1940 | | - }; |
1941 | 1924 | return window.AcbSSE.startSSE({ |
1942 | 1925 | getActiveThreadId: () => activeThreadId, |
1943 | 1926 | onMsgNew: async () => loadNewMessages(), |
|
2073 | 2056 |
|
2074 | 2057 | function openThreadContextMenu(event, thread) { |
2075 | 2058 | contextMenuThread = window.AcbThreads.openThreadContextMenu(event, thread); |
2076 | | - // 保存右键点击的位置,用于确认对话框定位 |
| 2059 | + // Store right-click position for confirm dialog positioning |
2077 | 2060 | contextMenuThread._clickX = event.clientX; |
2078 | 2061 | contextMenuThread._clickY = event.clientY; |
2079 | 2062 | } |
|
2213 | 2196 | const avatarEmoji = window.AcbUtils?.getAgentAvatarEmoji ? window.AcbUtils.getAgentAvatarEmoji(authorKey) : '🤖'; |
2214 | 2197 | const bgAlpha = `${color}22`; // ~13% alpha tint for avatar bg |
2215 | 2198 |
|
| 2199 | + // ---- Author grouping: detect if this message continues a group ---- |
| 2200 | + const prevRow = box.querySelector('.msg-row[data-seq]:last-of-type') ?? [...box.querySelectorAll('.msg-row[data-seq]')].at(-1); |
| 2201 | + const prevAuthorKey = prevRow?.getAttribute('data-author-id') ?? null; |
| 2202 | + const prevTimestamp = prevRow?.getAttribute('data-created-at') ?? null; |
| 2203 | + const isContinuation = window.AcbUtils?.shouldGroupWithPrevious |
| 2204 | + ? window.AcbUtils.shouldGroupWithPrevious(prevAuthorKey, prevTimestamp, authorKey, m.created_at, isSystem, isHuman) |
| 2205 | + : false; |
| 2206 | + |
2216 | 2207 | const row = document.createElement('div'); |
2217 | | - row.className = `msg-row ${isHuman ? 'msg-row-right' : 'msg-row-left'}`; |
| 2208 | + row.className = `msg-row ${isHuman ? 'msg-row-right' : 'msg-row-left'} ${isContinuation ? 'msg-group-cont' : 'msg-group-start'}`; |
2218 | 2209 | row.setAttribute('data-author-id', String(m.author_id ?? authorLabel)); |
| 2210 | + row.setAttribute('data-created-at', String(m.created_at ?? '')); |
2219 | 2211 | row.setAttribute('data-seq', String(m.seq)); |
2220 | 2212 |
|
2221 | 2213 | // Check if this message has mentions (look in metadata if string/object) |
|
2231 | 2223 | row.innerHTML = ` |
2232 | 2224 | <div class="msg-avatar" style="background:${bgAlpha};color:${color};border:1px solid ${color}44">${avatarEmoji}</div> |
2233 | 2225 | <div class="msg-col"> |
2234 | | - <div class="msg-header"> |
| 2226 | + ${isContinuation ? '' : `<div class="msg-header"> |
2235 | 2227 | <span class="msg-author-label" style="color:${color}">${esc(authorLabel)}</span> |
2236 | 2228 | <span class="msg-time-label">seq ${m.seq} ${fmtTime(m.created_at)}</span> |
2237 | | - </div> |
| 2229 | + </div>`} |
2238 | 2230 | <div class="bubble-v2" style="border-color:${color}55;${isHuman ? 'border-right-color' : 'border-left-color'}:${color}"></div> |
2239 | 2231 | </div>`; |
2240 | 2232 |
|
|
2244 | 2236 |
|
2245 | 2237 | // Render images if any |
2246 | 2238 | let images = null; |
2247 | | - console.log(`[Bubble] Message metadata:`, m.metadata, `Type: ${typeof m.metadata}`); |
2248 | 2239 | if (m.metadata) { |
2249 | | - // Parse metadata if it's a string |
2250 | 2240 | if (typeof m.metadata === 'string') { |
2251 | 2241 | try { |
2252 | 2242 | const meta = JSON.parse(m.metadata); |
2253 | 2243 | images = meta.images; |
2254 | | - console.log(`[Bubble] Parsed metadata as string, images:`, images); |
2255 | 2244 | } catch (e) { |
2256 | | - console.error(`[Bubble] Failed to parse metadata JSON:`, e); |
| 2245 | + console.warn(`[Bubble] Failed to parse metadata JSON:`, e); |
2257 | 2246 | } |
2258 | 2247 | } else if (typeof m.metadata === 'object') { |
2259 | 2248 | images = m.metadata.images; |
2260 | | - console.log(`[Bubble] Got metadata as object, images:`, images); |
2261 | 2249 | } |
2262 | | - } else { |
2263 | | - console.log(`[Bubble] No metadata in message`); |
2264 | 2250 | } |
2265 | 2251 |
|
2266 | 2252 | if (images && Array.isArray(images) && images.length > 0) { |
2267 | | - console.log(`[Bubble] Rendering ${images.length} images`); |
2268 | 2253 | const imgContainer = document.createElement('div'); |
2269 | 2254 | imgContainer.style.cssText = 'display: flex; gap: 8px; flex-wrap: wrap; margin-top: 8px;'; |
2270 | 2255 | images.forEach(img => { |
|
2274 | 2259 | imgEl.style.cssText = 'width: min(520px, 80vw); max-width: 100%; max-height: 70vh; border-radius: 8px; object-fit: contain; cursor: zoom-in; background: rgba(0,0,0,0.05);'; |
2275 | 2260 | imgEl.onclick = () => window.open(img.url, '_blank'); |
2276 | 2261 | imgContainer.appendChild(imgEl); |
2277 | | - console.log(`[Bubble] Added image: ${img.url}`); |
2278 | 2262 | }); |
2279 | 2263 | bubbleEl.appendChild(imgContainer); |
2280 | | - } else { |
2281 | | - console.log(`[Bubble] No images to render`); |
2282 | 2264 | } |
2283 | 2265 | } |
2284 | 2266 |
|
|
2384 | 2366 | } |
2385 | 2367 |
|
2386 | 2368 | function getAgentState(agent) { |
2387 | | - // 优先使用last_activity_time判断状态,而非is_online(后者基于heartbeat超时) |
| 2369 | + // Prefer last_activity_time for state detection; is_online relies on heartbeat timeout |
2388 | 2370 | const activityTime = agent.last_activity_time ? new Date(agent.last_activity_time) : null; |
2389 | 2371 | const now = new Date(); |
2390 | 2372 |
|
2391 | 2373 | if (!activityTime) { |
2392 | | - // 完全没有活动记录 |
| 2374 | + // No activity record at all |
2393 | 2375 | return agent.is_online ? 'Waiting' : 'Offline'; |
2394 | 2376 | } |
2395 | 2377 |
|
2396 | 2378 | const secondsAgo = (now - activityTime) / 1000; |
2397 | 2379 |
|
2398 | | - // 根据活动类型和时间判断状态 |
| 2380 | + // Determine state based on activity type and elapsed time |
2399 | 2381 | if (agent.last_activity === 'msg_wait' && secondsAgo < 60) return 'Waiting'; |
2400 | 2382 | if (secondsAgo < 30) return 'Active'; |
2401 | | - if (secondsAgo < 300) return 'Idle'; // 5σêΘÆƒσൣëµ┤╗σè¿τ«ù Idle |
| 2383 | + if (secondsAgo < 300) return 'Idle'; // active within 5 minutes → Idle |
2402 | 2384 |
|
2403 | | - // Φ╢àΦ┐ç5σêΘÆƒµ▓íµ┤╗σè¿Σ╕öis_online=falseµëìµÿ╛τñ║Offline |
| 2385 | + // More than 5 minutes inactive and is_online=false → Offline |
2404 | 2386 | return agent.is_online ? 'Idle' : 'Offline'; |
2405 | 2387 | } |
2406 | 2388 |
|
|
0 commit comments