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)
}
-
-