Share pages: lazy, cached, our-own-words article summaries
The /a/<id> page now carries an original short summary so it stands on its own,
without republishing the publisher's article:
- summarize.py: transient SSRF-guarded fetch of the article text → local LLM
writes a 2-4 sentence ORIGINAL summary (our words). Cached in article_summaries
forever; we store only our summary, never the body. Generated lazily (only for
shared/viewed articles), de-duped so concurrent hits don't double-generate.
- /a serves cached-or-pending; when pending it shows a calm "summary on its way,
read at {source}" note and self-polls /api/summary/<id>, swapping the summary
in the moment it's ready (never blocks the page on the batch-tier LLM).
- Share menu warms generation on open so recipients usually get the rich version.
- Container reaches the arbiter at arbiter:8080 over caddy_web (LLM env added to
the API container). 124 tests pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+37
-6
@@ -17,7 +17,7 @@ def _tag(name: str, content: str | None, attr: str = "property") -> str:
|
||||
return f'<meta {attr}="{escape(name)}" content="{escape(content)}">'
|
||||
|
||||
|
||||
def render_share_page(article: dict, base_url: str) -> str:
|
||||
def render_share_page(article: dict, base_url: str, summary: str | None = None) -> str:
|
||||
aid = article["id"]
|
||||
title = (article.get("title") or "Upbeat Bytes").strip()
|
||||
why = (article.get("reason_text") or article.get("description")
|
||||
@@ -49,6 +49,31 @@ def render_share_page(article: dict, base_url: str) -> str:
|
||||
if image else ""
|
||||
)
|
||||
|
||||
source_short = source.split(" ")[0] if source else "the source"
|
||||
if summary:
|
||||
summary_block = f'<p class="summary" id="summary">{escape(summary)}</p>'
|
||||
poll = ""
|
||||
else:
|
||||
pending = (
|
||||
f"✦ We’re putting together a quick summary — in the meantime, "
|
||||
f"read the full story at {escape(source)} below."
|
||||
)
|
||||
summary_block = f'<p class="summary pending" id="summary">{pending}</p>'
|
||||
# Quietly poll; swap the real summary in the moment it's cached.
|
||||
poll = f"""<script>
|
||||
(function(){{
|
||||
var id={aid}, box=document.getElementById('summary'), n=0;
|
||||
function go(){{
|
||||
n++;
|
||||
fetch('/api/summary/'+id).then(function(r){{return r.json();}}).then(function(d){{
|
||||
if(d&&d.status==='ready'&&d.summary){{ box.textContent=d.summary; box.className='summary'; }}
|
||||
else if(n<12){{ setTimeout(go,2500); }}
|
||||
}}).catch(function(){{ if(n<12) setTimeout(go,3000); }});
|
||||
}}
|
||||
go();
|
||||
}})();
|
||||
</script>"""
|
||||
|
||||
return f"""<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
@@ -76,8 +101,12 @@ def render_share_page(article: dict, base_url: str) -> str:
|
||||
.body {{ padding:24px 26px 28px; }}
|
||||
.src {{ color:var(--muted); font-size:.82rem; text-transform:uppercase; letter-spacing:.08em; }}
|
||||
h1 {{ font-family:"Iowan Old Style",Palatino,Georgia,serif; font-weight:600;
|
||||
font-size:1.7rem; line-height:1.2; margin:6px 0 12px; }}
|
||||
.why {{ color:#3b4754; font-style:italic; border-left:2px solid #e0eef3; padding-left:12px; margin:0 0 22px; }}
|
||||
font-size:1.7rem; line-height:1.2; margin:6px 0 14px; }}
|
||||
.summary {{ font-size:1.05rem; color:var(--ink); margin:0 0 18px; }}
|
||||
.summary.pending {{ color:var(--muted); font-style:italic; }}
|
||||
.why {{ color:#3b4754; font-size:.92rem; margin:0 0 22px; }}
|
||||
.why .lbl {{ display:block; text-transform:uppercase; letter-spacing:.08em; font-size:.7rem;
|
||||
color:var(--muted); margin-bottom:3px; }}
|
||||
.actions {{ display:flex; flex-wrap:wrap; gap:12px; align-items:center; }}
|
||||
.primary {{ background:var(--accent); color:#fff; text-decoration:none; font-weight:600;
|
||||
padding:12px 20px; border-radius:999px; display:inline-block; }}
|
||||
@@ -95,15 +124,17 @@ def render_share_page(article: dict, base_url: str) -> str:
|
||||
<div class="body">
|
||||
<div class="src">{escape(source)}</div>
|
||||
<h1>{escape(title)}</h1>
|
||||
<p class="why">{escape(why)}</p>
|
||||
{summary_block}
|
||||
<p class="why"><span class="lbl">Why it's here</span>{escape(why)}</p>
|
||||
<div class="actions">
|
||||
<a class="primary" href="{escape(src_url)}" rel="noopener" data-src-click>Read the full story at {escape(source)}</a>
|
||||
<a class="primary" href="{escape(src_url)}" rel="noopener" data-src-click>Read the full story at {escape(source_short)}</a>
|
||||
<a class="secondary" href="/">Explore more on Upbeat Bytes →</a>
|
||||
</div>
|
||||
<p class="note">Upbeat Bytes links to the original publisher — it doesn't host the article.</p>
|
||||
<p class="note">Upbeat Bytes summarizes in its own words and links to the original publisher — it doesn't host the article.</p>
|
||||
</div>
|
||||
</article>
|
||||
</main>
|
||||
{poll}
|
||||
</body>
|
||||
</html>"""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user