// chat.js —— 光湖桥聊天主逻辑(只依赖 window.GHB,与壳无关) (() => { const $ = (id) => document.getElementById(id); const stream = $('stream'); const statusLine = $('status-line'); const peerInput = $('peer'); const textInput = $('text'); const sendBtn = $('send'); const connDot = $('conn'); const nodeIdEl = $('node-id'); const fmtTime = (ts) => new Date(ts || Date.now()).toLocaleTimeString('zh-CN', { hour12: false }); function sysMsg(text) { const d = document.createElement('div'); d.className = 'msg sys'; d.textContent = text; stream.appendChild(d); stream.scrollTop = stream.scrollHeight; } function bubble({ who, from, text, msgId }) { const d = document.createElement('div'); d.className = `msg ${who}`; if (msgId) d.dataset.msgId = msgId; const meta = document.createElement('span'); meta.className = 'meta'; meta.textContent = who === 'me' ? `我 · ${fmtTime()}` : `${from} · ${fmtTime()}`; const body = document.createElement('span'); body.textContent = text; d.appendChild(meta); d.appendChild(body); if (who === 'me') { const tick = document.createElement('span'); tick.className = 'tick'; tick.textContent = '…'; d.appendChild(tick); } stream.appendChild(d); stream.scrollTop = stream.scrollHeight; return d; } function setStatus(text, isErr) { statusLine.textContent = text || ''; statusLine.classList.toggle('err', !!isErr); } function markReceipt(msgId, verified) { const d = stream.querySelector(`[data-msg-id="${msgId}"]`); if (!d) return; const tick = d.querySelector('.tick'); if (!tick) return; tick.textContent = verified ? '✓ 已送达·已验签' : '✓ 已送达'; tick.classList.toggle('ok', verified); } function doSend() { const to = peerInput.value.trim(); const text = textInput.value.trim(); if (!to) { setStatus('请先填对方节点号', true); return; } if (!text) return; bubble({ who: 'me', text, msgId: null }); // 占位,sent 事件后补 msg_id pendingSend = { text }; window.GHB.send(to, text); textInput.value = ''; setStatus(`正在发送 → ${to} …`); } let pendingSend = null; // ── 事件绑定 ── window.GHB.on('connected', (ev) => { nodeIdEl.textContent = `我的节点号:${ev.node_id}`; connDot.className = 'conn on'; sysMsg(`已接入光湖桥,节点号 ${ev.node_id}(宿主:${window.GHB.kind})`); }); window.GHB.on('message', (ev) => { bubble({ who: 'peer', from: ev.from, text: ev.text, msgId: ev.msg_id }); peerInput.placeholder = ev.from; // 记住最近来信的对端 setStatus(''); }); window.GHB.on('sent', (ev) => { // 给最后一个"我"的气泡补上 msg_id 以便回执对账 const mine = stream.querySelectorAll('.msg.me'); const last = mine[mine.length - 1]; if (last && !last.dataset.msgId) last.dataset.msgId = ev.msg_id; setStatus(`已交给光湖桥 → ${ev.to}`); pendingSend = null; }); window.GHB.on('receipt', (ev) => { markReceipt(ev.msg_id, ev.verified); setStatus(ev.verified ? '对方已确认,回执验签通过' : '对方已确认'); }); window.GHB.on('error', (ev) => setStatus(`错误:${ev.message}`, true)); // 壳层自检钩子:自动填入并发送,让 UI 真实渲染完整链路 window.GHB.on('__autosend', (ev) => { peerInput.value = ev.to; textInput.value = ev.text; doSend(); }); sendBtn.addEventListener('click', doSend); textInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') doSend(); }); })();