refactor podcast api calls

This commit is contained in:
thierryverse 2025-11-18 11:35:06 +02:00
parent 7223a7b04d
commit 5361290315
10 changed files with 288 additions and 377 deletions

View file

@ -6,7 +6,7 @@ import {
MoreHorizontal, MoreHorizontal,
Pause, Pause,
Play, Play,
Podcast, Podcast as PodcastIcon,
Search, Search,
SkipBack, SkipBack,
SkipForward, SkipForward,
@ -46,16 +46,8 @@ import {
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import { Slider } from "@/components/ui/slider"; import { Slider } from "@/components/ui/slider";
import type { Podcast } from "@/contracts/types/podcast.types";
export interface PodcastItem { import { podcastsApiService } from "@/lib/apis/podcasts-api.service";
id: number;
title: string;
created_at: string;
file_location: string;
podcast_transcript: any[];
search_space_id: number;
chat_state_version: number | null;
}
interface PodcastsPageClientProps { interface PodcastsPageClientProps {
searchSpaceId: string; searchSpaceId: string;
@ -85,8 +77,8 @@ const podcastCardVariants: Variants = {
const MotionCard = motion(Card); const MotionCard = motion(Card);
export default function PodcastsPageClient({ searchSpaceId }: PodcastsPageClientProps) { export default function PodcastsPageClient({ searchSpaceId }: PodcastsPageClientProps) {
const [podcasts, setPodcasts] = useState<PodcastItem[]>([]); const [podcasts, setPodcasts] = useState<Podcast[]>([]);
const [filteredPodcasts, setFilteredPodcasts] = useState<PodcastItem[]>([]); const [filteredPodcasts, setFilteredPodcasts] = useState<Podcast[]>([]);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [searchQuery, setSearchQuery] = useState(""); const [searchQuery, setSearchQuery] = useState("");
@ -99,7 +91,7 @@ export default function PodcastsPageClient({ searchSpaceId }: PodcastsPageClient
const [isDeleting, setIsDeleting] = useState(false); const [isDeleting, setIsDeleting] = useState(false);
// Audio player state // Audio player state
const [currentPodcast, setCurrentPodcast] = useState<PodcastItem | null>(null); const [currentPodcast, setCurrentPodcast] = useState<Podcast | null>(null);
const [audioSrc, setAudioSrc] = useState<string | undefined>(undefined); const [audioSrc, setAudioSrc] = useState<string | undefined>(undefined);
const [isAudioLoading, setIsAudioLoading] = useState(false); const [isAudioLoading, setIsAudioLoading] = useState(false);
const [isPlaying, setIsPlaying] = useState(false); const [isPlaying, setIsPlaying] = useState(false);
@ -148,7 +140,7 @@ export default function PodcastsPageClient({ searchSpaceId }: PodcastsPageClient
); );
} }
const data: PodcastItem[] = await response.json(); const data: Podcast[] = await response.json();
setPodcasts(data); setPodcasts(data);
setFilteredPodcasts(data); setFilteredPodcasts(data);
setError(null); setError(null);
@ -305,7 +297,7 @@ export default function PodcastsPageClient({ searchSpaceId }: PodcastsPageClient
}; };
// Play podcast - Fetch blob and set object URL // 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 the same podcast is selected, just toggle play/pause
if (currentPodcast && currentPodcast.id === podcast.id) { if (currentPodcast && currentPodcast.id === podcast.id) {
togglePlayPause(); togglePlayPause();
@ -326,11 +318,6 @@ export default function PodcastsPageClient({ searchSpaceId }: PodcastsPageClient
setIsPlaying(false); setIsPlaying(false);
setIsAudioLoading(true); 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) // Revoke previous object URL if exists (only after we've started the new request)
if (currentObjectUrlRef.current) { if (currentObjectUrlRef.current) {
URL.revokeObjectURL(currentObjectUrlRef.current); URL.revokeObjectURL(currentObjectUrlRef.current);
@ -342,22 +329,11 @@ export default function PodcastsPageClient({ searchSpaceId }: PodcastsPageClient
const timeoutId = setTimeout(() => controller.abort(), 30000); // 30 second timeout const timeoutId = setTimeout(() => controller.abort(), 30000); // 30 second timeout
try { try {
const response = await fetch( const response = await podcastsApiService.loadPodcast({
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/podcasts/${podcast.id}/stream`, podcast,
{ controller,
headers: { });
Authorization: `Bearer ${token}`, const objectUrl = URL.createObjectURL(response);
},
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);
currentObjectUrlRef.current = objectUrl; currentObjectUrlRef.current = objectUrl;
// Set audio source // Set audio source
@ -501,7 +477,7 @@ export default function PodcastsPageClient({ searchSpaceId }: PodcastsPageClient
{!isLoading && !error && filteredPodcasts.length === 0 && ( {!isLoading && !error && filteredPodcasts.length === 0 && (
<div className="flex flex-col items-center justify-center h-40 gap-2 text-center"> <div className="flex flex-col items-center justify-center h-40 gap-2 text-center">
<Podcast className="h-8 w-8 text-muted-foreground" /> <PodcastIcon className="h-8 w-8 text-muted-foreground" />
<h3 className="font-medium">No podcasts found</h3> <h3 className="font-medium">No podcasts found</h3>
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
{searchQuery {searchQuery
@ -829,7 +805,7 @@ export default function PodcastsPageClient({ searchSpaceId }: PodcastsPageClient
duration: 2, duration: 2,
}} }}
> >
<Podcast className="h-6 w-6 text-primary" /> <PodcastIcon className="h-6 w-6 text-primary" />
</motion.div> </motion.div>
</div> </div>

View file

@ -1,7 +1,7 @@
import { atomWithMutation } from "jotai-tanstack-query"; import { atomWithMutation } from "jotai-tanstack-query";
import { toast } from "sonner"; import { toast } from "sonner";
import type { Chat } from "@/app/dashboard/[search_space_id]/chats/chats-client"; import type { Chat } from "@/app/dashboard/[search_space_id]/chats/chats-client";
import { chatApiService } from "@/lib/apis/chats-api.service"; import { chatsApiService } from "@/lib/apis/chats-api.service";
import { cacheKeys } from "@/lib/query-client/cache-keys"; import { cacheKeys } from "@/lib/query-client/cache-keys";
import { queryClient } from "@/lib/query-client/client"; import { queryClient } from "@/lib/query-client/client";
import { activeSearchSpaceIdAtom } from "../seach-spaces/seach-space-queries.atom"; import { activeSearchSpaceIdAtom } from "../seach-spaces/seach-space-queries.atom";
@ -14,7 +14,7 @@ export const deleteChatMutationAtom = atomWithMutation((get) => {
mutationKey: cacheKeys.activeSearchSpace.chats(searchSpaceId ?? ""), mutationKey: cacheKeys.activeSearchSpace.chats(searchSpaceId ?? ""),
enabled: !!searchSpaceId && !!authToken, enabled: !!searchSpaceId && !!authToken,
mutationFn: async (chatId: number) => { mutationFn: async (chatId: number) => {
return chatApiService.deleteChat({ id: chatId }); return chatsApiService.deleteChat({ id: chatId });
}, },
onSuccess: (_, chatId) => { onSuccess: (_, chatId) => {

View file

@ -1,16 +1,16 @@
import { atom } from "jotai"; import { atom } from "jotai";
import { atomWithQuery } from "jotai-tanstack-query"; import { atomWithQuery } from "jotai-tanstack-query";
import type { ChatDetails } from "@/app/dashboard/[search_space_id]/chats/chats-client"; import type { ChatDetails } from "@/app/dashboard/[search_space_id]/chats/chats-client";
import type { PodcastItem } from "@/app/dashboard/[search_space_id]/podcasts/podcasts-client";
import { activeSearchSpaceIdAtom } from "@/atoms/seach-spaces/seach-space-queries.atom"; import { activeSearchSpaceIdAtom } from "@/atoms/seach-spaces/seach-space-queries.atom";
import { chatApiService } from "@/lib/apis/chats-api.service"; import type { Podcast } from "@/contracts/types/podcast.types";
import { getPodcastByChatId } from "@/lib/apis/podcasts.api"; import { chatsApiService } from "@/lib/apis/chats-api.service";
import { podcastsApiService } from "@/lib/apis/podcasts-api.service";
import { cacheKeys } from "@/lib/query-client/cache-keys"; import { cacheKeys } from "@/lib/query-client/cache-keys";
type ActiveChatState = { type ActiveChatState = {
chatId: string | null; chatId: string | null;
chatDetails: ChatDetails | null; chatDetails: ChatDetails | null;
podcast: PodcastItem | null; podcast: Podcast | null;
}; };
export const activeChatIdAtom = atom<string | null>(null); export const activeChatIdAtom = atom<string | null>(null);
@ -31,8 +31,8 @@ export const activeChatAtom = atomWithQuery<ActiveChatState>((get) => {
} }
const [podcast, chatDetails] = await Promise.all([ const [podcast, chatDetails] = await Promise.all([
getPodcastByChatId(activeChatId, authToken), podcastsApiService.getPodcastByChatId({ chat_id: Number(activeChatId) }),
chatApiService.getChatDetails({ id: Number(activeChatId) }), chatsApiService.getChatDetails({ id: Number(activeChatId) }),
]); ]);
return { chatId: activeChatId, chatDetails, podcast }; return { chatId: activeChatId, chatDetails, podcast };
@ -48,7 +48,7 @@ export const activeSearchSpaceChatsAtom = atomWithQuery((get) => {
queryKey: cacheKeys.activeSearchSpace.chats(searchSpaceId ?? ""), queryKey: cacheKeys.activeSearchSpace.chats(searchSpaceId ?? ""),
enabled: !!searchSpaceId && !!authToken, enabled: !!searchSpaceId && !!authToken,
queryFn: async () => { queryFn: async () => {
return chatApiService.getChatsBySearchSpace({ search_space_id: Number(searchSpaceId) }); return chatsApiService.getChatsBySearchSpace({ search_space_id: Number(searchSpaceId) });
}, },
}; };
}); });

View file

@ -4,18 +4,11 @@ import { LoaderIcon, PanelRight, TriangleAlert } from "lucide-react";
import { toast } from "sonner"; import { toast } from "sonner";
import { activeChatAtom, activeChatIdAtom } from "@/atoms/chats/chat-querie.atoms"; import { activeChatAtom, activeChatIdAtom } from "@/atoms/chats/chat-querie.atoms";
import { activeChathatUIAtom } from "@/atoms/chats/ui.atoms"; import { activeChathatUIAtom } from "@/atoms/chats/ui.atoms";
import { generatePodcast } from "@/lib/apis/podcasts.api"; import type { GeneratePodcastRequest } from "@/contracts/types/podcast.types";
import { podcastsApiService } from "@/lib/apis/podcasts-api.service";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { ChatPanelView } from "./ChatPanelView"; import { ChatPanelView } from "./ChatPanelView";
export interface GeneratePodcastRequest {
type: "CHAT" | "DOCUMENT";
ids: number[];
search_space_id: number;
podcast_title?: string;
user_prompt?: string;
}
export function ChatPanelContainer() { export function ChatPanelContainer() {
const { const {
data: activeChatState, data: activeChatState,
@ -31,7 +24,7 @@ export function ChatPanelContainer() {
if (!authToken) { if (!authToken) {
throw new Error("Authentication error. Please log in again."); throw new Error("Authentication error. Please log in again.");
} }
await generatePodcast(request, authToken); await podcastsApiService.generatePodcast(request);
toast.success(`Podcast generation started!`); toast.success(`Podcast generation started!`);
} catch (error) { } catch (error) {
toast.error("Error generating podcast. Please log in again."); toast.error("Error generating podcast. Please log in again.");

View file

@ -1,16 +1,17 @@
"use client"; "use client";
import { Pause, Play, Podcast, SkipBack, SkipForward, Volume2, VolumeX, X } from "lucide-react"; import { Pause, Play, SkipBack, SkipForward, Volume2, VolumeX, X } from "lucide-react";
import { motion } from "motion/react"; import { motion } from "motion/react";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import type { PodcastItem } from "@/app/dashboard/[search_space_id]/podcasts/podcasts-client";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Slider } from "@/components/ui/slider"; import { Slider } from "@/components/ui/slider";
import type { Podcast } from "@/contracts/types/podcast.types";
import { podcastsApiService } from "@/lib/apis/podcasts-api.service";
import { PodcastPlayerCompactSkeleton } from "./PodcastPlayerCompactSkeleton"; import { PodcastPlayerCompactSkeleton } from "./PodcastPlayerCompactSkeleton";
interface PodcastPlayerProps { interface PodcastPlayerProps {
podcast: PodcastItem | null; podcast: Podcast | null;
isLoading?: boolean; isLoading?: boolean;
onClose?: () => void; onClose?: () => void;
compact?: boolean; compact?: boolean;
@ -56,11 +57,6 @@ export function PodcastPlayer({
const loadPodcast = async () => { const loadPodcast = async () => {
setIsFetching(true); setIsFetching(true);
try { try {
const token = localStorage.getItem("surfsense_bearer_token");
if (!token) {
throw new Error("Authentication token not found.");
}
// Revoke previous object URL if exists // Revoke previous object URL if exists
if (currentObjectUrlRef.current) { if (currentObjectUrlRef.current) {
URL.revokeObjectURL(currentObjectUrlRef.current); URL.revokeObjectURL(currentObjectUrlRef.current);
@ -71,22 +67,12 @@ export function PodcastPlayer({
const timeoutId = setTimeout(() => controller.abort(), 30000); const timeoutId = setTimeout(() => controller.abort(), 30000);
try { try {
const response = await fetch( const response = await podcastsApiService.loadPodcast({
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/podcasts/${podcast.id}/stream`, podcast,
{ controller,
headers: { });
Authorization: `Bearer ${token}`,
},
signal: controller.signal,
}
);
if (!response.ok) { const objectUrl = URL.createObjectURL(response);
throw new Error(`Failed to fetch audio stream: ${response.statusText}`);
}
const blob = await response.blob();
const objectUrl = URL.createObjectURL(blob);
currentObjectUrlRef.current = objectUrl; currentObjectUrlRef.current = objectUrl;
setAudioSrc(objectUrl); setAudioSrc(objectUrl);
} catch (error) { } catch (error) {

View file

@ -1,29 +1,27 @@
import { z } from "zod"; import { z } from "zod";
export const podcast = z.object({ export const podcast = z.object({
id: z.number(), id: z.number(),
title: z.string(), title: z.string(),
created_at: z.string(), created_at: z.string(),
file_location: z.string(), file_location: z.string(),
podcast_transcript: z.array(z.any()), podcast_transcript: z.array(z.any()),
search_space_id: z.number(), search_space_id: z.number(),
chat_state_version: z.number().nullable(), chat_state_version: z.number().nullable(),
}); });
export const generatePodcastRequest = z.object({ export const generatePodcastRequest = z.object({
type: z.enum(["CHAT", "DOCUMENT"]), type: z.enum(["CHAT", "DOCUMENT"]),
ids: z.array(z.number()), ids: z.array(z.number()),
search_space_id: z.number(), search_space_id: z.number(),
podcast_title: z.string().optional(), podcast_title: z.string().optional(),
user_prompt: z.string().optional(), user_prompt: z.string().optional(),
}); });
export const getPodcastByChatIdRequest = z.object({ export const getPodcastByChatIdRequest = z.object({
chat_id: z.number(), chat_id: z.number(),
}); });
export type GeneratePodcastRequest = z.infer<typeof generatePodcastRequest>; export type GeneratePodcastRequest = z.infer<typeof generatePodcastRequest>;
export type GetPodcastByChatIdRequest = z.infer< export type GetPodcastByChatIdRequest = z.infer<typeof getPodcastByChatIdRequest>;
typeof getPodcastByChatIdRequest
>;
export type Podcast = z.infer<typeof podcast>; export type Podcast = z.infer<typeof podcast>;

View file

@ -9,7 +9,7 @@ import {
import { ValidationError } from "../error"; import { ValidationError } from "../error";
import { baseApiService } from "./base-api.service"; import { baseApiService } from "./base-api.service";
class AuthApiService { class AuthApiService {
login = async (request: LoginRequest) => { login = async (request: LoginRequest) => {
// Validate the request // Validate the request
const parsedRequest = loginRequest.safeParse(request); const parsedRequest = loginRequest.safeParse(request);

View file

@ -1,264 +1,232 @@
import type z from "zod"; import type z from "zod";
import { import { AppError, AuthenticationError, AuthorizationError, NotFoundError } from "../error";
AppError,
AuthenticationError,
AuthorizationError,
NotFoundError,
} from "../error";
enum ResponseType { enum ResponseType {
JSON = "json", JSON = "json",
TEXT = "text", TEXT = "text",
BLOB = "blob", BLOB = "blob",
ARRAY_BUFFER = "arrayBuffer", ARRAY_BUFFER = "arrayBuffer",
// Add more response types as needed // Add more response types as needed
} }
export type RequestOptions = { export type RequestOptions = {
method: "GET" | "POST" | "PUT" | "DELETE"; method: "GET" | "POST" | "PUT" | "DELETE";
headers?: Record<string, string>; headers?: Record<string, string>;
contentType?: "application/json" | "application/x-www-form-urlencoded"; contentType?: "application/json" | "application/x-www-form-urlencoded";
signal?: AbortSignal; signal?: AbortSignal;
body?: any; body?: any;
responseType?: ResponseType; responseType?: ResponseType;
// Add more options as needed // Add more options as needed
}; };
class BaseApiService { class BaseApiService {
bearerToken: string; bearerToken: string;
baseUrl: string; baseUrl: string;
noAuthEndpoints: string[] = [ noAuthEndpoints: string[] = ["/auth/jwt/login", "/auth/register", "/auth/refresh"]; // Add more endpoints as needed
"/auth/jwt/login",
"/auth/register",
"/auth/refresh",
]; // Add more endpoints as needed
constructor(bearerToken: string, baseUrl: string) { constructor(bearerToken: string, baseUrl: string) {
this.bearerToken = bearerToken; this.bearerToken = bearerToken;
this.baseUrl = baseUrl; this.baseUrl = baseUrl;
} }
setBearerToken(bearerToken: string) { setBearerToken(bearerToken: string) {
this.bearerToken = bearerToken; this.bearerToken = bearerToken;
} }
async request<T, R extends ResponseType = ResponseType.JSON>( async request<T, R extends ResponseType = ResponseType.JSON>(
url: string, url: string,
responseSchema?: z.ZodSchema<T>, responseSchema?: z.ZodSchema<T>,
options?: RequestOptions & { responseType?: R } options?: RequestOptions & { responseType?: R }
): Promise< ): Promise<
R extends ResponseType.JSON R extends ResponseType.JSON
? T ? T
: R extends ResponseType.TEXT : R extends ResponseType.TEXT
? string ? string
: R extends ResponseType.BLOB : R extends ResponseType.BLOB
? Blob ? Blob
: R extends ResponseType.ARRAY_BUFFER : R extends ResponseType.ARRAY_BUFFER
? ArrayBuffer ? ArrayBuffer
: unknown : unknown
> { > {
try { try {
const defaultOptions: RequestOptions = { const defaultOptions: RequestOptions = {
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
Authorization: `Bearer ${this.bearerToken || ""}`, Authorization: `Bearer ${this.bearerToken || ""}`,
}, },
method: "GET", method: "GET",
responseType: ResponseType.JSON, responseType: ResponseType.JSON,
}; };
const mergedOptions: RequestOptions = { const mergedOptions: RequestOptions = {
...defaultOptions, ...defaultOptions,
...(options ?? {}), ...(options ?? {}),
headers: { headers: {
...defaultOptions.headers, ...defaultOptions.headers,
...(options?.headers ?? {}), ...(options?.headers ?? {}),
}, },
}; };
if (!this.baseUrl) { if (!this.baseUrl) {
throw new AppError("Base URL is not set."); throw new AppError("Base URL is not set.");
} }
if (!this.bearerToken && !this.noAuthEndpoints.includes(url)) { if (!this.bearerToken && !this.noAuthEndpoints.includes(url)) {
throw new AuthenticationError( throw new AuthenticationError("You are not authenticated. Please login again.");
"You are not authenticated. Please login again." }
);
}
const fullUrl = new URL(url, this.baseUrl).toString(); const fullUrl = new URL(url, this.baseUrl).toString();
const response = await fetch(fullUrl, mergedOptions); const response = await fetch(fullUrl, mergedOptions);
if (!response.ok) { if (!response.ok) {
// biome-ignore lint/suspicious: Unknown // biome-ignore lint/suspicious: Unknown
let data; let data;
try { try {
data = await response.json(); data = await response.json();
} catch (error) { } catch (error) {
console.error("Failed to parse response as JSON:", error); console.error("Failed to parse response as JSON:", error);
throw new AppError( throw new AppError("Something went wrong", response.status, response.statusText);
"Something went wrong", }
response.status,
response.statusText
);
}
// for fastapi errors response // for fastapi errors response
if (typeof data === "object" && "detail" in data) { if (typeof data === "object" && "detail" in data) {
throw new AppError(data.detail, response.status, response.statusText); throw new AppError(data.detail, response.status, response.statusText);
} }
switch (response.status) { switch (response.status) {
case 401: case 401:
throw new AuthenticationError( throw new AuthenticationError(
"You are not authenticated. Please login again.", "You are not authenticated. Please login again.",
response.status, response.status,
response.statusText response.statusText
); );
case 403: case 403:
throw new AuthorizationError( throw new AuthorizationError(
"You don't have permission to access this resource.", "You don't have permission to access this resource.",
response.status, response.status,
response.statusText response.statusText
); );
case 404: case 404:
throw new NotFoundError( throw new NotFoundError("Resource not found", response.status, response.statusText);
"Resource not found", // Add more cases as needed
response.status, default:
response.statusText throw new AppError("Something went wrong", response.status, response.statusText);
); }
// Add more cases as needed }
default:
throw new AppError(
"Something went wrong",
response.status,
response.statusText
);
}
}
// biome-ignore lint/suspicious: Unknown // biome-ignore lint/suspicious: Unknown
let data; let data;
const responseType = mergedOptions.responseType const responseType = mergedOptions.responseType;
try { try {
switch (responseType) { switch (responseType) {
case ResponseType.JSON: case ResponseType.JSON:
data = await response.json(); data = await response.json();
break; break;
case ResponseType.TEXT: case ResponseType.TEXT:
data = await response.text(); data = await response.text();
break; break;
case ResponseType.BLOB: case ResponseType.BLOB:
data = await response.blob(); data = await response.blob();
break; break;
case ResponseType.ARRAY_BUFFER: case ResponseType.ARRAY_BUFFER:
data = await response.arrayBuffer(); data = await response.arrayBuffer();
break; break;
// Add more cases as needed // Add more cases as needed
default: default:
data = await response.text(); data = await response.text();
} }
} catch (error) { } catch (error) {
console.error("Failed to parse response as JSON:", error); console.error("Failed to parse response as JSON:", error);
throw new AppError( throw new AppError("Failed to parse response", response.status, response.statusText);
"Failed to parse response", }
response.status,
response.statusText
);
}
if (responseType === ResponseType.JSON) { if (responseType === ResponseType.JSON) {
if (!responseSchema) { if (!responseSchema) {
return data; return data;
} }
const parsedData = responseSchema.safeParse(data); const parsedData = responseSchema.safeParse(data);
if (!parsedData.success) { if (!parsedData.success) {
/** The request was successful, but the response data does not match the expected schema. /** The request was successful, but the response data does not match the expected schema.
* This is a client side error, and should be fixed by updating the responseSchema to keep things typed. * This is a client side error, and should be fixed by updating the responseSchema to keep things typed.
* This error should not be shown to the user , it is for dev only. * This error should not be shown to the user , it is for dev only.
*/ */
console.error("Invalid API response schema:", parsedData.error); console.error("Invalid API response schema:", parsedData.error);
} }
return data; return data;
} }
return data; return data;
} catch (error) { } catch (error) {
console.error("Request failed:", error); console.error("Request failed:", error);
throw error; throw error;
} }
} }
async get<T>( async get<T>(
url: string, url: string,
responseSchema?: z.ZodSchema<T>, responseSchema?: z.ZodSchema<T>,
options?: Omit<RequestOptions, "method" | "responseType"> options?: Omit<RequestOptions, "method" | "responseType">
) { ) {
return this.request(url, responseSchema, { return this.request(url, responseSchema, {
...options, ...options,
method: "GET", method: "GET",
responseType: ResponseType.JSON, responseType: ResponseType.JSON,
}); });
} }
async post<T>( async post<T>(
url: string, url: string,
responseSchema?: z.ZodSchema<T>, responseSchema?: z.ZodSchema<T>,
options?: Omit<RequestOptions, "method" | "responseType"> options?: Omit<RequestOptions, "method" | "responseType">
) { ) {
return this.request(url, responseSchema, { return this.request(url, responseSchema, {
method: "POST", method: "POST",
...options, ...options,
responseType: ResponseType.JSON, responseType: ResponseType.JSON,
}); });
} }
async put<T>( async put<T>(
url: string, url: string,
responseSchema?: z.ZodSchema<T>, responseSchema?: z.ZodSchema<T>,
options?: Omit<RequestOptions, "method" | "responseType"> options?: Omit<RequestOptions, "method" | "responseType">
) { ) {
return this.request(url, responseSchema, { return this.request(url, responseSchema, {
method: "PUT", method: "PUT",
...options, ...options,
responseType: ResponseType.JSON, responseType: ResponseType.JSON,
}); });
} }
async delete<T>( async delete<T>(
url: string, url: string,
responseSchema?: z.ZodSchema<T>, responseSchema?: z.ZodSchema<T>,
options?: Omit<RequestOptions, "method" | "responseType">, options?: Omit<RequestOptions, "method" | "responseType">
) { ) {
return this.request(url, responseSchema, { return this.request(url, responseSchema, {
method: "DELETE", method: "DELETE",
...options, ...options,
responseType: ResponseType.JSON, responseType: ResponseType.JSON,
}); });
} }
async getBlob( async getBlob(url: string, options?: Omit<RequestOptions, "method" | "responseType">) {
url: string, return this.request(url, undefined, {
options?: Omit<RequestOptions, "method" | "responseType"> ...options,
) { method: "GET",
return this.request(url, undefined, { responseType: ResponseType.BLOB,
...options, });
method: "GET", }
responseType: ResponseType.BLOB,
});
}
} }
export const baseApiService = new BaseApiService( export const baseApiService = new BaseApiService(
typeof window !== "undefined" typeof window !== "undefined" ? localStorage.getItem("surfsense_bearer_token") || "" : "",
? localStorage.getItem("surfsense_bearer_token") || "" process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL || ""
: "",
process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL || ""
); );

View file

@ -17,7 +17,7 @@ import {
import { ValidationError } from "../error"; import { ValidationError } from "../error";
import { baseApiService } from "./base-api.service"; import { baseApiService } from "./base-api.service";
class ChatApiService { class ChatApiService {
getChatDetails = async (request: GetChatDetailsRequest) => { getChatDetails = async (request: GetChatDetailsRequest) => {
// Validate the request // Validate the request
const parsedRequest = getChatDetailsRequest.safeParse(request); const parsedRequest = getChatDetailsRequest.safeParse(request);

View file

@ -1,68 +1,58 @@
import { baseApiService } from "./base-api.service";
import { import {
GeneratePodcastRequest, type GeneratePodcastRequest,
generatePodcastRequest, type GetPodcastByChatIdRequest,
getPodcastByChatIdRequest, generatePodcastRequest,
GetPodcastByChatIdRequest, getPodcastByChatIdRequest,
Podcast, type Podcast,
podcast, podcast,
} from "@/contracts/types/podcast.types"; } from "@/contracts/types/podcast.types";
import { ValidationError } from "../error"; import { ValidationError } from "../error";
import { baseApiService } from "./base-api.service";
class PodcastsApiService { class PodcastsApiService {
getPodcastByChatId = async (request: GetPodcastByChatIdRequest) => { getPodcastByChatId = async (request: GetPodcastByChatIdRequest) => {
// Validate the request // Validate the request
const parsedRequest = getPodcastByChatIdRequest.safeParse(request); const parsedRequest = getPodcastByChatIdRequest.safeParse(request);
if (!parsedRequest.success) { if (!parsedRequest.success) {
console.error("Invalid request:", parsedRequest.error); console.error("Invalid request:", parsedRequest.error);
// Format a user frendly error message // Format a user frendly error message
const errorMessage = parsedRequest.error.errors const errorMessage = parsedRequest.error.errors.map((err) => err.message).join(", ");
.map((err) => err.message) throw new ValidationError(`Invalid request: ${errorMessage}`);
.join(", "); }
throw new ValidationError(`Invalid request: ${errorMessage}`);
}
return baseApiService.get( return baseApiService.get(`/api/v1/podcasts/by-chat/${request.chat_id}`, podcast);
`/api/v1/podcasts/by-chat/${request.chat_id}`, };
podcast
);
};
generatePodcast = async (request: GeneratePodcastRequest) => { generatePodcast = async (request: GeneratePodcastRequest) => {
// Validate the request // Validate the request
const parsedRequest = generatePodcastRequest.safeParse(request); const parsedRequest = generatePodcastRequest.safeParse(request);
if (!parsedRequest.success) { if (!parsedRequest.success) {
console.error("Invalid request:", parsedRequest.error); console.error("Invalid request:", parsedRequest.error);
// Format a user frendly error message // Format a user frendly error message
const errorMessage = parsedRequest.error.errors const errorMessage = parsedRequest.error.errors.map((err) => err.message).join(", ");
.map((err) => err.message) throw new ValidationError(`Invalid request: ${errorMessage}`);
.join(", "); }
throw new ValidationError(`Invalid request: ${errorMessage}`);
}
return baseApiService.post(`/api/v1/podcasts/generate`, undefined, { return baseApiService.post(`/api/v1/podcasts/generate`, undefined, {
body: request, body: request,
}); });
}; };
loadPodcast = async ({ loadPodcast = async ({
podcast, podcast,
controller, controller,
}: { }: {
podcast: Podcast; podcast: Podcast;
controller?: AbortController; controller?: AbortController;
}) => { }) => {
return await baseApiService.getBlob( return await baseApiService.getBlob(`/api/v1/podcasts/${podcast.id}/stream`, {
`/api/v1/podcasts/${podcast.id}/stream`, signal: controller?.signal,
{ });
signal: controller?.signal, };
}
);
};
} }
export const podcastsApiService = new PodcastsApiService(); export const podcastsApiService = new PodcastsApiService();