Files
homefeed/frontend/src/lib/components/ArticleListRow.svelte
T
Claude fbb879effe Rearchitect tracked events: a real displayed category, recap is additive
Previously a tracked event withheld all matching items from ever
publishing individually — they sat unclustered until the recap job fired,
which then bundled them into one article and discarded the raw items.
Visitors saw nothing from an event until a recap happened, and never saw
the underlying pieces at all.

Matching items now publish through the exact same pipeline as everything
else (individually or merged with same-story coverage via the normal
clustering pipeline), just tagged with the event's id via a new
publishDirect/publishCluster opts.eventId. An item whose source is
assigned to an event but doesn't match its keyword filter still
publishes normally, just without the tag, instead of being dropped.

The recap is now a periodic *additional* AI-written summary of everything
already published under the event since the last recap (new
synthesizeRecap prompt + publishEventRecap, reading MergedArticle bodies
rather than raw feed items) — never a replacement. Added a new
MergedArticle.isRecap flag so the two are visually distinguishable.

Frontend: a tracked event is now a real displayed category — new
/event/[id] page (mirrors /category/[name]), active events appended to
the site nav, and a "🧵 AI Recap" marker on recap articles.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014c1L8ghNBFjfiH64UMViP8
2026-07-23 23:25:36 +00:00

107 lines
2.3 KiB
Svelte

<script lang="ts">
import type { MergedArticle } from '$lib/types';
import { timeAgo, exactTime, excerpt } from '$lib/format';
import { resolveMediaUrl } from '$lib/config';
import TweetCard from './TweetCard.svelte';
import TelegramCard from './TelegramCard.svelte';
let { article }: { article: MergedArticle } = $props();
const sourceLabel = $derived(
article.sourceCount > 1
? `⇄ ${article.sourceCount} sources`
: (article.sources[0]?.sourceName ?? 'Single source')
);
</script>
{#if article.tweet}
<TweetCard {article} />
{:else if article.telegramMessage}
<TelegramCard {article} />
{:else}
<a class="row" href={`/article/${article.id}`}>
{#if article.heroImage}
<img class="thumb" src={resolveMediaUrl(article.heroImage.url)} alt="" loading="lazy" />
{:else}
<div class="thumb placeholder"></div>
{/if}
<div class="content">
<div class="meta">
{#if article.isRecap}
<span>🧵 AI Recap</span>
<span>&middot;</span>
{/if}
<span>{article.category[0] ?? ''}</span>
<span>&middot;</span>
<span>{sourceLabel}</span>
{#if article.video}
<span>&middot;</span>
<span>▶ Video</span>
{/if}
</div>
<div class="title">{article.title}</div>
<div class="excerpt">{excerpt(article.body)}</div>
<div class="time">{timeAgo(article.publishedAt)} &middot; {exactTime(article.publishedAt)}</div>
</div>
</a>
{/if}
<style>
.row {
display: flex;
gap: 16px;
padding: 16px 0;
border-bottom: 0.5px solid var(--border);
color: inherit;
}
.row:hover {
text-decoration: none;
}
.row:hover .title {
text-decoration: underline;
}
.thumb {
width: 88px;
height: 88px;
flex-shrink: 0;
object-fit: cover;
border-radius: var(--radius);
background: var(--surface-1);
}
.thumb.placeholder {
display: block;
}
.content {
min-width: 0;
flex: 1;
}
.meta {
font-size: 11px;
color: var(--text-accent);
display: flex;
gap: 6px;
margin-bottom: 4px;
}
.title {
font-size: 16px;
font-weight: 500;
line-height: 1.35;
margin-bottom: 4px;
}
.excerpt {
font-size: 13px;
color: var(--text-secondary);
line-height: 1.5;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
margin-bottom: 4px;
}
.time {
font-size: 11px;
color: var(--text-muted);
}
</style>