'use client' import { useEffect, useMemo, useState } from 'react' import { formatDistanceToNow } from 'date-fns' import { InfoIcon, Trash2 } from 'lucide-react' import { resolvePodcastAssetUrl } from '@/lib/api/podcasts' import { EpisodeStatus, PodcastEpisode } from '@/lib/types/podcasts' import { cn } from '@/lib/utils' import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger, } from '@/components/ui/alert-dialog' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import { Card, CardContent } from '@/components/ui/card' import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger, } from '@/components/ui/dialog' import { ScrollArea } from '@/components/ui/scroll-area' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' interface EpisodeCardProps { episode: PodcastEpisode onDelete: (episodeId: string) => Promise | void deleting?: boolean } const STATUS_META: Record< EpisodeStatus | 'unknown', { label: string; className: string } > = { running: { label: 'Processing', className: 'bg-amber-100 text-amber-800 border-amber-200', }, processing: { label: 'Processing', className: 'bg-amber-100 text-amber-800 border-amber-200', }, completed: { label: 'Completed', className: 'bg-emerald-100 text-emerald-800 border-emerald-200', }, failed: { label: 'Failed', className: 'bg-red-100 text-red-800 border-red-200', }, error: { label: 'Failed', className: 'bg-red-100 text-red-800 border-red-200', }, pending: { label: 'Pending', className: 'bg-sky-100 text-sky-800 border-sky-200', }, submitted: { label: 'Pending', className: 'bg-sky-100 text-sky-800 border-sky-200', }, unknown: { label: 'Unknown', className: 'bg-muted text-muted-foreground border-transparent', }, } function StatusBadge({ status }: { status?: EpisodeStatus | null }) { // Don't show badge for completed episodes if (status === 'completed') { return null } const meta = STATUS_META[status ?? 'unknown'] return ( {meta.label} ) } type OutlineSegment = { name?: string description?: string size?: string } type OutlineData = { segments?: OutlineSegment[] } type TranscriptEntry = { speaker?: string dialogue?: string } type TranscriptData = { transcript?: TranscriptEntry[] } function extractOutlineSegments(outline: unknown): OutlineSegment[] { if (outline && typeof outline === 'object' && 'segments' in outline) { const data = outline as OutlineData if (Array.isArray(data.segments)) { return data.segments } } return [] } function extractTranscriptEntries(transcript: unknown): TranscriptEntry[] { if (transcript && typeof transcript === 'object' && 'transcript' in transcript) { const data = transcript as TranscriptData if (Array.isArray(data.transcript)) { return data.transcript } } return [] } export function EpisodeCard({ episode, onDelete, deleting }: EpisodeCardProps) { const [audioSrc, setAudioSrc] = useState() const [audioError, setAudioError] = useState(null) const [detailsOpen, setDetailsOpen] = useState(false) const outlineSegments = useMemo(() => extractOutlineSegments(episode.outline), [episode.outline]) const transcriptEntries = useMemo(() => extractTranscriptEntries(episode.transcript), [episode.transcript]) useEffect(() => { let revokeUrl: string | undefined setAudioError(null) // If backend exposed a protected endpoint, fetch it with auth headers const loadProtectedAudio = async () => { // First resolve the audio URL const directAudioUrl = await resolvePodcastAssetUrl(episode.audio_url ?? episode.audio_file) if (!directAudioUrl || !episode.audio_url) { setAudioSrc(directAudioUrl) return } try { let token: string | undefined if (typeof window !== 'undefined') { const raw = window.localStorage.getItem('auth-storage') if (raw) { try { const parsed = JSON.parse(raw) token = parsed?.state?.token } catch (error) { console.error('Failed to parse auth storage', error) } } } const headers: HeadersInit = {} if (token) { headers.Authorization = `Bearer ${token}` } const response = await fetch(directAudioUrl, { headers }) if (!response.ok) { throw new Error(`Audio request failed with status ${response.status}`) } const blob = await response.blob() revokeUrl = URL.createObjectURL(blob) setAudioSrc(revokeUrl) } catch (error) { console.error('Unable to load podcast audio', error) setAudioError('Audio unavailable') setAudioSrc(undefined) } } void loadProtectedAudio() return () => { if (revokeUrl) { URL.revokeObjectURL(revokeUrl) } } }, [episode.audio_url, episode.audio_file]) const createdLabel = episode.created ? formatDistanceToNow(new Date(episode.created), { addSuffix: true, }) : null const handleDelete = () => { void onDelete(episode.id) } return (

{episode.name}

Profile: {episode.episode_profile?.name ?? 'Unknown'} {createdLabel ? ` • Created ${createdLabel}` : ''}

{episode.name} {episode.episode_profile?.name ?? 'Unknown profile'} {createdLabel ? ` • Created ${createdLabel}` : ''}
{audioSrc ? (
Delete episode? This will remove “{episode.name}” and its audio file permanently. Cancel {deleting ? 'Deleting…' : 'Delete'}
{audioSrc ? (
) }