"use client"; import React, { useEffect, useState } from "react"; import Link from "next/link"; import { Radio, Plus, Headphones, Trash2, Clock, Play, Pause, Maximize2, Search, RefreshCw, Loader2, AlertCircle, RotateCw, Hourglass, Sparkles, } from "lucide-react"; import { useAudio, Episode } from "@/contexts/AudioContext"; import { ThemeToggle } from "@/components/ThemeToggle"; export default function HomePage() { const { activeEpisode, isPlaying, playEpisode, togglePlay, openZen } = useAudio(); const [episodes, setEpisodes] = useState([]); const [workerStatus, setWorkerStatus] = useState(null); const [isLoading, setIsLoading] = useState(true); const [searchQuery, setSearchQuery] = useState(""); const fetchEpisodes = async () => { try { const [epRes, workerRes] = await Promise.all([ fetch("/api/episodes"), fetch("/api/worker/status").catch(() => null), ]); const data = await epRes.json(); if (data.success && data.episodes) { setEpisodes(data.episodes); } if (workerRes && workerRes.ok) { const workerData = await workerRes.json(); if (workerData.success) { setWorkerStatus(workerData.worker); } } } catch (err) { console.error("Failed to load episodes:", err); } finally { setIsLoading(false); } }; useEffect(() => { fetchEpisodes(); }, []); // Real-time polling when any episode is QUEUED or PROCESSING, or worker is active useEffect(() => { const hasPending = episodes.some( (ep) => ep.status === "QUEUED" || ep.status === "PROCESSING" ); const workerActive = workerStatus && (!workerStatus.isIdle || workerStatus.isBackingOff); if (!hasPending && !workerActive) return; const interval = setInterval(() => { fetchEpisodes(); }, 2500); return () => clearInterval(interval); }, [episodes, workerStatus]); const handleDeleteEpisode = async (id: string, e: React.MouseEvent) => { e.stopPropagation(); if (!confirm("Are you sure you want to delete this episode?")) return; try { await fetch(`/api/episodes/${id}`, { method: "DELETE" }); await fetchEpisodes(); } catch (err) { console.error("Failed to delete episode:", err); } }; const handleRetryEpisode = async (id: string, e: React.MouseEvent) => { e.stopPropagation(); try { await fetch(`/api/episodes/${id}`, { method: "POST" }); await fetchEpisodes(); } catch (err) { console.error("Failed to retry episode:", err); } }; const formatDuration = (secs?: number | null) => { if (!secs) return "0:00"; const m = Math.floor(secs / 60); const s = Math.floor(secs % 60); return `${m}:${s.toString().padStart(2, "0")}`; }; // Filter episodes purely by search query const filteredEpisodes = episodes.filter((ep) => { if (!searchQuery.trim()) return true; const q = searchQuery.toLowerCase(); return ( ep.title.toLowerCase().includes(q) || ep.originalContent.toLowerCase().includes(q) ); }); return (
{/* Top Navbar */}
Be My Anchor v2.0
Add Article
{/* Main Container */}
{/* Search Bar Header */}
setSearchQuery(e.target.value)} placeholder="Search your articles..." className="w-full pl-9 pr-4 py-2.5 bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-xl text-xs sm:text-sm text-slate-900 dark:text-slate-100 placeholder-slate-400 dark:placeholder-slate-500 focus:outline-none focus:border-blue-500 transition shadow-sm" />
{episodes.length} {episodes.length === 1 ? "article" : "articles"}
{/* Worker Cooldown / Rate-limit Alert Banner */} {workerStatus?.isBackingOff && (
Worker Cooldown Active: AI provider is experiencing high demand or rate limits. The queue is automatically paused and will resume shortly.
)} {/* Episode List */} {isLoading ? (

Loading articles...

) : filteredEpisodes.length === 0 ? (

{searchQuery ? "No matching articles" : "No articles yet"}

{searchQuery ? "Try a different search keyword." : "Add your first article or technical paper to listen to it anytime."}

{!searchQuery && ( Add Your First Article )}
) : (
{filteredEpisodes.map((ep) => { const isSelected = activeEpisode?.id === ep.id; const isEpisodePlaying = isSelected && isPlaying; const isReady = ep.status === "READY" && !!ep.audioUrl; const isProcessing = ep.status === "PROCESSING"; const isQueued = ep.status === "QUEUED"; const isFailed = ep.status === "FAILED"; return (
{ if (isReady) { if (isSelected) { togglePlay(); } else { playEpisode(ep); } } }} className={`p-4 sm:p-5 rounded-2xl transition-all border ${ isReady ? "cursor-pointer" : "cursor-default" } ${ isSelected ? "bg-blue-50/70 dark:bg-slate-900 border-blue-400 dark:border-blue-500/50 shadow-md shadow-blue-500/5" : "bg-white dark:bg-slate-900/60 border-slate-200 dark:border-slate-850 hover:border-slate-300 dark:hover:border-slate-800 hover:bg-slate-50 dark:hover:bg-slate-900 shadow-sm" }`} >
{/* Status / Play icon button */} {/* Metadata & Title */}
{/* Mode Badge */} {ep.mode === "DUAL_HOST" ? "🎙️ 双人播客" : ep.mode === "FEYNMAN" ? "💡 费曼精讲" : "📖 逐句朗读"} {/* Language Badge */} {ep.targetLanguage && ep.targetLanguage !== "auto" && ( {ep.targetLanguage === "zh" ? "🇨🇳 中文输出" : "🇺🇸 English"} )} {/* Status Chip */} {isProcessing ? ( {ep.retryCount && ep.retryCount > 0 ? `Retrying (${ep.retryCount}/${ep.maxRetries || 3})...` : "Synthesizing audio..."} ) : isQueued ? ( In Queue ) : isFailed ? ( Failed ) : ( {formatDuration(ep.durationSeconds)} )} • {new Date(ep.createdAt).toLocaleDateString()}

{ep.title}

{/* Error snippet if failed */} {isFailed && ep.errorMessage && (

{ep.errorMessage}

)}
{/* Right side actions */}
{isFailed && ( )} {isReady && isSelected && ( )}
); })}
)}
); }