feat: live presence, delivery-tick sync, brand rollout, call fixes, desktop 0.1.3
Live presence (fixes stale in-call/status until refresh — impossible in apps): - server broadcasts a user's status over the chat socket on connect/disconnect, call join/leave, and status change (chat.js broadcastPresence; signaling + routes hooks). - client onPresence() updates the contact dot + open-chat header live. Chat delivery ticks (#6): chat-list row now mirrors the thread (delivered→double grey, read→blue) via a new 'with' field on the delivered relay + onChatRead/onChatDelivered. Call fixes: no bogus 'host handed over' when a 1:1 call ends (leaveMeeting forced); branded call-connecting + chat-thread loaders; header subtitle tracks live call state. Notifications: web notify + sw.js use sender/group DP + brand icon (not old wordmark); desktop shell drops Web Push so only the single native toast fires (#5). Brand: master icon/splash/loaders wired everywhere (PWA/favicon/apple-touch/.ico), branded login (blue + gold CTA), branded toasts (BZToast) on all pages, Electron splash. Desktop: dev auto-targets localhost (packaged→prod); version 0.1.3 with new multi-size icon; dropped unused node-notifier; removed home-mockup.html. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,12 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Biz Connect — Agent Console</title>
|
||||
<meta name="theme-color" content="#1F3B73">
|
||||
<link rel="icon" href="/favicon.ico?v=3" sizes="any">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32.png?v=3">
|
||||
<link rel="apple-touch-icon" href="/apple-touch-icon-180.png?v=3">
|
||||
<link rel="stylesheet" href="/bizconnect-toast.css">
|
||||
<script src="/bizconnect-toast.js"></script>
|
||||
<style>
|
||||
:root{ --brand:#FFC708; --brand-d:#E0AC00; --blue:#1F3B73; --blue-d:#16294f; --ink:#1f2430; --muted:#6b7280; --bg:#f6f8fb; --card:#fff; --line:#e6e9ef; }
|
||||
*{box-sizing:border-box;}
|
||||
@@ -165,7 +171,7 @@ async function startConnect(){
|
||||
const ticket=document.getElementById('ticketInput').value.trim();
|
||||
const code=document.getElementById('codeInput').value.trim();
|
||||
if(!/^\d{6}$/.test(code)){ statusEl.textContent='Please enter the 6-digit code.'; return; }
|
||||
statusEl.textContent='Connecting…';
|
||||
statusEl.innerHTML='<img src="/loaders/loader-orbit.svg" width="20" height="20" style="vertical-align:-5px;margin-right:7px" alt="">Connecting…';
|
||||
ws.send(JSON.stringify({type:'code-connect',code,ticket}));
|
||||
}
|
||||
|
||||
@@ -173,7 +179,7 @@ function connectWS(){
|
||||
ws=new WebSocket((location.protocol==='https:'?'wss://':'ws://')+location.host+'/ws');
|
||||
ws.onmessage=async(e)=>{const m=JSON.parse(e.data);const statusEl=document.getElementById('status');switch(m.type){
|
||||
case 'code-pending': sessionId=m.sessionId; renderWaiting(); setupPeer(); break;
|
||||
case 'session-ready': if(statusEl)statusEl.textContent='Allowed — connecting…'; break;
|
||||
case 'session-ready': if(statusEl)statusEl.innerHTML='<img src="/loaders/loader-orbit.svg" width="20" height="20" style="vertical-align:-5px;margin-right:7px" alt="">Allowed — connecting…'; break;
|
||||
case 'offer': await pc.setRemoteDescription(new RTCSessionDescription(m.sdp));
|
||||
// Acquire the agent mic once; on renegotiation (e.g. customer unmutes) just answer.
|
||||
if(!window.__mic){ try{ const mic=await navigator.mediaDevices.getUserMedia({audio:true}); window.__mic=mic; mic.getAudioTracks().forEach(t=>pc.addTrack(t,mic)); }catch(e){} }
|
||||
@@ -346,8 +352,9 @@ function buildChatPanel(){
|
||||
}
|
||||
function toggleChat(){const p=document.getElementById('chatPanel');if(!p)return;chatOpen=!chatOpen;p.style.display=chatOpen?'flex':'none';const b=document.getElementById('chatBtn');if(chatOpen){b&&(b.style.background='#475569');const i=document.getElementById('chatInput');if(i)setTimeout(()=>i.focus(),50);}}
|
||||
function addChat(msg){const c=document.getElementById('chatMsgs');if(!c)return;const mine=msg.from==='__self';const w=document.createElement('div');w.style.cssText='max-width:85%;padding:.4rem .6rem;border-radius:10px;'+(mine?'align-self:flex-end;background:#EAF0FB;color:#16294f':'align-self:flex-start;background:#f1f5f9;color:#1f2430');w.innerHTML='<div style="font-size:.7rem;opacity:.65;margin-bottom:2px">'+esc(msg.name||'')+'</div>'+esc(msg.text);c.appendChild(w);c.scrollTop=c.scrollHeight;if(!mine)notifyMsg(msg);}
|
||||
function notifyMsg(msg){const b=document.getElementById('chatBtn');if(b&&!chatOpen){b.style.background='#FFC708';b.style.color='#1f2430';}toast((msg.name||'Message')+': '+msg.text);try{beep();}catch(_){}if('Notification' in window && Notification.permission==='granted' && (document.hidden||!chatOpen)){try{new Notification('New message from '+(msg.name||'support'),{body:msg.text});}catch(_){}}}
|
||||
function toast(text){let t=document.getElementById('msgToast');if(!t){t=document.createElement('div');t.id='msgToast';t.style.cssText='position:fixed;top:18px;left:50%;transform:translateX(-50%);z-index:2147483600;background:#16a34a;color:#fff;padding:.7rem 1.1rem;border-radius:12px;box-shadow:0 10px 26px rgba(0,0,0,.35);font-size:.92rem;font-weight:600;border:2px solid #0c7a36;max-width:82vw;transition:opacity .4s';document.body.appendChild(t);}t.innerHTML='\ud83d\udcac '+text;t.style.opacity='1';clearTimeout(window.__toastT);window.__toastT=setTimeout(()=>{t.style.opacity='0';},2800);}
|
||||
function notifyMsg(msg){const b=document.getElementById('chatBtn');if(b&&!chatOpen){b.style.background='#FFC708';b.style.color='#1f2430';}if(window.BZToast)BZToast.message(msg.text,{title:(msg.name||'Message')});try{beep();}catch(_){}if('Notification' in window && Notification.permission==='granted' && (document.hidden||!chatOpen)){try{new Notification('New message from '+(msg.name||'support'),{body:msg.text});}catch(_){}}}
|
||||
// Branded toast (BZToast, /bizconnect-toast.js). Classify by wording so errors show red.
|
||||
function toast(text){var s=String(text==null?'':text);if(!window.BZToast)return;if(/could ?n.t|cannot|failed|invalid|error|denied|expired|not found|please enter/i.test(s))return BZToast.error(s);return BZToast.message(s);}
|
||||
let __ac=null;
|
||||
function ensureAudio(){try{__ac=__ac||new (window.AudioContext||window.webkitAudioContext)();if(__ac.state==='suspended')__ac.resume();}catch(_){}}
|
||||
function beep(){ensureAudio();if(!__ac)return;try{const o=__ac.createOscillator(),g=__ac.createGain();o.type='sine';o.connect(g);g.connect(__ac.destination);const t0=__ac.currentTime;o.frequency.setValueAtTime(880,t0);o.frequency.setValueAtTime(660,t0+0.09);g.gain.setValueAtTime(0.0001,t0);g.gain.exponentialRampToValueAtTime(0.12,t0+0.02);g.gain.exponentialRampToValueAtTime(0.0001,t0+0.22);o.start(t0);o.stop(t0+0.24);}catch(_){}}
|
||||
|
||||
Reference in New Issue
Block a user