fix(history): prevent duplicate prepend with seen-set and fetch guard; reset seen on channel switch
This commit is contained in:
parent
e9d764817f
commit
95e1f77956
2 changed files with 35 additions and 15 deletions
|
|
@ -1,5 +1,5 @@
|
||||||
// Shared state
|
// Shared state
|
||||||
const st = { tailLoading: false, atBottom: true, current: '#', earliest: null, sse: null, channels: [], twLoaded: false };
|
const st = { tailLoading: false, atBottom: true, current: '#', earliest: null, sse: null, channels: [], twLoaded: false, seen: new Set(), loadingHistory: false };
|
||||||
|
|
||||||
function measureBars(){
|
function measureBars(){
|
||||||
const hdr = document.querySelector('header.nav');
|
const hdr = document.querySelector('header.nav');
|
||||||
|
|
@ -13,6 +13,8 @@ function escapeHtml(s){ return s.replace(/[&<>"]/g, c=>({'&':'&','<':'<',
|
||||||
function linkify(t){ return t.replace(/https?:\/\/\S+/g, function(u){ return '<a href="' + u + '" target="_blank" rel="noopener">' + u + '</a>'; }); }
|
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 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 msgKey(m){ return m.time + '|' + m.author + '|' + m.body; }
|
||||||
|
|
||||||
function snapBottom(){ window.scrollTo(0, document.documentElement.scrollHeight); }
|
function snapBottom(){ window.scrollTo(0, document.documentElement.scrollHeight); }
|
||||||
function pinBottomMulti(){ if(!st.atBottom) return; [0,16,64,200].forEach(d=> setTimeout(()=>requestAnimationFrame(snapBottom), d)); }
|
function pinBottomMulti(){ if(!st.atBottom) return; [0,16,64,200].forEach(d=> setTimeout(()=>requestAnimationFrame(snapBottom), d)); }
|
||||||
|
|
||||||
|
|
@ -28,8 +30,8 @@ async function api(path, params){
|
||||||
|
|
||||||
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 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 appendBatch(arr){ const el=document.getElementById('tail'); const frag=document.createDocumentFragment(); arr.forEach(m=>{ const key=msgKey(m); if(st.seen.has(key)) return; st.seen.add(key); const div=document.createElement('div'); div.className='msg'; div.dataset.key=key; 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 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 key=msgKey(m); if(st.seen.has(key)) return; st.seen.add(key); const div=document.createElement('div'); div.className='msg'; div.dataset.key=key; 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');
|
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 and render card
|
||||||
|
|
@ -77,8 +79,8 @@ function processLinks(scope){ const links = scope.querySelectorAll('a[href]:not(
|
||||||
async function loadChannels(){ try{ const data = await api('/api/channels'); st.channels = data; renderChannels(); if(data.length>0){ selectChannel(data[0]); } } catch(e){} }
|
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); }); }
|
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(); }
|
async function selectChannel(ch){ if(st.sse){ st.sse.close(); st.sse=null; } st.current=ch; st.seen = new Set(); st.loadingHistory = false; 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 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 && !st.loadingHistory){ try{ st.loadingHistory = true; const before = st.earliest; const older = await api('/api/history',{query:{channel:st.current,before:before,limit:50}}); if(older.length){ prependBatch(older); st.earliest = older[0].time; } } catch(e){} finally { st.loadingHistory = false; } } }; 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); } }
|
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); } }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -194,16 +194,20 @@ func (o *OpenAI) SummarizeLink(ctx context.Context, rawURL string) (string, erro
|
||||||
oembed := "https://www.youtube.com/oembed?format=json&url=" + url.QueryEscape(watchURL)
|
oembed := "https://www.youtube.com/oembed?format=json&url=" + url.QueryEscape(watchURL)
|
||||||
req, _ := http.NewRequestWithContext(ctx2, http.MethodGet, oembed, nil)
|
req, _ := http.NewRequestWithContext(ctx2, http.MethodGet, oembed, nil)
|
||||||
if resp, err := http.DefaultClient.Do(req); err == nil {
|
if resp, err := http.DefaultClient.Do(req); err == nil {
|
||||||
func(){
|
func() {
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
|
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
|
||||||
var oem struct{
|
var oem struct {
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Thumb string `json:"thumbnail_url"`
|
Thumb string `json:"thumbnail_url"`
|
||||||
}
|
}
|
||||||
if err := json.NewDecoder(resp.Body).Decode(&oem); err == nil {
|
if err := json.NewDecoder(resp.Body).Decode(&oem); err == nil {
|
||||||
if oem.Title != "" { title = oem.Title }
|
if oem.Title != "" {
|
||||||
if oem.Thumb != "" { img = oem.Thumb }
|
title = oem.Title
|
||||||
|
}
|
||||||
|
if oem.Thumb != "" {
|
||||||
|
img = oem.Thumb
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
@ -226,13 +230,17 @@ func (o *OpenAI) SummarizeLink(ctx context.Context, rawURL string) (string, erro
|
||||||
if art, err := readability.FromReader(strings.NewReader(text), base); err == nil {
|
if art, err := readability.FromReader(strings.NewReader(text), base); err == nil {
|
||||||
if at := strings.TrimSpace(art.TextContent); at != "" {
|
if at := strings.TrimSpace(art.TextContent); at != "" {
|
||||||
text = at
|
text = at
|
||||||
if title == "" && strings.TrimSpace(art.Title) != "" { title = strings.TrimSpace(art.Title) }
|
if title == "" && strings.TrimSpace(art.Title) != "" {
|
||||||
|
title = strings.TrimSpace(art.Title)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
text = strings.ReplaceAll(text, "\r", "")
|
text = strings.ReplaceAll(text, "\r", "")
|
||||||
text = strings.TrimSpace(text)
|
text = strings.TrimSpace(text)
|
||||||
if len(text) > 6000 { text = text[:6000] }
|
if len(text) > 6000 {
|
||||||
|
text = text[:6000]
|
||||||
|
}
|
||||||
content = text
|
content = text
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
@ -247,7 +255,11 @@ func (o *OpenAI) SummarizeLink(ctx context.Context, rawURL string) (string, erro
|
||||||
b.WriteString("URL: ")
|
b.WriteString("URL: ")
|
||||||
b.WriteString(rawURL)
|
b.WriteString(rawURL)
|
||||||
b.WriteString("\n")
|
b.WriteString("\n")
|
||||||
if title != "" { b.WriteString("Title: "); b.WriteString(title); b.WriteString("\n") }
|
if title != "" {
|
||||||
|
b.WriteString("Title: ")
|
||||||
|
b.WriteString(title)
|
||||||
|
b.WriteString("\n")
|
||||||
|
}
|
||||||
b.WriteString("\n")
|
b.WriteString("\n")
|
||||||
if content != "" {
|
if content != "" {
|
||||||
b.WriteString("Extracted content (may be truncated):\n")
|
b.WriteString("Extracted content (may be truncated):\n")
|
||||||
|
|
@ -274,11 +286,17 @@ func (o *OpenAI) SummarizeLink(ctx context.Context, rawURL string) (string, erro
|
||||||
},
|
},
|
||||||
MaxCompletionTokens: o.maxTokens,
|
MaxCompletionTokens: o.maxTokens,
|
||||||
}
|
}
|
||||||
if !reasoningLike { req.Temperature = 0.2 }
|
if !reasoningLike {
|
||||||
|
req.Temperature = 0.2
|
||||||
|
}
|
||||||
|
|
||||||
resp, err := client.CreateChatCompletion(ctx, req)
|
resp, err := client.CreateChatCompletion(ctx, req)
|
||||||
if err != nil { return "", err }
|
if err != nil {
|
||||||
if len(resp.Choices) == 0 { return "", nil }
|
return "", err
|
||||||
|
}
|
||||||
|
if len(resp.Choices) == 0 {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
return strings.TrimSpace(resp.Choices[0].Message.Content), nil
|
return strings.TrimSpace(resp.Choices[0].Message.Content), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue