Files
BizGaze_Remote/server/public/connect.html
T
2026-06-05 17:29:09 +05:30

184 lines
11 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>BizGaze Support — Agent Console</title>
<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;}
body{font-family:'Segoe UI',system-ui,sans-serif;background:var(--bg);color:var(--ink);margin:0;}
.topbar{background:var(--blue);padding:.7rem 1.2rem;display:flex;align-items:center;justify-content:space-between;gap:.6rem;}
.brandrow{display:flex;align-items:center;gap:.6rem;}
.logo{width:28px;height:28px;border-radius:7px;background:var(--brand);display:grid;place-items:center;font-weight:800;color:var(--blue);}
.brand{font-weight:700;color:#fff;} .brand span{color:var(--brand);font-weight:600;}
.agentchip{color:#dbe4f5;font-size:.85rem;}
.agentchip b{color:#fff;} .agentchip a{color:var(--brand);text-decoration:none;margin-left:.5rem;cursor:pointer;}
.wrap{min-height:calc(100vh - 50px);display:grid;place-items:center;padding:1.5rem;}
.card{background:var(--card);border:1px solid var(--line);border-radius:18px;padding:2.2rem;max-width:430px;width:100%;box-shadow:0 10px 30px rgba(20,30,60,.06);}
h1{font-size:1.35rem;margin:.1rem 0 .3rem;color:var(--blue);text-align:center;}
.sub{color:var(--muted);font-size:.92rem;margin-bottom:1.3rem;text-align:center;}
.lbl{display:block;font-size:.74rem;color:var(--muted);text-transform:uppercase;letter-spacing:.06em;margin:.85rem 0 .25rem;}
input{width:100%;padding:.7rem;border-radius:10px;border:2px solid var(--line);background:#fbfcfe;color:var(--ink);font-size:1rem;}
input:focus{outline:none;border-color:var(--brand);}
input.code{font-size:1.7rem;letter-spacing:.35rem;text-align:center;}
.btn{margin-top:1.1rem;width:100%;padding:.85rem;background:var(--brand);color:var(--ink);border:none;border-radius:10px;font-weight:700;font-size:1.02rem;cursor:pointer;}
.btn:hover{background:var(--brand-d);}
.status{color:var(--muted);font-size:.88rem;margin-top:.9rem;text-align:center;min-height:1.1em;}
.err{color:#b91c1c;}
.prefill{background:#EAF0FB;border:1px solid #c7d6f0;border-radius:8px;padding:.5rem .7rem;font-size:.85rem;color:var(--blue-d);margin-top:.25rem;}
.topbar2{background:var(--card);border-bottom:1px solid var(--line);padding:.5rem 1rem;display:none;justify-content:space-between;align-items:center;}
.topbar2.show{display:flex;} #barStatus{font-weight:600;font-size:.9rem;color:var(--blue);}
#endBtn{padding:.45rem 1rem;background:#fee2e2;color:#b91c1c;border:none;border-radius:8px;font-weight:600;cursor:pointer;}
#video{width:100vw;height:calc(100vh - 46px);background:#0b1220;object-fit:contain;display:none;cursor:crosshair;outline:none;}
</style>
</head>
<body>
<div class="topbar" id="topbar">
<div class="brandrow"><img src="/logo.png" alt="" style="height:46px;width:auto;max-width:190px;border-radius:8px;object-fit:contain;background:#fff;padding:5px 12px;image-rendering:-webkit-optimize-contrast" onerror="this.replaceWith(Object.assign(document.createElement('div'),{className:'logo',textContent:'B'}))"><div class="brand">BizGaze <span>Support</span></div></div>
<div class="agentchip" id="agentChip"></div>
</div>
<div class="topbar2" id="bar"><span id="barStatus"></span><button id="endBtn">End session</button></div>
<div class="wrap" id="wrap"><div class="card" id="card"></div></div>
<video id="video" autoplay playsinline muted tabindex="0"></video>
<script>
const params=new URLSearchParams(location.search);
const presetTicket=params.get('ticket')||'';
const presetCode=params.get('code')||'';
const card=document.getElementById('card'), wrap=document.getElementById('wrap'),
agentChip=document.getElementById('agentChip'), bar=document.getElementById('bar'),
topbar=document.getElementById('topbar'), video=document.getElementById('video'), barStatus=document.getElementById('barStatus');
let ws,pc,inputChannel,sessionId,me=null;
async function api(path,body,method='POST'){
const opt={method,headers:{'Content-Type':'application/json'}};
if(body)opt.body=JSON.stringify(body);
const r=await fetch(path,opt); const data=await r.json().catch(()=>({}));
if(!r.ok) throw new Error(data.error||'request failed'); return data;
}
function onEnter(ids, fn){ ids.forEach(id => { const el=document.getElementById(id); if(el) el.addEventListener('keydown', e=>{ if(e.key==='Enter'){ e.preventDefault(); fn(); } }); }); }
// ---- boot: are we a logged-in agent? ----
(async function boot(){
try{ me=await api('/api/me',null,'GET'); renderAgent(); }
catch{ renderLogin(); }
})();
// ---- LOGIN ----
function renderLogin(){
agentChip.textContent='';
card.innerHTML = `
<h1>Agent sign in</h1>
<div class="sub">Sign in to start a support session. Your name is shown to the customer.</div>
<span class="lbl">Email</span><input id="email" type="email" placeholder="you@bizgaze.com">
<span class="lbl">Password</span><input id="pw" type="password" placeholder="password">
<button class="btn" id="loginBtn">Sign in</button>
<div class="status err" id="err"></div>`;
{
const doSignIn=async()=>{
try{
await api('/api/login',{email:document.getElementById('email').value,password:document.getElementById('pw').value});
me=await api('/api/me',null,'GET'); renderAgent();
}catch(e){ document.getElementById('err').textContent=e.message; }
};
document.getElementById('loginBtn').onclick=doSignIn;
onEnter(['email','pw'], doSignIn);
}
}
// ---- AGENT CONNECT ----
function renderAgent(){
const displayName = me.name || me.email;
agentChip.innerHTML = `Signed in as <b>${esc(displayName)}</b><a id="logout">Log out</a>`;
document.getElementById('logout').onclick=async()=>{ await api('/api/logout'); location.reload(); };
card.innerHTML = `
<h1>Start a support session</h1>
<div class="sub">Ask the customer to open the share page and read you their code.</div>
<span class="lbl">Ticket number (optional)</span>
<input id="ticketInput" maxlength="40" placeholder="e.g. TKT-1042 — leave blank for a direct session" value="${esc(presetTicket)}" ${presetTicket?'readonly':''}>
${presetTicket?'<div class="prefill">Linked from service request '+esc(presetTicket)+'</div>':''}
<span class="lbl">Session code from customer</span>
<input id="codeInput" class="code" maxlength="6" inputmode="numeric" placeholder="000000" value="${esc(presetCode)}">
<button class="btn" id="connectBtn">Connect</button>
<div class="status" id="status">Enter the customer's code to begin.</div>`;
connectWS();
document.getElementById('connectBtn').onclick=startConnect;
onEnter(['ticketInput','codeInput'], startConnect);
}
async function startConnect(){
const statusEl=document.getElementById('status'); statusEl.className='status';
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…';
ws.send(JSON.stringify({type:'code-connect',code,ticket}));
}
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 'offer': await pc.setRemoteDescription(new RTCSessionDescription(m.sdp));
const ans=await pc.createAnswer(); await pc.setLocalDescription(ans);
ws.send(JSON.stringify({type:'answer',sessionId,sdp:pc.localDescription})); break;
case 'ice-candidate': if(m.candidate&&pc) await pc.addIceCandidate(new RTCIceCandidate(m.candidate)); break;
case 'session-denied': renderEnded('The customer declined the request.'); break;
case 'session-ended': {
const msgs={'share-cancelled':'The customer cancelled the screen selection. Ask them to refresh their page for a new code.',
'customer-ended':'The customer stopped sharing their screen. Ask them to refresh their page for a new code.',
'agent-ended':'You ended the session.'};
renderEnded(msgs[m.reason]||'The session has ended.'); break;
}
case 'error': if(statusEl){statusEl.className='status err';statusEl.textContent=m.message;} break;
}};
}
function renderWaiting(){
card.innerHTML=`
<h1>Waiting for the customer…</h1>
<div class="sub">Code accepted. The customer has been asked to tap <b>Allow</b> on their screen.</div>
<div class="status" id="status">Waiting for the customer to tap Allow…</div>
<button class="btn" id="cancelBtn" style="background:#eef1f6;color:#1F3B73">Cancel</button>`;
document.getElementById('cancelBtn').onclick=()=>{ try{ws.send(JSON.stringify({type:'end-session',sessionId}));}catch(e){} location.reload(); };
}
function renderEnded(msg){
if(pc){ try{pc.close();}catch(e){} pc=null; }
video.style.display='none'; bar.classList.remove('show');
topbar.style.display='flex'; wrap.style.display='grid';
card.innerHTML=`
<h1>Session ended</h1>
<div class="sub">${esc(msg)}</div>
<button class="btn" id="againBtn">Start a new session</button>`;
document.getElementById('againBtn').onclick=()=>location.reload();
}
function setupPeer(){
pc=new RTCPeerConnection({iceServers:[{urls:'stun:stun.l.google.com:19302'}]});
inputChannel=pc.createDataChannel('input',{ordered:true});
pc.ontrack=(ev)=>{
video.srcObject=ev.streams[0]; wrap.style.display='none'; topbar.style.display='none'; bar.classList.add('show'); video.style.display='block';
barStatus.textContent='Connected — viewing the customers screen'; video.focus();
};
pc.onicecandidate=(ev)=>{if(ev.candidate)ws.send(JSON.stringify({type:'ice-candidate',sessionId,candidate:ev.candidate}));};
}
const send=(o)=>{if(inputChannel&&inputChannel.readyState==='open')inputChannel.send(JSON.stringify(o));};
const rel=(e)=>{const r=video.getBoundingClientRect();return{x:(e.clientX-r.left)/r.width,y:(e.clientY-r.top)/r.height};};
let lm=0;
video.addEventListener('mousemove',e=>{const t=performance.now();if(t-lm<30)return;lm=t;send({kind:'mousemove',...rel(e)});});
video.addEventListener('mousedown',e=>{video.focus();send({kind:'mousedown',button:e.button,...rel(e)});});
video.addEventListener('mouseup',e=>send({kind:'mouseup',button:e.button,...rel(e)}));
video.addEventListener('dblclick',e=>send({kind:'dblclick',...rel(e)}));
video.addEventListener('wheel',e=>{e.preventDefault();send({kind:'scroll',dx:e.deltaX,dy:e.deltaY});},{passive:false});
video.addEventListener('contextmenu',e=>e.preventDefault());
video.addEventListener('keydown',e=>{e.preventDefault();send({kind:'keydown',key:e.key,code:e.code});});
video.addEventListener('keyup',e=>{e.preventDefault();send({kind:'keyup',key:e.key,code:e.code});});
document.getElementById('endBtn').onclick=()=>{ws.send(JSON.stringify({type:'end-session',sessionId,reason:'agent-ended'}));};
function esc(s){return String(s==null?'':s).replace(/[&<>"]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]));}
</script>
</body>
</html>