// ReleaseLogPage — vertical timeline of all release entries (newest first). // Reads window.RELEASE_LOG / TAG_LABELS / MILESTONE_LABELS from src/releaseLog.jsx. const TAG_VARIANT = { feature: 'success', fix: 'warning', enhancement: 'default', ui: 'muted', }; const ReleaseLogPage = ({ lang }) => { const t = lang === 'zh'; const { isMobile } = useViewport(); // Live-fetch admin stats; fallback to hardcoded values from releaseLog.jsx. const [stats, setStats] = React.useState(null); React.useEffect(() => { let cancelled = false; apiFetch('/stats').then(res => { if (cancelled || !res.ok) return; return res.json(); }).then(body => { if (cancelled || !body) return; setStats(body); }).catch(() => {}); return () => { cancelled = true; }; }, []); // All entries sorted by date DESC across all milestones. const sortedEntries = RELEASE_LOG.slice().sort((a, b) => (a.date < b.date ? 1 : a.date > b.date ? -1 : 0)); // Compute where to insert milestone markers: a marker appears before each // run of entries belonging to a new milestone (compared to previous entry). const items = []; let lastMilestone = null; sortedEntries.forEach(entry => { if (entry.milestone !== lastMilestone) { items.push({ type: 'marker', milestone: entry.milestone }); lastMilestone = entry.milestone; } items.push({ type: 'entry', entry }); }); // Layout: vertical line on the left; nodes/markers attach to it; cards to the right. const LINE_LEFT = isMobile ? 14 : 96; // x-position of the timeline line const NODE_SIZE = 12; const MARKER_SIZE = 18; return (
{/* Header */}

{t ? '更新日誌' : 'Change Log'}

{t ? 'PodcastRAG 一路長出來的功能,依時間由新到舊排列。' : 'What PodcastRAG has grown into, newest first along the timeline.'}

{/* Stats banner */} {/* Timeline */}
{/* The vertical line */} {items.length > 0 && (
)}
{items.map((it, idx) => it.type === 'marker' ? : )}
{items.length === 0 && (
{t ? '尚無更新紀錄' : 'No release entries yet'}
)}
); }; const StatsBanner = ({ lang, live, isMobile }) => { const t = lang === 'zh'; const figures = live ? { changes: STATS_CHANGES_COUNT, // not in admin/stats yet — keep static episodes: live.episodes_completed, chunks: live.transcript_chunks, users: live.users, } : { changes: STATS_CHANGES_COUNT, episodes: STATS_EPISODES_COUNT, chunks: STATS_VECTORS_COUNT, users: null, }; const labelDate = live ? (t ? '即時' : 'live') : `${t ? '截至' : 'as of'} ${STATS_AS_OF}`; const Stat = ({ value, zh, en }) => (
{value === null || value === undefined ? '—' : value.toLocaleString()}
{t ? zh : en}
); return (
{live && }
{labelDate}
); }; const TimelineMarker = ({ milestone, lang, isMobile, lineLeft, markerSize }) => { const label = MILESTONE_LABELS[milestone] ? MILESTONE_LABELS[milestone][lang] : milestone; return (
{/* large dot */}
{label}
); }; const TimelineEntry = ({ entry, lang, t, isMobile, lineLeft, nodeSize }) => { const tagLabel = TAG_LABELS[entry.tag] ? TAG_LABELS[entry.tag][lang] : entry.tag; const tagVariant = TAG_VARIANT[entry.tag] || 'default'; return (
{/* node */}
{/* date label on desktop sits to the LEFT of the line */} {!isMobile && (
{entry.date}
)} {/* card */}
{isMobile && ( {entry.date} )} {tagLabel}
{entry.title[lang] || entry.title.en}
{entry.summary[lang] || entry.summary.en}
); }; Object.assign(window, { ReleaseLogPage });