PageSourceSearch

https://lingovoice.ai/lumen.js?v=2026-08-09r2

js lingovoice.ai collected 2026-10-05 13:06:49 UTC 14,699 bytes, 259 lines download raw bytes

1/* LingoVoice LUMEN — shared interactions */
2/* Sign-up source (APP-12): a visitor from a QR code or campaign lands on a
3   marketing page first, and the source used to be lost by the time they
4   reached /app. Store it for this tab; /app keeps the first one it finds. */
5(function(){
6  try{
7    if(sessionStorage.getItem('lv_attribution'))return;
8    var p=new URLSearchParams(location.search),a={},has=false;
9    ['utm_source','utm_medium','utm_campaign','utm_content','utm_term'].forEach(function(k){if(p.get(k)){a[k]=p.get(k);has=true}});
10    if(document.referrer&&document.referrer.indexOf(location.hostname)===-1){a.referrer=document.referrer;has=true}
11    if(!has)return;
12    a.landing_page=location.pathname+location.search;
13    sessionStorage.setItem('lv_attribution',JSON.stringify(a));
14  }catch(e){}
15})();
16(function(){
17  var reduce = window.matchMedia('(prefers-reduced-motion:reduce)').matches;
18  var sleep = function(ms){return new Promise(function(r){setTimeout(r,ms)})};
19
20  /* ---- live caption stream (only if a room stage exists) ---- */
21  var stage = document.getElementById('stage');
22  if(stage){
23    var latEl = document.getElementById('lat');
24    /* three rotating scenarios: healthcare, council services, business */
25    var scripts = [
26      [
27        {cls:'a',av:'AM',nm:'Amina',lg:'Pashto',oL:'ps',tL:'en',orig:'ډاکټره، زه دری ورځې له سرخوږ څخه کړېږم',origAr:true,trans:"Doctor, I've had a headache for three days.",transAr:false,lat:'1.9s'},
28        {cls:'b',av:'DH',nm:'Dr Hughes',lg:'English',oL:'en',tL:'ps',orig:'I understand. Any fever, nausea, or medication?',origAr:false,trans:'زه پوهیږم. آیا تبه، زړه بدوالی یا کوم درمل شته؟',transAr:true,lat:'1.7s'},
29        {cls:'a',av:'AM',nm:'Amina',lg:'Pashto',oL:'ps',tL:'en',orig:'نه تبه، خو زړه مې بدیږي',origAr:true,trans:'No fever, but I feel nauseous.',transAr:false,lat:'1.8s'}
30      ],
31      [
32        {cls:'a',av:'OM',nm:'Oksana',lg:'Ukrainian',oL:'uk',tL:'en',orig:'Доброго дня, я дзвоню щодо своєї заявки на житло',origAr:false,trans:"Hello, I'm calling about my housing application.",transAr:false,lat:'1.8s'},
33        {cls:'b',av:'SC',nm:'Sam Carter',lg:'English',oL:'en',tL:'uk',orig:'Of course. Could you give me your reference number?',origAr:false,trans:'Звичайно. Назвіть, будь ласка, номер вашої заявки.',transAr:false,lat:'1.6s'},
34        {cls:'a',av:'OM',nm:'Oksana',lg:'Ukrainian',oL:'uk',tL:'en',orig:'Так, номер заявки 2284',origAr:false,trans:'Yes, the reference number is 2284.',transAr:false,lat:'1.7s'}
35      ],
36      [
37        {cls:'a',av:'LW',nm:'Li Wei',lg:'Mandarin',oL:'zh',tL:'en',orig:'第一批货周五之前可以发出',origAr:false,trans:'The first shipment can go out by Friday.',transAr:false,lat:'1.9s'},
38        {cls:'b',av:'ER',nm:'Emma Reid',lg:'English',oL:'en',tL:'zh',orig:'Perfect. And what are the payment terms?',origAr:false,trans:'太好了。付款条件是怎样的?',transAr:false,lat:'1.7s'},
39        {cls:'a',av:'LW',nm:'Li Wei',lg:'Mandarin',oL:'zh',tL:'en',orig:'百分之三十预付,其余货到后结清',origAr:false,trans:'Thirty percent upfront, the balance on delivery.',transAr:false,lat:'2.0s'}
40      ]
41    ];
42    var turns = scripts[0];
43    /* Plain-language pair label per scenario so the room header always matches
44       the conversation on screen (the old static "PS -> EN" never did). */
45    var pairs = ['Pashto \u21C4 English', 'Ukrainian \u21C4 English', 'Mandarin \u21C4 English'];
46    var pairEl = document.getElementById('hd-pair');
47    var setPair = function(i){ if(pairEl) pairEl.textContent = pairs[i % pairs.length]; };
48    setPair(0);
49    var mk = function(tag,cls,text){ var n=document.createElement(tag); if(cls)n.className=cls; if(text!=null)n.textContent=text; return n; };
50    var turnNode = function(t){
51      var el=mk('div','turn '+t.cls);
52      var spk=mk('div','spk');
53      spk.appendChild(mk('span','av '+t.cls,t.av));
54      spk.appendChild(mk('span','nm',t.nm));
55      spk.appendChild(mk('span','lg',t.lg));
56      var sp=mk('span','speaking'); sp.appendChild(mk('span','pulse live')); sp.appendChild(document.createTextNode('speaking'));
57      spk.appendChild(sp);
58      el.appendChild(spk);
59      var oNode=mk('div','orig'+(t.origAr?' ar':'')); if(t.oL && t.oL!=='en') oNode.lang=t.oL;
60      el.appendChild(oNode);
61      var tr=mk('div','translating'); tr.style.display='none'; tr.appendChild(mk('b')); tr.appendChild(mk('b')); tr.appendChild(mk('b'));
62      el.appendChild(tr);
63      var tNode=mk('div','trans'+(t.transAr?' ar':'')); if(t.tL && t.tL!=='en') tNode.lang=t.tL;
64      el.appendChild(tNode);
65      return el;
66    };
67    var typeInto = async function(node,text,withCaret){
68      node.textContent='';
69      var caret=document.createElement('span'); caret.className='caret';
70      if(withCaret) node.appendChild(caret);
71      for(var i=0;i<text.length;i++){
72        node.insertBefore(document.createTextNode(text[i]), withCaret?caret:null);
73        await sleep(14+Math.random()*24);
74      }
75      if(withCaret) caret.remove();
76    };
77    /* keep the room looking mid-conversation: a faded prior turn instead of an empty void */
78    var toPrior=function(node){
79      if(!node) return;
80      node.classList.add('prior');
81      var sp=node.querySelector('.speaking'); if(sp) sp.remove();
82      var tr=node.querySelector('.translating'); if(tr) tr.remove();
83    };
84    var seed=turnNode(turns[turns.length-1]);
85    seed.querySelector('.orig').textContent=turns[turns.length-1].orig;
86    seed.querySelector('.trans').textContent=turns[turns.length-1].trans;
87    seed.classList.add('show');
88    toPrior(seed);
89    stage.appendChild(seed);
90    /* the live hero demo (hero-demo.js) can take this stage over on first tap */
91    var stageStopped=false;
92    window.lvStopStageLoop=function(){ stageStopped=true; };
93    (async function run(){
94      var loopN=0;
95      while(true){
96        if(stageStopped) return;
97        var idx=loopN++%scripts.length;
98        turns=scripts[idx];
99        /* Each scenario is its own fresh, coherent conversation. Fade the old
100           one out, clear it (never leave a different room's message behind),
101           then set the header to this scene's languages. */
102        if(!reduce && stage.firstChild){ stage.style.transition='opacity .35s ease'; stage.style.opacity='0'; await sleep(360); }
103        if(stageStopped) return;
104        while(stage.firstChild){ stage.removeChild(stage.firstChild); }
105        setPair(idx);
106        stage.style.opacity='1';
107        for(var k=0;k<turns.length;k++){
108          if(stageStopped) return;
109          var t=turns[k], node=turnNode(t);
110          if(stageStopped) return;
111          stage.appendChild(node);
112          var origEl=node.querySelector('.orig'), trEl=node.querySelector('.translating'), transEl=node.querySelector('.trans');
113          /* Opening line of each scenario appears INSTANTLY (already-said context)
114             so the room is never a single lonely bubble with a big void above it. */
115          if(k===0 && !reduce){
116            /* place the opening line instantly and fall straight through to the
117               next turn (no await) so the column is never a single lonely bubble */
118            (function(n){requestAnimationFrame(function(){n.classList.add('show')})})(node);
119            origEl.textContent=t.orig; transEl.textContent=t.trans;
120            if(latEl)latEl.textContent=t.lat;
121            continue;
122          }
123          (function(n){requestAnimationFrame(function(){n.classList.add('show','talking')}
123)})(node);
124          if(reduce){ origEl.textContent=t.orig; transEl.textContent=t.trans; if(latEl)latEl.textContent=t.lat; await sleep(1600); continue; }
125          await typeInto(origEl,t.orig,true);
126          if(stageStopped) return;
127          await sleep(240);
128          trEl.style.display='inline-flex'; await sleep(800); trEl.style.display='none';
129          if(latEl)latEl.textContent=t.lat;
130          if(stageStopped) return;
131          await typeInto(transEl,t.trans,true);
132          node.classList.remove('talking');
133          /* keep the immediately-prior turn (same conversation) faded for context */
134          while(stage.children.length>2){ stage.removeChild(stage.firstChild); }
135          toPrior(stage.firstChild);
136          await sleep(1500);
137        }
138        await sleep(1100);
139      }
140    })();
141  }
142
143  /* ---- nav scroll shadow ---- */
144  var nav=document.getElementById('nav');
145  if(nav){ window.addEventListener('scroll',function(){ nav.classList.toggle('scrolled', window.scrollY>8) }); }
146
147  /* ---- mobile nav: hamburger + sheet (built from existing links) ---- */
148  (function(){
149    if(!nav) return;
150    var navIn=nav.querySelector('.nav-in'), links=nav.querySelector('.nav-links'), cta=nav.querySelector('.nav-cta');
151    if(!navIn||!links) return;
152    var mk=function(tag,cls){var n=document.createElement(tag); if(cls)n.className=cls; return n;};
153
154    var burger=mk('button','nav-burger');
155    burger.type='button';
156    burger.setAttribute('aria-label','Open menu');
157    burger.setAttribute('aria-expanded','false');
158    burger.setAttribute('aria-controls','navSheet');
159    for(var i=0;i<3;i++){ burger.appendChild(mk('span')); }
160
161    var sheet=mk('div','nav-sheet'); sheet.id='navSheet';
162    links.querySelectorAll('a').forEach(function(a){ sheet.appendChild(a.cloneNode(true)); });
163    var row=mk('div','nav-sheet-cta');
164    var signin=cta&&cta.querySelector('.signin'); if(signin){ row.appendChild(signin.cloneNode(true)); }
165    var start=cta&&cta.querySelector('.btn'); if(start){ var s2=start.cloneNode(true); s2.classList.remove('btn--sm'); row.appendChild(s2); }
166    if(row.children.length) sheet.appendChild(row);
167
168    var scrim=mk('div','nav-scrim');
169
170    var setOpen=function(open){
171      nav.classList.toggle('open',open);
172      document.body.classList.toggle('nav-open',open);
173      burger.setAttribute('aria-expanded',open?'true':'false');
174      burger.setAttribute('aria-label',open?'Close menu':'Open menu');
175    };
176    burger.addEventListener('click',function(){ setOpen(!nav.classList.contains('open')); });
177    scrim.addEventListener('click',function(){ setOpen(false); });
178    sheet.addEventListener('click',function(e){ if(e.target.closest('a')) setOpen(false); });
179    document.addEventListener('keydown',function(e){ if(e.key==='Escape') setOpen(false); });
180    window.addEventListener('resize',function(){ if(window.innerWidth>940) setOpen(false); });
181
182    if(cta){ cta.appendChild(burger); } else { navIn.appendChild(burger); }
183    nav.appendChild(scrim);
184    nav.appendChild(sheet);
185  })();
186
187  /* ---- spotlight caption strip: rotate language pairs (gated to viewport) ---- */
188  document.querySelectorAll('.spot-cap[data-rotate]').forEach(function(cap){
189    var pairs=[
190      {ln:'Dzień dobry, mam umówioną wizytę o dziewiątej', big:'Good morning, I have an appointment at nine', ar:false},
191      {ln:'Мені потрібен переклад цього документа', big:'I need this document translated', ar:false},
192      {ln:'我想咨询一下签证的事情', big:'I would like to ask about my visa', ar:false}
193    ];
194    if(reduce) return;
195    var live=cap.querySelector('.cap-live')||cap;
196    var i=0, lnEl=live.querySelector('.ln'), bigEl=live.querySelector('.big'), cur=bigEl.querySelector('.cur');
197    var visible=false, timer=null;
198    var show=function(p){
199      lnEl.textContent=p.ln;
200      lnEl.classList.toggle('ar',p.ar);
201      bigEl.textContent=p.big;
202      if(cur) bigEl.appendChild(cur);
203    };
204    var tick=function(){
205      cap.classList.add('swap');
206      setTimeout(function(){ i=(i+1)%pairs.length; show(pairs[i]); cap.classList.remove('swap'); },460);
207    };
208    var io2=new IntersectionObserver(function(es){ es.forEach(function(e){
209      visible=e.isIntersecting;
210      if(visible && !timer){ timer=setInterval(tick,4600); }
211      if(!visible && timer){ clearInterval(timer); timer=null; }
212    }) },{threshold:.3});
213    io2.observe(cap);
214  });
215
216  /* ---- reveals + how-it-works thread ---- */
217  var io=new IntersectionObserver(function(es){ es.forEach(function(e){ if(e.isIntersecting){ e.target.classList.add('in');
218    var th=e.target.querySelector&&e.target.querySelector('#thread'); if(th){ th.style.width='66.666%'; }
219    io.unobserve(e.target); } }) }, {threshold:.16});
220  document.querySelectorAll('.reveal, .darkband .scn').forEach(function(n){ io.observe(n) });
221  setTimeout(function(){ document.querySelectorAll('.reveal:not(.in), .darkband .scn:not(.in)').forEach(function(n){ n.classList.add('in') }) }, 2400);
222
223  /* ---- generic form submit (contact etc.) -> data-endpoint as JSON ---- */
224  document.querySelectorAll('form[data-endpoint]').forEach(function(form){
225    form.addEventListener('submit', async function(e){
226      e.preventDefault();
227      var btn=form.querySelector('button[type="submit"]');
228      var status=form.querySelector('.form-status');
229      var orig=btn?btn.textContent:'';
230      if(btn){ btn.disabled=true; btn.textContent='Sending'; }
231      if(status){ status.className='form-status'; }
232      try{
233        var data=Object.fromEntries(new FormData(form));
234        var res=await fetch(form.getAttribute('data-endpoint'),{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(data)});
235        var json={}; try{ json=await res.json(); }catch(_){}
236        if(res.ok){
237          if(status){ status.className='form-status success'; status.textContent="Thanks, message received. We will reply within one working day."; }
238          form.reset();
239        } else {
240          if(status){ status.className='form-status error'; status.textContent=(json&&json.error)||'Something went wrong. Please try again.'; }
241        }
242      }catch(err){
243        if(status){ status.className='form-status error'; status.textContent='Network error. Please try again.'; }
244      }
245      if(btn){ btn.disabled=false; btn.textContent=orig; }
246    });
247  });
248
249  /* ---- faq accordion ---- */
250  document.querySelectorAll('.qa button').forEach(function(b){
251    b.setAttribute('aria-expanded','false');
252    b.addEventListener('click',function(){
253      var qa=b.parentElement, open=qa.classList.contains('open');
254      document.querySelectorAll('.qa').forEach(function(x){ x.classList.remove('open'); x.querySelector('.ans').style.maxHeight=null;
255        var xb=x.querySelector('button'); if(xb) xb.setAttribute('aria-expanded','false'); });
256      if(!open){ qa.classList.add('open'); var ans=qa.querySelector('.ans'); ans.style.maxHeight=ans.scrollHeight+'px'; b.setAttribute('aria-expanded','true'); }
257    });
258  });
259})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.