mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-07-22 23:31:12 +02:00
refactor podcast api calls
This commit is contained in:
parent
7223a7b04d
commit
5361290315
10 changed files with 288 additions and 377 deletions
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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) => {
|
||||||
|
|
|
||||||
|
|
@ -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) });
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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.");
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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>;
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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 || ""
|
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue