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 : 100 vh ;}
. brandpanel { flex : 1 ; background : linear-gradient ( 160 deg , var ( -- blue ), var ( -- blue - d )); color : #fff ; display : flex ; flex-direction : column ; justify-content : center ; align-items : center ; text-align : center ; padding : 2.5 rem ;}
. mark { width : 88 px ; height : 88 px ; border-radius : 22 px ; background : var ( -- brand ); display : grid ; place-items : center ; font-weight : 800 ; font-size : 2.6 rem ; color : var ( -- blue ); margin-bottom : 1.2 rem ; box-shadow : 0 12 px 30 px rgba ( 0 , 0 , 0 , .25 );}
. wordmark { font-size : 2.2 rem ; font-weight : 800 ; letter-spacing : .01 em ;}
. wordmark span { color : var ( -- brand );}
. tagline { color : #cdd7ee ; margin-top : .6 rem ; font-size : 1 rem ; max-width : 300 px ; line-height : 1.5 ;}
. panelside { flex : 1 ; display : flex ; align-items : center ; justify-content : center ; padding : 2 rem ;}
. card { background : var ( -- card ); border : 1 px solid var ( -- line ); border-radius : 18 px ; padding : 2.4 rem ; max-width : 440 px ; width : 100 % ; text-align : center ; box-shadow : 0 10 px 30 px rgba ( 20 , 30 , 60 , .06 );}
h1 { font-size : 1.45 rem ; margin : .2 rem 0 .4 rem ; color : var ( -- blue );}
. sub { color : var ( -- muted ); font-size : .97 rem ; line-height : 1.5 ; margin-bottom : 1.6 rem ;}
. codewrap { background : #fffdf2 ; border : 2 px dashed var ( -- brand ); border-radius : 14 px ; padding : 1.2 rem ;}
. codelabel { font-size : .78 rem ; letter-spacing : .08 em ; text-transform : uppercase ; color : var ( -- muted ); margin-bottom : .3 rem ;}
2026-06-23 16:15:29 +05:30
. code { font-size : clamp ( 1.6 rem , 9 vw , 2.6 rem ); letter-spacing : clamp ( .1 rem , 2 vw , .4 rem ); padding-left : clamp ( .1 rem , 2 vw , .4 rem ); font-weight : 800 ; color : var ( -- ink ); white-space : nowrap ;}
2026-06-05 17:29:09 +05:30
. status { margin-top : 1.3 rem ; padding : .7 rem 1 rem ; border-radius : 10 px ; background : #f1f5f9 ; color : #475569 ; font-size : .92 rem ;}
. status . on { background : #ecfdf3 ; color : #15803d ;}
. consent { margin-top : 1.3 rem ; border : 1 px solid #c7d6f0 ; background : var ( -- blue - soft ); border-left : 5 px solid var ( -- blue ); border-radius : 12 px ; padding : 1.3 rem ; text-align : left ; color : var ( -- blue - d );}
. consent . who { font-weight : 700 ; color : var ( -- blue );}
. btns { margin-top : 1 rem ; display : flex ; gap : .6 rem ;}
button { flex : 1 ; padding : .8 rem 1 rem ; border : none ; border-radius : 10 px ; font-weight : 700 ; font-size : .98 rem ; cursor : pointer ;}
. grant { background : var ( -- brand ); color : var ( -- ink );} . grant : hover { background : var ( -- brand - d );}
. deny { background : #fff ; color : var ( -- blue ); border : 1 px solid #c7d6f0 ;} . deny : hover { background : #f3f6fc ;}
. foot { color : var ( -- muted ); font-size : .8 rem ; margin-top : 1.4 rem ;}
. indicator { position : fixed ; top : 0 ; left : 0 ; right : 0 ; background : #dc2626 ; color : #fff ; text-align : center ; padding : .5 rem ; font-size : .9 rem ; display : none ; font-weight : 600 ; z-index : 9 ;}
. indicator . show { display : block ;}
2026-06-12 00:40:07 +05:30
/* 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 : 2 rem ; min-height : auto ;} . mark { width : 60 px ; height : 60 px ; border-radius : 16 px ; font-size : 1.8 rem ; margin-bottom : .7 rem ;} . wordmark { font-size : 1.5 rem ;} . tagline { display : none ;} }
2026-06-09 16:47:43 +05:30
. profile { position : relative }
. profile . pbtn { display : flex ; align-items : center ; gap : .4 rem ; background : rgba ( 255 , 255 , 255 , .14 ); color : #fff ; border : 1 px solid #46598c ; border-radius : 10 px ; padding : .45 rem .85 rem ; font-weight : 600 ; font-size : .88 rem ; cursor : pointer }
. profile . pbtn : hover { background : rgba ( 255 , 255 , 255 , .24 )}
. profile . pmenu { position : absolute ; right : 0 ; top : calc ( 100 % + 6 px ); background : #fff ; border : 1 px solid #e6e9ef ; border-radius : 10 px ; box-shadow : 0 10 px 28 px rgba ( 0 , 0 , 0 , .18 ); min-width : 190 px ; overflow : hidden ; z-index : 5000 ; display : none }
. profile . pmenu . open { display : block }
. profile . pmenu a { display : block ; padding : .6 rem .9 rem ; color : #1f2430 ; text-decoration : none ; font-size : .9 rem ; cursor : pointer }
. profile . pmenu a : hover { background : #f1f5f9 }
. profile . pmenu a . danger { color : #b91c1c ; border-top : 1 px solid #eef1f6 }
2026-06-05 17:29:09 +05:30
</ style >
2026-06-23 16:15:29 +05:30
< script src = "/icons.js" ></ script >
2026-06-05 17:29:09 +05:30
</ head >
< body >
2026-06-12 00:40:07 +05:30
< 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 >
2026-06-23 16:15:29 +05:30
< 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 >
2026-06-23 16:15:29 +05:30
< 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 >
2026-06-23 16:15:29 +05:30
< 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 >
2026-06-09 16:47:43 +05:30
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 ( _ ){}
2026-06-10 16:46:03 +05:30
const IS_MOBILE = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini|Mobile/i . test ( navigator . userAgent || '' );
2026-06-23 16:15:29 +05:30
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 ;}
2026-06-09 16:47:43 +05:30
function pEsc ( s ){ return String ( s == null ? '' : s ). replace ( /[&<>"]/g , c =>({ '&' : '&' , '<' : '<' , '>' : '>' , '"' : '"' }[ c ]));}
2026-06-12 00:40:07 +05:30
// 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">▾</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>' ;}
2026-06-09 16:47:43 +05:30
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 ;
2026-06-23 16:15:29 +05:30
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 );
};
2026-06-09 16:47:43 +05:30
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 ;
2026-06-10 16:46:03 +05:30
case 'recording' : recNotice ( m . on ); if ( m . on ) startCustTranscription (); else stopCustTranscription (); break ;
2026-06-09 16:47:43 +05:30
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 ; }
2026-06-23 16:15:29 +05:30
// 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' );
2026-06-23 16:15:29 +05:30
// 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' }); }
2026-06-23 16:15:29 +05:30
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 ();
2026-06-12 00:40:07 +05:30
indicator . classList . add ( 'show' ); setStatus ( 'You are now sharing your screen with your agent.' , 'on' ); bzcSession ( true );
2026-06-10 16:46:03 +05:30
{ 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.' ; } };
2026-06-09 16:47:43 +05:30
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 = ()=>{};};
2026-06-09 16:47:43 +05:30
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 }));};
2026-06-23 16:15:29 +05:30
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." ); } };
2026-06-09 16:47:43 +05:30
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 }));
2026-06-23 16:15:29 +05:30
// Watchdog: clear failure message instead of a blank screen if no path establishes.
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 ();};
}
2026-06-10 16:46:03 +05:30
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 (); }
}
2026-06-09 16:47:43 +05:30
function endShareSession ( msgText ){
2026-06-12 00:40:07 +05:30
sessionOver = true ; window . onbeforeunload = null ; bzcSession ( false ); { const hl = document . getElementById ( 'homeLink' ); if ( hl ) hl . style . display = '' ; } try { recNotice ( false ); stopCustTranscription ();} catch ( _ ){}
2026-06-09 16:47:43 +05:30
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>' ; }
}
2026-06-12 00:40:07 +05:30
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.' );}
2026-06-09 16:47:43 +05:30
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>' ;
2026-06-23 16:15:29 +05:30
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 ;}
2026-06-09 16:47:43 +05:30
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)' ;
2026-06-23 16:15:29 +05:30
const mic = _btn ( 'micBtn' , SVG_MICOFF , 'Muted' , '#6b7280' );
2026-06-09 16:47:43 +05:30
const chat = _btn ( 'chatBtn' , SVG_CHAT , 'Chat' , '#475569' );
2026-06-23 16:15:29 +05:30
const end = _btn ( 'endBtn2' , SVG_END , 'End' , '#dc2626' );
2026-06-09 16:47:43 +05:30
bar . appendChild ( mic ); bar . appendChild ( chat ); bar . appendChild ( end );
document . body . appendChild ( bar );
2026-06-23 16:15:29 +05:30
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 );
};
2026-06-09 16:47:43 +05:30
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">✕</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 = '' ;}
2026-06-09 16:47:43 +05:30
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 =>({ '&' : '&' , '<' : '<' , '>' : '>' , '"' : '"' }[ c ]));}
</ script >
2026-06-23 16:15:29 +05:30
< 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 >