diff --git a/frontend/src/views/BriefingView.vue b/frontend/src/views/BriefingView.vue index d3c0fce..894a868 100644 --- a/frontend/src/views/BriefingView.vue +++ b/frontend/src/views/BriefingView.vue @@ -18,6 +18,15 @@ import { } from '@/api/client' import type { Message } from '@/types/chat' +interface RssItemMeta { + id: number + title: string + url: string + source: string + snippet: string + published_at: string | null +} + interface MessageMetadata { weather?: { location: string @@ -31,6 +40,7 @@ interface MessageMetadata { forecast: { day: string; condition: string; high: number; low: number }[] } | null rss_item_ids?: number[] + rss_items?: RssItemMeta[] } const chatStore = useChatStore() @@ -154,6 +164,16 @@ function msgMetadata(msg: BriefingMessage): MessageMetadata | null { return (msg.metadata as MessageMetadata | null | undefined) ?? null } +function formatRelativeDate(iso: string | null): string { + if (!iso) return '' + const d = new Date(iso) + const now = new Date() + const diffH = (now.getTime() - d.getTime()) / 3_600_000 + if (diffH < 24) return `${Math.round(diffH)}h ago` + if (diffH < 48) return 'Yesterday' + return d.toLocaleDateString(undefined, { month: 'short', day: 'numeric' }) +} + // Manual trigger const triggering = ref(false) async function triggerNow() { @@ -262,29 +282,43 @@ onMounted(async () => { :message="toMsg(msg)" :is-streaming="false" /> - +
- Story {{ index + 1 }} - - +
+ {{ item.source }} + {{ formatRelativeDate(item.published_at) }} +
+ {{ item.title }} +

{{ item.title }}

+

{{ item.snippet }}

+
+ + +
@@ -483,33 +517,78 @@ onMounted(async () => { .btn-send:disabled { opacity: 0.4; cursor: not-allowed; } .btn-send:hover:not(:disabled) { opacity: 0.9; } -.rss-reactions { +.news-cards { display: flex; flex-direction: column; - gap: 0.35rem; - padding: 0.5rem 0.75rem; + gap: 0.5rem; + margin-top: 0.25rem; margin-bottom: 0.25rem; } -.rss-reaction-row { +.news-card { + background: var(--color-bg-card); + border: 1px solid var(--color-border); + border-radius: 10px; + padding: 0.65rem 0.85rem; display: flex; - align-items: center; - gap: 0.4rem; + flex-direction: column; + gap: 0.25rem; } -.reaction-label { +.news-card-meta { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.news-source { + font-size: 0.72rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.04em; + color: var(--color-primary); +} + +.news-date { + font-size: 0.72rem; + color: var(--color-text-muted); +} + +.news-title { + font-size: 0.88rem; + font-weight: 600; + color: var(--color-text); + line-height: 1.35; + text-decoration: none; + margin: 0; +} + +a.news-title:hover { text-decoration: underline; color: var(--color-primary); } + +.news-snippet { font-size: 0.78rem; color: var(--color-text-muted); - min-width: 4rem; + line-height: 1.45; + margin: 0; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} + +.news-reactions { + display: flex; + gap: 0.3rem; + margin-top: 0.15rem; } .reaction-btn { background: none; border: 1px solid var(--color-border); border-radius: 6px; - padding: 0.15rem 0.4rem; + padding: 0.1rem 0.35rem; cursor: pointer; - font-size: 0.85rem; + font-size: 0.82rem; line-height: 1.4; opacity: 0.55; transition: opacity 0.15s, border-color 0.15s; diff --git a/src/fabledassistant/services/briefing_pipeline.py b/src/fabledassistant/services/briefing_pipeline.py index 0fa741a..ebc3526 100644 --- a/src/fabledassistant/services/briefing_pipeline.py +++ b/src/fabledassistant/services/briefing_pipeline.py @@ -416,6 +416,18 @@ async def run_compilation( max_items=10, ) rss_item_ids = [item["id"] for item in filtered_rss if item.get("id")] + rss_items_meta = [ + { + "id": item["id"], + "title": item.get("title", ""), + "url": item.get("url", ""), + "source": item.get("feed_title", ""), + "snippet": (item.get("content") or "")[:300], + "published_at": item.get("published_at"), + } + for item in filtered_rss + if item.get("id") + ] # Weather staleness gate — returns None if data is >24h old weather_card = parse_weather_card_data(weather_rows[0], temp_unit) if weather_rows else None @@ -449,7 +461,7 @@ async def run_compilation( # ── Post-processing ───────────────────────────────────────────────────────── await upsert_task_snapshots(user_id, all_tasks) - metadata: dict = {"rss_item_ids": rss_item_ids, "weather": weather_card} + metadata: dict = {"rss_item_ids": rss_item_ids, "rss_items": rss_items_meta, "weather": weather_card} if not internal_text and not external_text: logger.warning("Briefing compilation produced no content for user %d slot %s", user_id, slot)