diff --git a/internal/httpapi/static/app.js b/internal/httpapi/static/app.js index dd52cfb..e910e1b 100644 --- a/internal/httpapi/static/app.js +++ b/internal/httpapi/static/app.js @@ -37,21 +37,6 @@ function processLinks(scope){ const links = scope.querySelectorAll('a[href]:not( if(!card) return; if(card.title||card.description||card.image||card.html){ const c=document.createElement('div'); c.className='card'; - // Header with favicon/domain + title (clickable), actions on the right - const head=document.createElement('div'); head.className='card-head'; - const left=document.createElement('div'); left.style.display='flex'; left.style.alignItems='center'; left.style.gap='.5rem'; - const u=new URL(a.href); - const fav=document.createElement('img'); fav.src='https://www.google.com/s2/favicons?domain='+u.hostname+'&sz=32'; fav.width=16; fav.height=16; fav.alt=''; fav.loading='lazy'; fav.referrerPolicy='no-referrer'; - const title=document.createElement('a'); title.href=a.href; title.target='_blank'; title.rel='noopener'; title.textContent=card.title||u.hostname; title.style.fontWeight='600'; title.style.textDecoration='none'; - left.appendChild(fav); left.appendChild(title); - const actions=document.createElement('div'); actions.className='card-actions'; - const chevron=document.createElement('button'); chevron.type='button'; chevron.title='Expand/collapse'; chevron.textContent='▾'; chevron.style.padding='0 .4rem'; chevron.style.fontSize='.9rem'; - const btn=document.createElement('button'); btn.type='button'; btn.title='Summarize this link'; btn.textContent='🌝'; btn.style.padding='0 .4rem'; btn.style.fontSize='.9rem'; - const spinner=document.createElement('span'); spinner.textContent=''; spinner.style.marginLeft='.5rem'; - actions.appendChild(btn); actions.appendChild(chevron); actions.appendChild(spinner); - head.appendChild(left); head.appendChild(actions); - c.appendChild(head); - // Body (details) const details=document.createElement('div'); details.className='card-details'; var html=''; if(card.image){ html += '
'; } @@ -61,28 +46,30 @@ function processLinks(scope){ const links = scope.querySelectorAll('a[href]:not( html += ''; 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(); } + details.querySelectorAll('img').forEach(img=> img.addEventListener('load', ()=> pinBottomMulti())); c.appendChild(details); - // Summary block (hidden until requested) - const sum=document.createElement('div'); sum.className='link-summary'; sum.style.whiteSpace='pre-wrap'; sum.style.marginTop='.25rem'; sum.style.display='none'; - c.appendChild(sum); - - const toggleDetails = ()=>{ const collapsed = details.style.display==='none'; details.style.display = collapsed? '' : 'none'; chevron.textContent = collapsed? '▾':'▸'; pinBottomMulti(); }; - head.addEventListener('click', (ev)=>{ if(ev.target===btn || actions.contains(ev.target)) return; toggleDetails(); }); - chevron.onclick=(ev)=>{ ev.stopPropagation(); toggleDetails(); }; - - btn.onclick = async ()=>{ - if(sum.style.display!== 'none' && sum.textContent){ // hide existing - sum.style.display='none'; btn.textContent='🌝'; pinBottomMulti(); return; - } - btn.disabled=true; spinner.textContent='…'; sum.textContent=''; sum.style.display=''; - try{ const data = await api('/api/linksummary',{query:{url:a.href}}); sum.textContent = (data && data.summary) ? data.summary : '(no summary)'; btn.textContent='🌚'; } + const sum=document.createElement('div'); sum.className='link-summary'; sum.style.whiteSpace='pre-wrap'; sum.style.marginTop='.25rem'; sum.style.display='none'; c.appendChild(sum); + // Place actions inline next to the original link + const act=document.createElement('span'); act.style.marginLeft='.35rem'; + const sumBtn=document.createElement('button'); sumBtn.type='button'; sumBtn.title='Summarize'; sumBtn.textContent='🌝'; sumBtn.style.padding='0 .35rem'; sumBtn.style.fontSize='.9rem'; + const chevron=document.createElement('button'); chevron.type='button'; chevron.title='Expand/collapse'; chevron.textContent='▾'; chevron.style.padding='0 .35rem'; chevron.style.fontSize='.9rem'; + const spinner=document.createElement('span'); spinner.textContent=''; spinner.style.marginLeft='.25rem'; + act.appendChild(sumBtn); act.appendChild(chevron); act.appendChild(spinner); + a.insertAdjacentElement('afterend', act); + // Insert card after the link line + a.parentNode.insertBefore(c, act.nextSibling); + const toggle = ()=>{ const hidden = c.style.display==='none'; c.style.display = hidden? '' : 'none'; chevron.textContent = hidden? '▾':'▸'; pinBottomMulti(); }; + chevron.onclick = (ev)=>{ ev.stopPropagation(); toggle(); }; + sumBtn.onclick = async ()=>{ + if(sum.style.display!== 'none' && sum.textContent){ sum.style.display='none'; sumBtn.textContent='🌝'; pinBottomMulti(); return; } + // Ensure card is visible when showing summary + if(c.style.display==='none'){ toggle(); } + sumBtn.disabled=true; spinner.textContent='…'; sum.textContent=''; sum.style.display=''; + try{ const data = await api('/api/linksummary',{query:{url:a.href}}); sum.textContent = (data && data.summary) ? data.summary : '(no summary)'; sumBtn.textContent='🌚'; } catch(e){ sum.textContent = 'error: '+e; } - spinner.textContent=''; btn.disabled=false; pinBottomMulti(); + spinner.textContent=''; sumBtn.disabled=false; pinBottomMulti(); }; - - a.parentNode.insertBefore(c, a.nextSibling); } }).catch(()=>{}); }); } diff --git a/internal/summarizer/openai.go b/internal/summarizer/openai.go index 4aabdab..7e7ab01 100644 --- a/internal/summarizer/openai.go +++ b/internal/summarizer/openai.go @@ -197,7 +197,9 @@ func (o *OpenAI) SummarizeLink(ctx context.Context, rawURL string) (string, erro } text = strings.ReplaceAll(text, "\r", "") text = strings.TrimSpace(text) - if len(text) > 6000 { text = text[:6000] } + if len(text) > 6000 { + text = text[:6000] + } content = text } }() @@ -237,11 +239,17 @@ func (o *OpenAI) SummarizeLink(ctx context.Context, rawURL string) (string, erro }, MaxCompletionTokens: o.maxTokens, } - if !reasoningLike { req.Temperature = 0.2 } + if !reasoningLike { + req.Temperature = 0.2 + } resp, err := client.CreateChatCompletion(ctx, req) - if err != nil { return "", err } - if len(resp.Choices) == 0 { return "", nil } + if err != nil { + return "", err + } + if len(resp.Choices) == 0 { + return "", nil + } return strings.TrimSpace(resp.Choices[0].Message.Content), nil } diff --git a/internal/summarizer/summarizer.go b/internal/summarizer/summarizer.go index 3feea2a..dd416b9 100644 --- a/internal/summarizer/summarizer.go +++ b/internal/summarizer/summarizer.go @@ -1,15 +1,13 @@ package summarizer import ( - "context" - "time" + "context" + "time" - "sojuboy/internal/store" + "sojuboy/internal/store" ) type Summarizer interface { - Summarize(ctx context.Context, channel string, msgs []store.Message, window time.Duration) (string, error) - SummarizeLink(ctx context.Context, rawURL string) (string, error) + Summarize(ctx context.Context, channel string, msgs []store.Message, window time.Duration) (string, error) + SummarizeLink(ctx context.Context, rawURL string) (string, error) } - -