diff --git a/.gitignore b/.gitignore index d2ac76d14..342c0b258 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ .flashrank_cache* -podcasts/ +./surfsense_backend/podcasts/ .env node_modules/ .ruff_cache/ \ No newline at end of file diff --git a/surfsense_web/app/dashboard/[search_space_id]/chats/chats-client.tsx b/surfsense_web/app/dashboard/[search_space_id]/chats/chats-client.tsx index 64661620d..1d00ef01a 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/chats/chats-client.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/chats/chats-client.tsx @@ -15,7 +15,7 @@ import { AnimatePresence, motion, type Variants } from "motion/react"; import { useRouter, useSearchParams } from "next/navigation"; import { useEffect, useState } from "react"; import { deleteChatMutationAtom } from "@/atoms/chats/chat-mutation.atoms"; -import { activeSearchSpaceChatsAtom } from "@/atoms/chats/chat-querie.atoms"; +import { chatsAtom } from "@/atoms/chats/chat-query.atoms"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; @@ -103,11 +103,7 @@ export default function ChatsPageClient({ searchSpaceId }: ChatsPageClientProps) id: number; title: string; } | null>(null); - const { - isFetching: isFetchingChats, - data: chats, - error: fetchError, - } = useAtomValue(activeSearchSpaceChatsAtom); + const { isFetching: isFetchingChats, data: chats, error: fetchError } = useAtomValue(chatsAtom); const [{ isPending: isDeletingChat, mutateAsync: deleteChat, error: deleteError }] = useAtom(deleteChatMutationAtom); @@ -161,7 +157,7 @@ export default function ChatsPageClient({ searchSpaceId }: ChatsPageClientProps) const handleDeleteChat = async () => { if (!chatToDelete) return; - await deleteChat(chatToDelete.id); + await deleteChat({ id: chatToDelete.id }); setDeleteDialogOpen(false); setChatToDelete(null); diff --git a/surfsense_web/app/dashboard/[search_space_id]/client-layout.tsx b/surfsense_web/app/dashboard/[search_space_id]/client-layout.tsx index 213868314..4ec8046a4 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/client-layout.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/client-layout.tsx @@ -7,8 +7,7 @@ import { useParams, usePathname, useRouter } from "next/navigation"; import { useTranslations } from "next-intl"; import type React from "react"; import { useEffect, useMemo, useState } from "react"; -import { activeChatIdAtom } from "@/atoms/chats/chat-querie.atoms"; -import { activeChathatUIAtom } from "@/atoms/chats/ui.atoms"; +import { activeChathatUIAtom, activeChatIdAtom } from "@/atoms/chats/ui.atoms"; import { activeSearchSpaceIdAtom } from "@/atoms/seach-spaces/seach-space-queries.atom"; import { ChatPanelContainer } from "@/components/chat/ChatPanel/ChatPanelContainer"; import { DashboardBreadcrumb } from "@/components/dashboard-breadcrumb"; diff --git a/surfsense_web/app/dashboard/[search_space_id]/podcasts/podcasts-client.tsx b/surfsense_web/app/dashboard/[search_space_id]/podcasts/podcasts-client.tsx index 9f0a7be29..730defae8 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/podcasts/podcasts-client.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/podcasts/podcasts-client.tsx @@ -1,12 +1,13 @@ "use client"; import { format } from "date-fns"; +import { useAtom, useAtomValue } from "jotai"; import { Calendar, MoreHorizontal, Pause, Play, - Podcast, + Podcast as PodcastIcon, Search, SkipBack, SkipForward, @@ -19,6 +20,8 @@ import { AnimatePresence, motion, type Variants } from "motion/react"; import Image from "next/image"; import { useEffect, useRef, useState } from "react"; import { toast } from "sonner"; +import { deletePodcastMutationAtom } from "@/atoms/podcasts/podcast-mutation.atoms"; +import { podcastsAtom } from "@/atoms/podcasts/podcast-query.atoms"; // UI Components import { Button } from "@/components/ui/button"; import { Card } from "@/components/ui/card"; @@ -46,16 +49,8 @@ import { SelectValue, } from "@/components/ui/select"; import { Slider } from "@/components/ui/slider"; - -export interface PodcastItem { - id: number; - title: string; - created_at: string; - file_location: string; - podcast_transcript: any[]; - search_space_id: number; - chat_state_version: number | null; -} +import type { Podcast } from "@/contracts/types/podcast.types"; +import { podcastsApiService } from "@/lib/apis/podcasts-api.service"; interface PodcastsPageClientProps { searchSpaceId: string; @@ -85,10 +80,7 @@ const podcastCardVariants: Variants = { const MotionCard = motion(Card); export default function PodcastsPageClient({ searchSpaceId }: PodcastsPageClientProps) { - const [podcasts, setPodcasts] = useState([]); - const [filteredPodcasts, setFilteredPodcasts] = useState([]); - const [isLoading, setIsLoading] = useState(true); - const [error, setError] = useState(null); + const [filteredPodcasts, setFilteredPodcasts] = useState([]); const [searchQuery, setSearchQuery] = useState(""); const [sortOrder, setSortOrder] = useState("newest"); const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); @@ -96,10 +88,9 @@ export default function PodcastsPageClient({ searchSpaceId }: PodcastsPageClient id: number; title: string; } | null>(null); - const [isDeleting, setIsDeleting] = useState(false); // Audio player state - const [currentPodcast, setCurrentPodcast] = useState(null); + const [currentPodcast, setCurrentPodcast] = useState(null); const [audioSrc, setAudioSrc] = useState(undefined); const [isAudioLoading, setIsAudioLoading] = useState(false); const [isPlaying, setIsPlaying] = useState(false); @@ -109,64 +100,39 @@ export default function PodcastsPageClient({ searchSpaceId }: PodcastsPageClient const [isMuted, setIsMuted] = useState(false); const audioRef = useRef(null); const currentObjectUrlRef = useRef(null); + const [{ isPending: isDeletingPodcast, mutateAsync: deletePodcast, error: deleteError }] = + useAtom(deletePodcastMutationAtom); + const { + data: podcasts, + isLoading: isFetchingPodcasts, + error: fetchError, + } = useAtomValue(podcastsAtom); // Add podcast image URL constant const PODCAST_IMAGE_URL = "https://static.vecteezy.com/system/resources/thumbnails/002/157/611/small_2x/illustrations-concept-design-podcast-channel-free-vector.jpg"; - // Fetch podcasts from API useEffect(() => { - const fetchPodcasts = async () => { - try { - setIsLoading(true); + if (isFetchingPodcasts) return; - // Get token from localStorage - const token = localStorage.getItem("surfsense_bearer_token"); + if (fetchError) { + console.error("Error fetching podcasts:", fetchError); + setFilteredPodcasts([]); + return; + } - if (!token) { - setError("Authentication token not found. Please log in again."); - setIsLoading(false); - return; - } + if (!podcasts) { + setFilteredPodcasts([]); + return; + } - // Fetch all podcasts for this search space - const response = await fetch( - `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/podcasts`, - { - headers: { - Authorization: `Bearer ${token}`, - "Content-Type": "application/json", - }, - cache: "no-store", - } - ); - - if (!response.ok) { - const errorData = await response.json().catch(() => null); - throw new Error( - `Failed to fetch podcasts: ${response.status} ${errorData?.detail || ""}` - ); - } - - const data: PodcastItem[] = await response.json(); - setPodcasts(data); - setFilteredPodcasts(data); - setError(null); - } catch (error) { - console.error("Error fetching podcasts:", error); - setError(error instanceof Error ? error.message : "Unknown error occurred"); - setPodcasts([]); - setFilteredPodcasts([]); - } finally { - setIsLoading(false); - } - }; - - fetchPodcasts(); + setFilteredPodcasts(podcasts); }, []); // Filter and sort podcasts based on search query and sort order useEffect(() => { + if (!podcasts) return; + let result = [...podcasts]; // Filter by search term @@ -305,7 +271,7 @@ export default function PodcastsPageClient({ searchSpaceId }: PodcastsPageClient }; // Play podcast - Fetch blob and set object URL - const playPodcast = async (podcast: PodcastItem) => { + const playPodcast = async (podcast: Podcast) => { // If the same podcast is selected, just toggle play/pause if (currentPodcast && currentPodcast.id === podcast.id) { togglePlayPause(); @@ -326,11 +292,6 @@ export default function PodcastsPageClient({ searchSpaceId }: PodcastsPageClient setIsPlaying(false); setIsAudioLoading(true); - const token = localStorage.getItem("surfsense_bearer_token"); - if (!token) { - throw new Error("Authentication token not found."); - } - // Revoke previous object URL if exists (only after we've started the new request) if (currentObjectUrlRef.current) { URL.revokeObjectURL(currentObjectUrlRef.current); @@ -342,22 +303,11 @@ export default function PodcastsPageClient({ searchSpaceId }: PodcastsPageClient const timeoutId = setTimeout(() => controller.abort(), 30000); // 30 second timeout try { - const response = await fetch( - `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/podcasts/${podcast.id}/stream`, - { - headers: { - Authorization: `Bearer ${token}`, - }, - signal: controller.signal, - } - ); - - if (!response.ok) { - throw new Error(`Failed to fetch audio stream: ${response.statusText}`); - } - - const blob = await response.blob(); - const objectUrl = URL.createObjectURL(blob); + const response = await podcastsApiService.loadPodcast({ + request: { id: podcast.id }, + controller, + }); + const objectUrl = URL.createObjectURL(response); currentObjectUrlRef.current = objectUrl; // Set audio source @@ -388,38 +338,13 @@ export default function PodcastsPageClient({ searchSpaceId }: PodcastsPageClient const handleDeletePodcast = async () => { if (!podcastToDelete) return; - setIsDeleting(true); try { - const token = localStorage.getItem("surfsense_bearer_token"); - if (!token) { - setIsDeleting(false); - return; - } + await deletePodcast({ id: podcastToDelete.id }); - const response = await fetch( - `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/podcasts/${podcastToDelete.id}`, - { - method: "DELETE", - headers: { - Authorization: `Bearer ${token}`, - "Content-Type": "application/json", - }, - } - ); - - if (!response.ok) { - throw new Error(`Failed to delete podcast: ${response.statusText}`); - } - - // Close dialog and refresh podcasts + // Close dialog setDeleteDialogOpen(false); setPodcastToDelete(null); - // Update local state by removing the deleted podcast - setPodcasts((prevPodcasts) => - prevPodcasts.filter((podcast) => podcast.id !== podcastToDelete.id) - ); - // If the current playing podcast is deleted, stop playback if (currentPodcast && currentPodcast.id === podcastToDelete.id) { if (audioRef.current) { @@ -428,13 +353,9 @@ export default function PodcastsPageClient({ searchSpaceId }: PodcastsPageClient setCurrentPodcast(null); setIsPlaying(false); } - - toast.success("Podcast deleted successfully"); } catch (error) { console.error("Error deleting podcast:", error); toast.error(error instanceof Error ? error.message : "Failed to delete podcast"); - } finally { - setIsDeleting(false); } }; @@ -483,7 +404,7 @@ export default function PodcastsPageClient({ searchSpaceId }: PodcastsPageClient {/* Status Messages */} - {isLoading && ( + {isFetchingPodcasts && (
@@ -492,16 +413,16 @@ export default function PodcastsPageClient({ searchSpaceId }: PodcastsPageClient
)} - {error && !isLoading && ( + {fetchError && !isFetchingPodcasts && (

Error loading podcasts

-

{error}

+

{fetchError.message ?? "Failed to load podcasts"}

)} - {!isLoading && !error && filteredPodcasts.length === 0 && ( + {!isFetchingPodcasts && !fetchError && filteredPodcasts.length === 0 && (
- +

No podcasts found

{searchQuery @@ -512,7 +433,7 @@ export default function PodcastsPageClient({ searchSpaceId }: PodcastsPageClient )} {/* Podcast Grid */} - {!isLoading && !error && filteredPodcasts.length > 0 && ( + {!isFetchingPodcasts && !fetchError && filteredPodcasts.length > 0 && ( - +

@@ -957,17 +878,17 @@ export default function PodcastsPageClient({ searchSpaceId }: PodcastsPageClient