/** * Telegram Digest Feed — React artifact template (ultra-compact reader) * * Usage: * 1. Agent runs: bash scripts/digest_json.sh --period today * 2. Script outputs JSON: { posts: [...], channels: {...} } * 3. Agent reads this template, replaces __DIGEST_DATA__ with the JSON * 4. Renders as React artifact */ import { useState, useMemo } from "react"; // __DIGEST_DATA__ — agent replaces this line with JSON from digest_json.sh const _data: { posts: Post[]; channels: Record } = __DIGEST_DATA__; const POSTS_DATA = _data.posts; const CHANNELS = _data.channels; interface Post { id: string; channel: string; date: string; views: string; reactions: string; fwd_from?: string; fwd_link?: string; text: string; mediaUrl?: string; } type Period = "24h" | "today" | "week" | "month" | "all"; const PERIOD_LABELS: Record = { "24h": "24ч", today: "Сегодня", week: "Неделя", month: "Месяц", all: "Все", }; function getChannelColor(channel: string): string { const colors = [ "#2AABEE", "#E14E54", "#9B59B6", "#3498DB", "#E67E22", "#1ABC9C", "#E74C3C", "#2ECC71", "#F39C12", "#8E44AD", "#16A085", "#D35400", "#2980B9", "#C0392B", "#27AE60", ]; let hash = 0; for (let i = 0; i < channel.length; i++) { hash = channel.charCodeAt(i) + ((hash << 5) - hash); } return colors[Math.abs(hash) % colors.length]; } function timeAgo(dateStr: string): string { const now = new Date(); const date = new Date(dateStr); const diff = Math.floor((now.getTime() - date.getTime()) / 1000); if (diff < 60) return "сейчас"; if (diff < 3600) return `${Math.floor(diff / 60)}м`; if (diff < 86400) return `${Math.floor(diff / 3600)}ч`; if (diff < 604800) return `${Math.floor(diff / 86400)}д`; return date.toLocaleDateString("ru-RU", { day: "numeric", month: "short" }); } function filterByPeriod(posts: Post[], period: Period): Post[] { if (period === "all") return posts; const now = new Date(); const cutoff = new Date(); switch (period) { case "24h": cutoff.setHours(now.getHours() - 24); break; case "today": cutoff.setHours(0, 0, 0, 0); break; case "week": cutoff.setDate(now.getDate() - 7); break; case "month": cutoff.setMonth(now.getMonth() - 1); break; } return posts.filter((p) => new Date(p.date) >= cutoff); } function PostRow({ post }: { post: Post }) { const [imgError, setImgError] = useState(false); const color = getChannelColor(post.channel); const postUrl = `https://t.me/${post.channel}/${post.id}`; return (
{/* Header: initial + channel + time */}
{post.channel[0].toUpperCase()} @{post.channel} {timeAgo(post.date)}
{/* Forwarded from */} {post.fwd_from && (
↩ {post.fwd_from}
)} {/* Media */} {post.mediaUrl && (
{imgError ? (
🖼
) : ( setImgError(true)} style={{ width: "100%", borderRadius: 8, objectFit: "cover", maxHeight: 300, display: "block" }} /> )}
)} {/* Full post text with HTML formatting */}
{/* Metrics + open link */}
{post.views && 👁 {post.views}} {post.reactions && ❤️ {post.reactions}} Открыть →
); } export default function TelegramDigest() { const [period, setPeriod] = useState("today"); const [channelFilter, setChannelFilter] = useState("all"); const allChannels = useMemo(() => [...new Set(POSTS_DATA.map((p) => p.channel))], []); const filtered = useMemo(() => { let posts = filterByPeriod(POSTS_DATA, period); if (channelFilter !== "all") { posts = posts.filter((p) => p.channel === channelFilter); } return posts.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()); }, [period, channelFilter]); return (
{/* Header */}

Telegram Digest

{filtered.length} постов
{/* Period tabs */}
{(Object.keys(PERIOD_LABELS) as Period[]).map((p) => ( ))}
{/* Channel chips */}
{allChannels.map((ch) => ( ))}
{/* Divider */}
{/* Posts */} {filtered.length === 0 ? (
Нет постов за выбранный период
) : ( filtered.map((post) => ) )}
); }