sojuboy/internal/httpapi/static/app.js

100 lines
9.9 KiB
JavaScript
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.

// Shared state
const st = { tailLoading: false, atBottom: true, current: '#', earliest: null, sse: null, channels: [], twLoaded: false };
function measureBars(){
const hdr = document.querySelector('header.nav');
const ftr = document.querySelector('footer');
if(hdr){ document.documentElement.style.setProperty('--headerH', hdr.getBoundingClientRect().height+'px'); }
if(ftr){ document.documentElement.style.setProperty('--footerH', ftr.getBoundingClientRect().height+'px'); }
}
function colorFor(nick){ let h=0; for(let i=0;i<nick.length;i++){ h=(h*31+nick.charCodeAt(i))>>>0 } return 'hsl('+(h%360)+',60%,'+(window.matchMedia('(prefers-color-scheme: dark)').matches? '70%':'35%')+')'; }
function escapeHtml(s){ return s.replace(/[&<>"]/g, c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c])); }
function linkify(t){ return t.replace(/https?:\/\/\S+/g, function(u){ return '<a href="' + u + '" target="_blank" rel="noopener">' + u + '</a>'; }); }
function lineHTML(m){ const ts = '<span class=ts>[' + m.time + ']</span>'; const nick = '<b style="color:' + colorFor(m.author) + '">' + m.author + '</b>'; const body = escapeHtml(m.body); return ts + ' ' + nick + ': ' + linkify(body); }
function snapBottom(){ window.scrollTo(0, document.documentElement.scrollHeight); }
function pinBottomMulti(){ if(!st.atBottom) return; [0,16,64,200].forEach(d=> setTimeout(()=>requestAnimationFrame(snapBottom), d)); }
async function api(path, params){
const url = new URL(path, window.location.origin);
if(params && params.query){ Object.entries(params.query).forEach(([k,v])=>url.searchParams.set(k,v)); }
const res = await fetch(url);
if(!res.ok) throw new Error('HTTP '+res.status);
const ct = res.headers.get('content-type')||'';
if(ct.includes('application/json')) return res.json();
return res.text();
}
function ensureTwitterWidgets(){ if(st.twLoaded) return; st.twLoaded = true; const s=document.createElement('script'); s.async=true; s.src='https://platform.twitter.com/widgets.js'; document.head.appendChild(s); }
function appendBatch(arr){ const el=document.getElementById('tail'); const frag=document.createDocumentFragment(); arr.forEach(m=>{ const div=document.createElement('div'); div.className='msg'; div.innerHTML=lineHTML(m); frag.appendChild(div); processLinks(div); }); el.appendChild(frag); pinBottomMulti(); }
function prependBatch(arr){ const el=document.getElementById('tail'); const oldTop=el.firstChild; const beforeTop = oldTop ? oldTop.getBoundingClientRect().top : 0; const frag=document.createDocumentFragment(); arr.forEach(m=>{ const div=document.createElement('div'); div.className='msg'; div.innerHTML=lineHTML(m); frag.appendChild(div); processLinks(div); }); el.insertBefore(frag, el.firstChild); if(oldTop){ const afterTop = oldTop.getBoundingClientRect().top; const delta = afterTop - beforeTop; window.scrollBy(0, delta); } }
function processLinks(scope){ const links = scope.querySelectorAll('a[href]:not([data-card])'); links.forEach(a=>{ a.setAttribute('data-card','1');
// Fetch and render card
fetch('/api/linkcard?url='+encodeURIComponent(a.href)).then(r=>r.json()).then(card=>{
if(!card) return;
if(card.title||card.description||card.image||card.html){
const c=document.createElement('div'); c.className='card';
const details=document.createElement('div'); details.className='card-details';
var html='';
if(card.image){ html += '<div><img src="'+card.image+'" alt="" style="max-width:160px;max-height:120px;object-fit:cover;border-radius:.25rem"></div>'; }
html += '<div style="flex:1;margin-left:.5rem">';
if(card.title){ html += '<div style="font-weight:600">'+escapeHtml(card.title)+'</div>'; }
if(card.description){ html += '<div style="opacity:.8">'+escapeHtml(card.description)+'</div>'; }
html += '</div>';
const row = document.createElement('div'); row.style.display='flex'; row.style.alignItems='flex-start'; row.style.gap='.5rem'; row.innerHTML = html;
details.appendChild(row);
details.querySelectorAll('img').forEach(img=> img.addEventListener('load', ()=> pinBottomMulti()));
if(card.html){ const wrap=document.createElement('div'); wrap.innerHTML=card.html; details.appendChild(wrap); ensureTwitterWidgets(); }
c.appendChild(details);
// Controls
const ctrl = document.createElement('div'); ctrl.style.marginTop='.25rem';
const btn = document.createElement('button'); btn.type='button'; btn.title='Summarize this link'; btn.textContent='\u25B6'; btn.style.padding='0 .4rem'; btn.style.fontSize='.9rem';
const spinner = document.createElement('span'); spinner.textContent=''; spinner.style.marginLeft='.5rem';
const sum = document.createElement('div'); sum.className='link-summary'; sum.style.whiteSpace='pre-wrap'; sum.style.marginTop='.25rem';
const toggleCard = document.createElement('button'); toggleCard.type='button'; toggleCard.title='Collapse/expand card'; toggleCard.textContent=''; toggleCard.style.padding='0 .4rem'; toggleCard.style.fontSize='.9rem'; toggleCard.style.marginLeft='.5rem';
const toggleSum = document.createElement('button'); toggleSum.type='button'; toggleSum.title='Collapse/expand summary'; toggleSum.textContent=''; toggleSum.style.padding='0 .4rem'; toggleSum.style.fontSize='.9rem'; toggleSum.style.marginLeft='.25rem';
toggleCard.onclick = ()=>{ const hidden = details.style.display==='none'; details.style.display = hidden? '' : 'none'; toggleCard.textContent = hidden? '' : '+'; pinBottomMulti(); };
toggleSum.onclick = ()=>{ const hidden = sum.style.display==='none'; sum.style.display = hidden? '' : 'none'; toggleSum.textContent = hidden? '' : '+'; pinBottomMulti(); };
btn.onclick = async ()=>{
btn.disabled=true; spinner.textContent='…'; sum.textContent=''; sum.style.display=''; toggleSum.textContent='';
try{ const data = await api('/api/linksummary',{query:{url:a.href}}); sum.textContent = (data && data.summary) ? data.summary : '(no summary)'; }
catch(e){ sum.textContent = 'error: '+e; }
spinner.textContent=''; btn.disabled=false; pinBottomMulti();
};
ctrl.appendChild(btn); ctrl.appendChild(spinner); ctrl.appendChild(toggleCard); ctrl.appendChild(toggleSum);
c.appendChild(ctrl);
c.appendChild(sum);
a.parentNode.insertBefore(c, a.nextSibling);
}
}).catch(()=>{});
}); }
async function loadChannels(){ try{ const data = await api('/api/channels'); st.channels = data; renderChannels(); if(data.length>0){ selectChannel(data[0]); } } catch(e){} }
function renderChannels(){ const list=document.getElementById('nav-chans') || document.getElementById('brand-chans')?.querySelector('ul'); if(!list) return; list.innerHTML=''; st.channels.forEach(c=>{ const li=document.createElement('li'); const a=document.createElement('a'); a.href='#'; a.textContent=c + (c===st.current? ' ✓':''); a.onclick=(ev)=>{ev.preventDefault(); selectChannel(c);}; li.appendChild(a); list.appendChild(li); }); }
async function selectChannel(ch){ if(st.sse){ st.sse.close(); st.sse=null; } st.current=ch; renderChannels(); const el=document.getElementById('tail'); if(!el) return; el.textContent=''; const data = await api('/api/tail',{query:{channel:ch,limit:50}}); appendBatch(data); requestAnimationFrame(()=>{ snapBottom(); st.atBottom=true; }); st.earliest = data.length? data[0].time : null; startStream(); initScrollHandlers(); }
function initScrollHandlers(){ const el=document.getElementById('tail'); if(!el) return; const onScroll = async ()=>{ const nearBottom = (window.innerHeight + window.pageYOffset + 16) >= document.documentElement.scrollHeight; st.atBottom = nearBottom; const nearTop = window.pageYOffset <= 4; if(nearTop && st.earliest){ try{ const older = await api('/api/history',{query:{channel:st.current,before:st.earliest,limit:50}}); if(older.length){ prependBatch(older); st.earliest = older[0].time; } } catch(e){} } }; window.removeEventListener('scroll', st._scrollHandler || (()=>{})); st._scrollHandler = onScroll; window.addEventListener('scroll', onScroll, {passive:true}); }
function startStream(){ const el=document.getElementById('tail'); if(!el) return; const url=new URL('/api/stream', window.location.origin); url.searchParams.set('channel', st.current); const es=new EventSource(url); st.sse=es; es.onmessage=(ev)=>{ try{ const m=JSON.parse(ev.data); appendBatch([m]); }catch(e){} }; es.onerror=()=>{ es.close(); st.sse=null; setTimeout(startStream, 3000); } }
async function summarize(){ const ch=document.getElementById('channel'); const win=document.getElementById('window'); const push=document.getElementById('push'); const btn=document.getElementById('btn'); const out=document.getElementById('out'); if(!ch||!win||!btn||!out) return; btn.disabled=true; out.textContent=''; try{ const data = await api('/api/trigger',{query:{channel:ch.value,window:win.value||'6h',push:push && push.checked? '1':'0'}}); if(typeof data === 'string'){ out.textContent = data; } else { out.textContent = (data.summary||''); } } catch(e){ out.textContent = 'error: '+e; } btn.disabled=false; }
window.addEventListener('DOMContentLoaded', ()=>{
if('scrollRestoration' in history){ history.scrollRestoration = 'manual'; }
measureBars();
loadChannels();
// Open brand dropdown on hover (desktop) for the dashboard
const brandDetails = document.getElementById('brand-chans');
if(brandDetails){
brandDetails.addEventListener('mouseenter', ()=>{ brandDetails.setAttribute('open',''); });
brandDetails.addEventListener('mouseleave', ()=>{ brandDetails.removeAttribute('open'); });
}
if(document.getElementById('channel')){
fetch('/api/channels').then(r=>r.json()).then(arr=>{ const sel=document.getElementById('channel'); arr.forEach(c=>{ const o=document.createElement('option'); o.value=c; o.textContent=c; sel.appendChild(o); }); }).catch(()=>{});
}
});
window.addEventListener('load', measureBars);
window.addEventListener('resize', ()=>{ measureBars(); if(st.atBottom) pinBottomMulti(); });