Files
BizGaze_Remote/server/public/share.html
T

285 строки
28 KiB
HTML
Исходник Обычный вид История

2026-06-05 17:29:09 +05:30
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>BizGaze Support — Share your screen</title>
<style>
:root{ --brand:#FFC708; --brand-d:#E0AC00; --blue:#1F3B73; --blue-d:#16294f; --blue-soft:#EAF0FB; --ink:#1f2430; --muted:#6b7280; --bg:#f6f8fb; --card:#ffffff; --line:#e6e9ef; }
*{box-sizing:border-box;}
body{font-family:'Segoe UI',system-ui,sans-serif;background:var(--bg);color:var(--ink);margin:0;}
.stage{display:flex;min-height:100vh;}
.brandpanel{flex:1;background:linear-gradient(160deg,var(--blue),var(--blue-d));color:#fff;display:flex;flex-direction:column;justify-content:center;align-items:center;text-align:center;padding:2.5rem;}
.mark{width:88px;height:88px;border-radius:22px;background:var(--brand);display:grid;place-items:center;font-weight:800;font-size:2.6rem;color:var(--blue);margin-bottom:1.2rem;box-shadow:0 12px 30px rgba(0,0,0,.25);}
.wordmark{font-size:2.2rem;font-weight:800;letter-spacing:.01em;}
.wordmark span{color:var(--brand);}
.tagline{color:#cdd7ee;margin-top:.6rem;font-size:1rem;max-width:300px;line-height:1.5;}
.panelside{flex:1;display:flex;align-items:center;justify-content:center;padding:2rem;}
.card{background:var(--card);border:1px solid var(--line);border-radius:18px;padding:2.4rem;max-width:440px;width:100%;text-align:center;box-shadow:0 10px 30px rgba(20,30,60,.06);}
h1{font-size:1.45rem;margin:.2rem 0 .4rem;color:var(--blue);}
.sub{color:var(--muted);font-size:.97rem;line-height:1.5;margin-bottom:1.6rem;}
.codewrap{background:#fffdf2;border:2px dashed var(--brand);border-radius:14px;padding:1.2rem;}
.codelabel{font-size:.78rem;letter-spacing:.08em;text-transform:uppercase;color:var(--muted);margin-bottom:.3rem;}
.code{font-size:clamp(1.6rem,9vw,2.6rem);letter-spacing:clamp(.1rem,2vw,.4rem);padding-left:clamp(.1rem,2vw,.4rem);font-weight:800;color:var(--ink);white-space:nowrap;}
2026-06-05 17:29:09 +05:30
.status{margin-top:1.3rem;padding:.7rem 1rem;border-radius:10px;background:#f1f5f9;color:#475569;font-size:.92rem;}
.status.on{background:#ecfdf3;color:#15803d;}
.consent{margin-top:1.3rem;border:1px solid #c7d6f0;background:var(--blue-soft);border-left:5px solid var(--blue);border-radius:12px;padding:1.3rem;text-align:left;color:var(--blue-d);}
.consent .who{font-weight:700;color:var(--blue);}
.btns{margin-top:1rem;display:flex;gap:.6rem;}
button{flex:1;padding:.8rem 1rem;border:none;border-radius:10px;font-weight:700;font-size:.98rem;cursor:pointer;}
.grant{background:var(--brand);color:var(--ink);} .grant:hover{background:var(--brand-d);}
.deny{background:#fff;color:var(--blue);border:1px solid #c7d6f0;} .deny:hover{background:#f3f6fc;}
.foot{color:var(--muted);font-size:.8rem;margin-top:1.4rem;}
.indicator{position:fixed;top:0;left:0;right:0;background:#dc2626;color:#fff;text-align:center;padding:.5rem;font-size:.9rem;display:none;font-weight:600;z-index:9;}
.indicator.show{display:block;}
/* Embedded inside the home shell: hide own chrome (the shell provides it). */
html.embed .brandpanel{display:none!important;}
html.embed #homeLink{display:none!important;}
html.embed .panelside{flex:1;}
2026-06-05 17:29:09 +05:30
@media(max-width:860px){ .stage{flex-direction:column;} .brandpanel{padding:2rem;min-height:auto;} .mark{width:60px;height:60px;border-radius:16px;font-size:1.8rem;margin-bottom:.7rem;} .wordmark{font-size:1.5rem;} .tagline{display:none;} }
.profile{position:relative}
.profile .pbtn{display:flex;align-items:center;gap:.4rem;background:rgba(255,255,255,.14);color:#fff;border:1px solid #46598c;border-radius:10px;padding:.45rem .85rem;font-weight:600;font-size:.88rem;cursor:pointer}
.profile .pbtn:hover{background:rgba(255,255,255,.24)}
.profile .pmenu{position:absolute;right:0;top:calc(100% + 6px);background:#fff;border:1px solid #e6e9ef;border-radius:10px;box-shadow:0 10px 28px rgba(0,0,0,.18);min-width:190px;overflow:hidden;z-index:5000;display:none}
.profile .pmenu.open{display:block}
.profile .pmenu a{display:block;padding:.6rem .9rem;color:#1f2430;text-decoration:none;font-size:.9rem;cursor:pointer}
.profile .pmenu a:hover{background:#f1f5f9}
.profile .pmenu a.danger{color:#b91c1c;border-top:1px solid #eef1f6}
2026-06-05 17:29:09 +05:30
</style>
<script src="/icons.js?v=3"></script>
2026-06-05 17:29:09 +05:30
</head>
<body>
<script>if(new URLSearchParams(location.search).get('embed')==='1')document.documentElement.classList.add('embed');</script>
2026-06-05 17:29:09 +05:30
<div class="indicator" id="indicator">● Your screen is being shared — close this tab anytime to stop</div>
<a href="/" id="homeLink" style="position:fixed;top:14px;left:16px;z-index:50;display:inline-flex;align-items:center;gap:6px;background:rgba(255,255,255,.92);color:#1F3B73;text-decoration:none;font-weight:600;font-size:.86rem;padding:.45rem .8rem;border-radius:10px;box-shadow:0 4px 12px rgba(0,0,0,.15)"><span data-ic="arrowLeft" data-sz="16"></span> Home</a>
2026-06-05 17:29:09 +05:30
<div class="stage">
<div class="brandpanel">
<img src="/logo.png" alt="" style="width:88px;height:88px;border-radius:22px;object-fit:contain;margin-bottom:1.2rem;background:#fff;padding:8px;box-shadow:0 12px 30px rgba(0,0,0,.25)" onerror="this.replaceWith(Object.assign(document.createElement('div'),{className:'mark',textContent:'B'}))">
<div class="wordmark">BizGaze <span>Support</span></div>
<div class="tagline">Secure, instant remote support — no downloads, you stay in control.</div>
</div>
<div class="panelside">
<div class="card">
<h1>Let's get you connected</h1>
<div class="sub">Share the code below with your BizGaze support agent.</div>
<div class="codewrap">
<div class="codelabel">Your session code</div>
<div style="display:flex;align-items:center;justify-content:center;gap:.7rem">
<div class="code" id="code">······</div>
<button id="copyBtn" title="Click to copy" aria-label="Click to copy" style="flex:0 0 auto;width:28px;height:28px;padding:0;background:#fff;border:1px solid var(--brand);color:var(--blue);border-radius:7px;cursor:pointer;display:inline-flex;align-items:center;justify-content:center"><svg id="copyIcon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button>
2026-06-05 17:29:09 +05:30
</div>
</div>
<div id="status" class="status">Preparing your code…</div>
<div id="consentBox"></div>
<div class="foot"><span data-ic="lock" data-sz="14"></span> You stay in control. The agent can only see your screen after you tap Allow, and you can stop anytime.</div>
2026-06-05 17:29:09 +05:30
</div>
</div>
</div>
<script>
let ICE={iceServers:[{urls:'stun:stun.l.google.com:19302'}]};
2026-06-10 15:47:02 +05:30
let SHARER_NAME='Customer';
try{fetch('/api/me').then(r=>r.ok?r.json():null).then(m=>{if(m&&(m.name||m.email))SHARER_NAME=m.name||m.email;}).catch(()=>{});}catch(_){}
const IS_MOBILE=/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini|Mobile/i.test(navigator.userAgent||'');
let __icePromise=Promise.resolve();try{__icePromise=fetch('/api/ice').then(r=>r.ok?r.json():null).then(c=>{if(c&&c.iceServers)ICE=c;}).catch(()=>{});}catch(_){}
2026-06-10 15:47:02 +05:30
async function ensureIce(){try{await __icePromise;}catch(_){}return ICE;}
function pEsc(s){return String(s==null?'':s).replace(/[&<>"]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]));}
// When embedded in the home shell, tell the parent when a session is live so the
// rail can show a "return here" indicator.
function bzcSession(active){try{if(window.parent&&window.parent!==window)window.parent.postMessage({type:'bzc-session',flow:'share',active:!!active},location.origin);}catch(_){}}
function profileHTML(name){return '<div class="profile"><button class="pbtn" id="pbtn">'+pEsc(name)+' <span style="font-size:.65rem">&#9662;</span></button><div class="pmenu" id="pmenu"><a href="/home">Home</a><a href="/dashboard">Dashboard</a><a id="plogout" class="danger">Logout</a></div></div>';}
function wireProfile(){const btn=document.getElementById('pbtn'),menu=document.getElementById('pmenu');if(!btn)return;btn.onclick=(e)=>{e.stopPropagation();menu.classList.toggle('open');};document.addEventListener('click',()=>menu.classList.remove('open'));const lo=document.getElementById('plogout');if(lo)lo.onclick=async()=>{try{await fetch('/api/logout',{method:'POST'});}catch(_){}location.href='/';};}
function makeBrandClickable(){document.querySelectorAll('.brandrow,.wordmark').forEach(el=>{el.style.cursor='pointer';el.addEventListener('click',()=>{location.href='/';});});}
makeBrandClickable();
2026-06-05 17:29:09 +05:30
const codeEl=document.getElementById('code'), statusEl=document.getElementById('status'),
consentBox=document.getElementById('consentBox'), indicator=document.getElementById('indicator');
const setStatus=(t,c='')=>{statusEl.textContent=t;statusEl.className='status '+c;};
document.getElementById('copyBtn').onclick=async()=>{
const code=codeEl.textContent.trim();
if(!/^\d{6}$/.test(code)) return;
try{ await navigator.clipboard.writeText(code); }
catch(e){ const ta=document.createElement('textarea');ta.value=code;document.body.appendChild(ta);ta.select();document.execCommand('copy');ta.remove(); }
const b=document.getElementById('copyBtn'); const old=b.innerHTML;
b.innerHTML='<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#16a34a" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"/></svg>';
2026-06-05 17:29:09 +05:30
setTimeout(()=>{b.innerHTML=old;},1500);
};
let ws,pc,localStream,chatChannel,sessionId;
2026-06-05 17:29:09 +05:30
ws=new WebSocket((location.protocol==='https:'?'wss://':'ws://')+location.host+'/ws');
ws.onopen=()=>ws.send(JSON.stringify({type:'share-create'}));
ws.onmessage=async(e)=>{const m=JSON.parse(e.data);switch(m.type){
case 'share-code': codeEl.textContent=m.code; setStatus('Waiting for your agent to enter the code…'); break;
case 'share-request': onAgentConnected(m); break;
case 'start-stream': sessionId=m.sessionId; await startStreaming(); break;
case 'answer': if(pc) await pc.setRemoteDescription(new RTCSessionDescription(m.sdp)); break;
case 'ice-candidate': if(m.candidate&&pc) await pc.addIceCandidate(new RTCIceCandidate(m.candidate)); break;
case 'recording': recNotice(m.on); if(m.on) startCustTranscription(); else stopCustTranscription(); break;
case 'session-ended': endShareSession('Your support agent ended the session. Tap below for a new code if you still need help.'); break;
2026-06-05 17:29:09 +05:30
case 'error': setStatus(m.message,''); break;
}};
2026-06-10 15:47:02 +05:30
ws.onclose=()=>{ if(document.getElementById('sessionBar')||localStream){ /* keep the live session: media flows independently of signaling. a real end is detected via pc 'failed' or an explicit session-ended message. */ } else { setStatus('Connection closed. Refresh the page to start again.'); } };
2026-06-05 17:29:09 +05:30
function onAgentConnected(m){
const cw=document.querySelector('.codewrap');
if(cw) cw.style.display='none';
setStatus('Your agent has connected. Please respond below.','on');
showConsent(m);
}
function showConsent(m){
const name=(m.technician&&m.technician.trim())?m.technician:'Your support agent';
consentBox.innerHTML='<div class="consent">Your support agent <span class="who">'+esc(name)+'</span> would like to view your screen to help you.'+
'<div class="btns"><button class="grant" id="g">Allow</button><button class="deny" id="d">Not now</button></div></div>';
2026-06-10 16:47:14 +05:30
const allow=async()=>{
document.removeEventListener('keydown',onKey);
setStatus('Opening the screen picker — choose your screen and tap Share / Start.','on');
const ok=await beginCapture();
if(!ok){ consentBox.innerHTML=''; try{ws.send(JSON.stringify({type:'consent',sessionId:m.sessionId,granted:false}));}catch(_){} setStatus('Screen share was cancelled. Refresh this page if you need a new code.'); return; }
consentBox.innerHTML='';
try{ws.send(JSON.stringify({type:'consent',sessionId:m.sessionId,granted:true}));}catch(_){}
};
2026-06-05 17:29:09 +05:30
const onKey=(e)=>{if(e.key==='Enter'){e.preventDefault();allow();}};
document.addEventListener('keydown',onKey);
document.getElementById('g').onclick=allow;
document.getElementById('d').onclick=()=>{consentBox.innerHTML='';document.removeEventListener('keydown',onKey);ws.send(JSON.stringify({type:'consent',sessionId:m.sessionId,granted:false}));setStatus('Connection declined. Refresh this page if you need a new code.');};
}
2026-06-10 16:47:14 +05:30
// Capture the screen (+mic) DIRECTLY from the Allow tap. Mobile browsers reject
// getDisplayMedia unless it is called from a user gesture, so this must not run
// after a server round-trip. getDisplayMedia is called first to keep the gesture.
async function beginCapture(){
try{ localStream=await navigator.mediaDevices.getDisplayMedia({video:{displaySurface:'monitor',frameRate:{ideal:30}},audio:false,monitorTypeSurfaces:'include'}); }
catch(err){ return false; }
// Mic is OFF by default — we do NOT prompt for it here. Asking for the screen and the
// mic at once confused customers and silently cancelled the share. The mic permission
// is requested only when the customer taps Unmute (see the bar's mic button).
2026-06-10 16:47:14 +05:30
try{ ensureIce(); }catch(_){}
return true;
}
2026-06-05 17:29:09 +05:30
async function startStreaming(){
2026-06-10 16:47:14 +05:30
// If the Allow tap already captured the screen (mobile path), reuse it.
if(!localStream){
await ensureIce();
setStatus('In the popup: choose your screen, then tap Share / Start.','on');
// Screen only — mic stays off until the customer taps Unmute (avoids the dual prompt).
2026-06-10 16:47:14 +05:30
try{ localStream=await navigator.mediaDevices.getDisplayMedia({video:{displaySurface:'monitor',frameRate:{ideal:30}},audio:false,monitorTypeSurfaces:'include'}); }
catch(err){ try{ws.send(JSON.stringify({type:'end-session',sessionId,reason:'share-cancelled'}));}catch(e){} setStatus('Screen share was cancelled. Refresh the page to try again.'); return; }
2026-06-10 16:47:14 +05:30
}
2026-06-10 15:47:02 +05:30
await ensureIce();
indicator.classList.add('show'); setStatus('You are now sharing your screen with your agent.','on'); bzcSession(true);
{ const hl=document.getElementById('homeLink'); if(hl) hl.style.display='none'; }
window.onbeforeunload=function(){ if((localStream||document.getElementById('sessionBar'))&&!sessionOver){ return 'Leaving or refreshing this page will end your screen sharing session.'; } };
pc=new RTCPeerConnection(ICE);
2026-06-10 15:47:02 +05:30
buildBar();
2026-06-05 17:29:09 +05:30
localStream.getTracks().forEach(t=>pc.addTrack(t,localStream));
pc.ondatachannel=(ev)=>{ev.channel.onmessage=()=>{};};
pc.ontrack=(ev)=>{ if(ev.track.kind==='audio'){ let a=document.getElementById('remoteAudio'); if(!a){a=document.createElement('audio');a.id='remoteAudio';a.autoplay=true;document.body.appendChild(a);} a.srcObject=ev.streams[0]; } };
2026-06-05 17:29:09 +05:30
pc.onicecandidate=(ev)=>{if(ev.candidate)ws.send(JSON.stringify({type:'ice-candidate',sessionId,candidate:ev.candidate}));};
pc.onconnectionstatechange=()=>{ if(!pc) return; if(pc.connectionState==='connected'){ clearTimeout(window.__connWatch); } if(pc.connectionState==='failed'){ clearTimeout(window.__connWatch); try{ws.send(JSON.stringify({type:'end-session',sessionId,reason:'customer-ended'}));}catch(_){} endShareSession("Couldn't connect on this network — it may be blocking screen sharing. Try a different network (e.g. mobile data / hotspot), then tap below for a new code."); } };
chatChannel=pc.createDataChannel('chat',{ordered:true});
chatChannel.onmessage=(e)=>{try{addChat(JSON.parse(e.data));}catch(_){}};
2026-06-05 17:29:09 +05:30
const offer=await pc.createOffer(); await pc.setLocalDescription(offer);
ws.send(JSON.stringify({type:'offer',sessionId,sdp:pc.localDescription}));
// Watchdog: if we can't establish the peer connection in time, show a clear reason
// instead of a blank screen (covers networks with no usable path even via TURN).
clearTimeout(window.__connWatch);
window.__connWatch=setTimeout(()=>{ if(pc && pc.connectionState!=='connected' && !sessionOver){ try{ws.send(JSON.stringify({type:'end-session',sessionId,reason:'customer-ended'}));}catch(_){} endShareSession("Couldn't connect on this network — it may be blocking screen sharing. Try a different network (e.g. mobile data / hotspot), then tap below for a new code."); } }, 20000);
2026-06-05 17:29:09 +05:30
localStream.getVideoTracks()[0].onended=()=>{ws.send(JSON.stringify({type:'end-session',sessionId,reason:'customer-ended'}));teardown();};
}
const SR = window.SpeechRecognition || window.webkitSpeechRecognition;
let crecog=null, crecogActive=false, sessionOver=false;
function startCustTranscription(){
if(!SR){ return; }
try{
crecog=new SR(); crecog.continuous=true; crecog.interimResults=false; crecog.lang='en-US';
crecog.onresult=(e)=>{ for(let i=e.resultIndex;i<e.results.length;i++){ if(e.results[i].isFinal){ const txt=(e.results[i][0].transcript||'').trim(); if(txt){ try{ws.send(JSON.stringify({type:'transcript',sessionId,role:'customer',name:SHARER_NAME,text:txt,chat:false}));}catch(_){} } } } };
crecog.onerror=()=>{};
crecog.onend=()=>{ if(crecogActive){ try{crecog.start();}catch(_){} } };
crecogActive=true; crecog.start();
}catch(e){}
}
function stopCustTranscription(){ crecogActive=false; if(crecog){ try{crecog.stop();}catch(_){} crecog=null; } }
let recTimerInt=null, recStartTs=0;
function fmtElapsed(ms){const s=Math.max(0,Math.floor(ms/1000));return String(Math.floor(s/60)).padStart(2,'0')+':'+String(s%60).padStart(2,'0');}
function recNotice(on){
if(on&&sessionOver) return;
let n=document.getElementById('recNotice');
if(on){
if(!n){ n=document.createElement('div'); n.id='recNotice';
n.style.cssText='position:fixed;top:14px;left:50%;transform:translateX(-50%);z-index:2147483600;background:#b91c1c;color:#fff;font-weight:600;font-size:.9rem;padding:.5rem 1rem;border-radius:999px;box-shadow:0 6px 18px rgba(0,0,0,.3);display:flex;align-items:center;gap:.5rem';
n.innerHTML='<span style="width:10px;height:10px;border-radius:50%;background:#fff;display:inline-block;animation:recPulse 1.2s infinite"></span> This session is being recorded \u00b7 <span id="recTimeVal">00:00</span>';
document.body.appendChild(n);
if(!document.getElementById('recPulseStyle')){const st=document.createElement('style');st.id='recPulseStyle';st.textContent='@keyframes recPulse{0%,100%{opacity:1}50%{opacity:.25}}';document.head.appendChild(st);}
}
recStartTs=Date.now(); clearInterval(recTimerInt);
const upd=()=>{ const t=document.getElementById('recTimeVal'); if(t) t.textContent=fmtElapsed(Date.now()-recStartTs); };
upd(); recTimerInt=setInterval(upd,1000);
} else { clearInterval(recTimerInt); recTimerInt=null; if(n) n.remove(); }
}
function endShareSession(msgText){
sessionOver=true; window.onbeforeunload=null; bzcSession(false); { const hl=document.getElementById('homeLink'); if(hl) hl.style.display=''; } try{recNotice(false);stopCustTranscription();}catch(_){}
removeSessionUI();
indicator.classList.remove('show');
if(window.__mic){try{window.__mic.getTracks().forEach(t=>t.stop());}catch(_){}window.__mic=null;}
if(localStream){try{localStream.getTracks().forEach(t=>t.stop());}catch(_){}localStream=null;}
if(pc){try{pc.close();}catch(_){}pc=null;}
var card=document.querySelector('.panelside .card');
if(card){ card.innerHTML='<h1 style="color:var(--blue)">Session ended</h1><div class="sub">'+esc(msgText||'The session has ended.')+'</div><button onclick="location.reload()" style="width:100%;margin-top:.4rem">Get a new code</button>'; }
}
function teardown(){sessionOver=true;window.onbeforeunload=null;bzcSession(false);{const hl=document.getElementById('homeLink');if(hl)hl.style.display='';}try{recNotice(false);stopCustTranscription();}catch(_){}indicator.classList.remove('show');removeSessionUI();if(window.__mic){window.__mic.getTracks().forEach(t=>t.stop());window.__mic=null;}if(localStream){localStream.getTracks().forEach(t=>t.stop());localStream=null;}if(pc){pc.close();pc=null;}consentBox.innerHTML='';setStatus('Session ended. Refresh this page to get a new code.');}
let chatOpen=false;
const SVG_MIC='<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="2" width="6" height="11" rx="3"/><path d="M5 10a7 7 0 0 0 14 0"/><line x1="12" y1="19" x2="12" y2="22"/></svg>';
const SVG_MICOFF='<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="3" y1="3" x2="21" y2="21"/><path d="M9 9v4a3 3 0 0 0 5 2.2"/><path d="M5 10a7 7 0 0 0 10.9 5.6"/><line x1="12" y1="19" x2="12" y2="22"/></svg>';
const SVG_CHAT='<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>';
const SVG_END='<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><g transform="rotate(135 12 12)"><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"/></g></svg>';
function _btn(id,svg,label,bg){const b=document.createElement('button');b.id=id;b.title=label;b.setAttribute('aria-label',label);b.innerHTML='<span style="display:inline-flex">'+svg+'</span>';b.style.cssText='display:inline-flex;align-items:center;justify-content:center;width:48px;height:48px;border:none;border-radius:50%;cursor:pointer;color:#fff;background:'+bg+';box-shadow:0 2px 6px rgba(0,0,0,.25);transition:background .15s,transform .08s';b.onmouseenter=()=>b.style.transform='translateY(-2px)';b.onmouseleave=()=>b.style.transform='none';return b;}
function buildBar(){
if(document.getElementById('sessionBar'))return;
const bar=document.createElement('div'); bar.id='sessionBar';
bar.style.cssText='position:fixed;right:18px;bottom:18px;z-index:2147483000;display:flex;gap:10px;align-items:center;background:rgba(15,23,42,.94);padding:8px 12px;border-radius:16px;box-shadow:0 10px 28px rgba(0,0,0,.35)';
const mic=_btn('micBtn',SVG_MICOFF,'Muted','#6b7280');
const chat=_btn('chatBtn',SVG_CHAT,'Chat','#475569');
const end=_btn('endBtn2',SVG_END,'End','#dc2626');
bar.appendChild(mic);bar.appendChild(chat);bar.appendChild(end);
document.body.appendChild(bar);
const setMic=(on)=>{mic.title=on?'Mute':'Unmute';mic.innerHTML='<span style="display:inline-flex">'+(on?SVG_MIC:SVG_MICOFF)+'</span>';mic.style.background=on?'#2563eb':'#6b7280';};
mic.onclick=async()=>{
if(!window.__mic){
// First unmute: NOW request mic permission, add the track, and renegotiate.
let m; try{ m=await navigator.mediaDevices.getUserMedia({audio:true}); }catch(e){ setStatus('Microphone permission was blocked. Allow it in the browser to talk.'); return; }
window.__mic=m; const t=m.getAudioTracks()[0];
try{ localStream.addTrack(t); if(pc){ pc.addTrack(t,localStream); const offer=await pc.createOffer(); await pc.setLocalDescription(offer); ws.send(JSON.stringify({type:'offer',sessionId,sdp:pc.localDescription})); } }catch(_){}
setMic(true); return;
}
const t=window.__mic.getAudioTracks()[0]; if(!t) return; t.enabled=!t.enabled; setMic(t.enabled);
};
chat.onclick=toggleChat;
end.onclick=()=>{ try{ws.send(JSON.stringify({type:'end-session',sessionId,reason:'customer-ended'}));}catch(_){} teardown(); };
buildChatPanel();
document.addEventListener('pointerdown',ensureAudio,{once:true});
document.addEventListener('keydown',ensureAudio,{once:true});
if('Notification' in window && Notification.permission==='default'){ try{Notification.requestPermission();}catch(_){}}
}
function buildChatPanel(){
if(document.getElementById('chatPanel'))return;
const p=document.createElement('div'); p.id='chatPanel';
p.style.cssText='position:fixed;right:18px;bottom:84px;width:300px;max-width:92vw;height:360px;max-height:62vh;z-index:2147483001;background:#fff;border:1px solid #e6e9ef;border-radius:16px;box-shadow:0 14px 34px rgba(0,0,0,.28);display:none;flex-direction:column;overflow:hidden';
p.innerHTML='<div style="background:#1F3B73;color:#fff;padding:.6rem .85rem;font-weight:600;font-size:.92rem;display:flex;justify-content:space-between;align-items:center">Chat <span id="chatClose" style="cursor:pointer;opacity:.85">&#10005;</span></div><div id="chatMsgs" style="flex:1;overflow-y:auto;padding:.6rem;display:flex;flex-direction:column;gap:.4rem;font-size:.85rem"></div><div style="display:flex;gap:.4rem;padding:.5rem;border-top:1px solid #e6e9ef"><input id="chatInput" placeholder="Type a message..." style="flex:1;padding:.5rem;border:1px solid #e6e9ef;border-radius:8px;font-size:.85rem;outline:none"><button id="chatSend" style="background:#FFC708;border:none;border-radius:8px;padding:.5rem .85rem;font-weight:700;cursor:pointer">Send</button></div>';
document.body.appendChild(p);
document.getElementById('chatSend').onclick=sendChat;
document.getElementById('chatClose').onclick=toggleChat;
document.getElementById('chatInput').addEventListener('keydown',e=>{if(e.key==='Enter')sendChat();});
}
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);}
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(_){}}
try{['pointerdown','keydown','touchstart','click'].forEach(ev=>document.addEventListener(ev,ensureAudio,{passive:true}));}catch(_){}
2026-06-10 15:47:02 +05:30
function sendChat(){const i=document.getElementById('chatInput');if(!i)return;const t=i.value.trim();if(!t)return;if(chatChannel&&chatChannel.readyState==='open'){chatChannel.send(JSON.stringify({name:SHARER_NAME,text:t}));}addChat({from:'__self',name:'You',text:t});i.value='';}
function removeSessionUI(){['sessionBar','chatPanel','remoteAudio','muteBtn','msgToast'].forEach(id=>{const e=document.getElementById(id);if(e)e.remove();});}
2026-06-05 17:29:09 +05:30
function esc(s){return String(s).replace(/[&<>"]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]));}
</script>
<script>(function(){var s=document.createElement('style');s.textContent='.ic{display:inline-block;vertical-align:middle}';document.head.appendChild(s);document.querySelectorAll('[data-ic]').forEach(function(e){e.insertAdjacentHTML('afterbegin',window.ic(e.getAttribute('data-ic'),+e.getAttribute('data-sz')||16));});})();</script>
2026-06-05 17:29:09 +05:30
</body>
</html>