format with biome

This commit is contained in:
thierryverse 2025-11-14 00:42:19 +02:00
parent 77d49ca11c
commit 81ee04c2a5
21 changed files with 1602 additions and 1752 deletions

View file

@ -2,511 +2,473 @@
import { format } from "date-fns";
import {
Calendar,
ExternalLink,
MessageCircleMore,
MoreHorizontal,
Search,
Tag,
Trash2,
Calendar,
ExternalLink,
MessageCircleMore,
MoreHorizontal,
Search,
Tag,
Trash2,
} from "lucide-react";
import { AnimatePresence, motion, type Variants } from "motion/react";
import { useRouter, useSearchParams } from "next/navigation";
import { useEffect, useState } from "react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
import {
Card,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Input } from "@/components/ui/input";
import {
Pagination,
PaginationContent,
PaginationItem,
PaginationLink,
PaginationNext,
PaginationPrevious,
Pagination,
PaginationContent,
PaginationItem,
PaginationLink,
PaginationNext,
PaginationPrevious,
} from "@/components/ui/pagination";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { useAtom, useAtomValue } from "jotai";
import { activeSearchSpaceChatsAtom } from "@/atoms/chats/chat-queries.atom";
import { deleteChatMutationAtom } from "@/atoms/chats/chat-mutations.atom";
export interface Chat {
created_at: string;
id: number;
type: "DOCUMENT" | "CHAT";
title: string;
search_space_id: number;
state_version: number;
created_at: string;
id: number;
type: "DOCUMENT" | "CHAT";
title: string;
search_space_id: number;
state_version: number;
}
export interface ChatDetails {
type: "DOCUMENT" | "CHAT";
title: string;
initial_connectors: string[];
messages: any[];
created_at: string;
id: number;
search_space_id: number;
state_version: number;
type: "DOCUMENT" | "CHAT";
title: string;
initial_connectors: string[];
messages: any[];
created_at: string;
id: number;
search_space_id: number;
state_version: number;
}
interface ChatsPageClientProps {
searchSpaceId: string;
searchSpaceId: string;
}
const pageVariants: Variants = {
initial: { opacity: 0 },
enter: { opacity: 1, transition: { duration: 0.3, ease: "easeInOut" } },
exit: { opacity: 0, transition: { duration: 0.3, ease: "easeInOut" } },
initial: { opacity: 0 },
enter: { opacity: 1, transition: { duration: 0.3, ease: "easeInOut" } },
exit: { opacity: 0, transition: { duration: 0.3, ease: "easeInOut" } },
};
const chatCardVariants: Variants = {
initial: { y: 20, opacity: 0 },
animate: { y: 0, opacity: 1 },
exit: { y: -20, opacity: 0 },
initial: { y: 20, opacity: 0 },
animate: { y: 0, opacity: 1 },
exit: { y: -20, opacity: 0 },
};
const MotionCard = motion(Card);
export default function ChatsPageClient({
searchSpaceId,
}: ChatsPageClientProps) {
const router = useRouter();
const [filteredChats, setFilteredChats] = useState<Chat[]>([]);
const [searchQuery, setSearchQuery] = useState("");
const [currentPage, setCurrentPage] = useState(1);
const [totalPages, setTotalPages] = useState(1);
const [selectedType, setSelectedType] = useState<string>("all");
const [sortOrder, setSortOrder] = useState<string>("newest");
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [chatToDelete, setChatToDelete] = useState<{
id: number;
title: string;
} | null>(null);
const {
isFetching: isFetchingChats,
data: chats,
error: fetchError,
} = useAtomValue(activeSearchSpaceChatsAtom);
const [
{ isPending: isDeletingChat, mutateAsync: deleteChat, error: deleteError }
] = useAtom(deleteChatMutationAtom);
export default function ChatsPageClient({ searchSpaceId }: ChatsPageClientProps) {
const router = useRouter();
const [filteredChats, setFilteredChats] = useState<Chat[]>([]);
const [searchQuery, setSearchQuery] = useState("");
const [currentPage, setCurrentPage] = useState(1);
const [totalPages, setTotalPages] = useState(1);
const [selectedType, setSelectedType] = useState<string>("all");
const [sortOrder, setSortOrder] = useState<string>("newest");
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [chatToDelete, setChatToDelete] = useState<{
id: number;
title: string;
} | null>(null);
const {
isFetching: isFetchingChats,
data: chats,
error: fetchError,
} = useAtomValue(activeSearchSpaceChatsAtom);
const [{ isPending: isDeletingChat, mutateAsync: deleteChat, error: deleteError }] =
useAtom(deleteChatMutationAtom);
const chatsPerPage = 9;
const searchParams = useSearchParams();
const chatsPerPage = 9;
const searchParams = useSearchParams();
// Get initial page from URL params if it exists
useEffect(() => {
const pageParam = searchParams.get("page");
if (pageParam) {
const pageNumber = parseInt(pageParam, 10);
if (!Number.isNaN(pageNumber) && pageNumber > 0) {
setCurrentPage(pageNumber);
}
}
}, [searchParams]);
// Get initial page from URL params if it exists
useEffect(() => {
const pageParam = searchParams.get("page");
if (pageParam) {
const pageNumber = parseInt(pageParam, 10);
if (!Number.isNaN(pageNumber) && pageNumber > 0) {
setCurrentPage(pageNumber);
}
}
}, [searchParams]);
useEffect(() => {
if (fetchError) {
console.error("Error fetching chats:", fetchError);
}
}, [fetchError]);
useEffect(() => {
if (fetchError) {
console.error("Error fetching chats:", fetchError);
}
}, [fetchError]);
useEffect(() => {
if (deleteError) {
console.error("Error deleting chat:", deleteError);
}
}, [deleteError]);
useEffect(() => {
if (deleteError) {
console.error("Error deleting chat:", deleteError);
}
}, [deleteError]);
// Filter and sort chats based on search query, type, and sort order
useEffect(() => {
let result = [...(chats || [])];
// Filter and sort chats based on search query, type, and sort order
useEffect(() => {
let result = [...(chats || [])];
// Filter by search term
if (searchQuery) {
const query = searchQuery.toLowerCase();
result = result.filter((chat) =>
chat.title.toLowerCase().includes(query)
);
}
// Filter by search term
if (searchQuery) {
const query = searchQuery.toLowerCase();
result = result.filter((chat) => chat.title.toLowerCase().includes(query));
}
// Filter by type
if (selectedType !== "all") {
result = result.filter((chat) => chat.type === selectedType);
}
// Filter by type
if (selectedType !== "all") {
result = result.filter((chat) => chat.type === selectedType);
}
// Sort chats
result.sort((a, b) => {
const dateA = new Date(a.created_at).getTime();
const dateB = new Date(b.created_at).getTime();
// Sort chats
result.sort((a, b) => {
const dateA = new Date(a.created_at).getTime();
const dateB = new Date(b.created_at).getTime();
return sortOrder === "newest" ? dateB - dateA : dateA - dateB;
});
return sortOrder === "newest" ? dateB - dateA : dateA - dateB;
});
setFilteredChats(result);
setTotalPages(Math.max(1, Math.ceil(result.length / chatsPerPage)));
setFilteredChats(result);
setTotalPages(Math.max(1, Math.ceil(result.length / chatsPerPage)));
// Reset to first page when filters change
if (
currentPage !== 1 &&
(searchQuery || selectedType !== "all" || sortOrder !== "newest")
) {
setCurrentPage(1);
}
}, [chats, searchQuery, selectedType, sortOrder, currentPage]);
// Reset to first page when filters change
if (currentPage !== 1 && (searchQuery || selectedType !== "all" || sortOrder !== "newest")) {
setCurrentPage(1);
}
}, [chats, searchQuery, selectedType, sortOrder, currentPage]);
// Function to handle chat deletion
const handleDeleteChat = async () => {
if (!chatToDelete) return;
// Function to handle chat deletion
const handleDeleteChat = async () => {
if (!chatToDelete) return;
await deleteChat(chatToDelete.id);
await deleteChat(chatToDelete.id);
setDeleteDialogOpen(false);
setChatToDelete(null);
};
setDeleteDialogOpen(false);
setChatToDelete(null);
};
// Calculate pagination
const indexOfLastChat = currentPage * chatsPerPage; // Index of last chat in the current page
const indexOfFirstChat = indexOfLastChat - chatsPerPage; // Index of first chat in the current page
const currentChats = filteredChats.slice(indexOfFirstChat, indexOfLastChat);
// Calculate pagination
const indexOfLastChat = currentPage * chatsPerPage; // Index of last chat in the current page
const indexOfFirstChat = indexOfLastChat - chatsPerPage; // Index of first chat in the current page
const currentChats = filteredChats.slice(indexOfFirstChat, indexOfLastChat);
// Get unique chat types for filter dropdown
const chatTypes = chats
? ["all", ...Array.from(new Set(chats.map((chat) => chat.type)))]
: [];
// Get unique chat types for filter dropdown
const chatTypes = chats ? ["all", ...Array.from(new Set(chats.map((chat) => chat.type)))] : [];
return (
<motion.div
className="container p-6 mx-auto"
initial="initial"
animate="enter"
exit="exit"
variants={pageVariants}
>
<div className="flex flex-col space-y-4 md:space-y-6">
<div className="flex flex-col space-y-2">
<h1 className="text-3xl font-bold tracking-tight">All Chats</h1>
<p className="text-muted-foreground">
View, search, and manage all your chats.
</p>
</div>
return (
<motion.div
className="container p-6 mx-auto"
initial="initial"
animate="enter"
exit="exit"
variants={pageVariants}
>
<div className="flex flex-col space-y-4 md:space-y-6">
<div className="flex flex-col space-y-2">
<h1 className="text-3xl font-bold tracking-tight">All Chats</h1>
<p className="text-muted-foreground">View, search, and manage all your chats.</p>
</div>
{/* Filter and Search Bar */}
<div className="flex flex-col space-y-4 md:flex-row md:items-center md:justify-between md:space-y-0">
<div className="flex flex-1 items-center gap-2">
<div className="relative w-full md:w-80">
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
type="text"
placeholder="Search chats..."
className="pl-8"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
</div>
{/* Filter and Search Bar */}
<div className="flex flex-col space-y-4 md:flex-row md:items-center md:justify-between md:space-y-0">
<div className="flex flex-1 items-center gap-2">
<div className="relative w-full md:w-80">
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
type="text"
placeholder="Search chats..."
className="pl-8"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
</div>
<Select value={selectedType} onValueChange={setSelectedType}>
<SelectTrigger className="w-full md:w-40">
<SelectValue placeholder="Filter by type" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
{chatTypes.map((type) => (
<SelectItem key={type} value={type}>
{type === "all"
? "All Types"
: type.charAt(0).toUpperCase() + type.slice(1)}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
</div>
<Select value={selectedType} onValueChange={setSelectedType}>
<SelectTrigger className="w-full md:w-40">
<SelectValue placeholder="Filter by type" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
{chatTypes.map((type) => (
<SelectItem key={type} value={type}>
{type === "all" ? "All Types" : type.charAt(0).toUpperCase() + type.slice(1)}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
</div>
<div className="flex items-center gap-2">
<Select value={sortOrder} onValueChange={setSortOrder}>
<SelectTrigger className="w-40">
<SelectValue placeholder="Sort order" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem value="newest">Newest First</SelectItem>
<SelectItem value="oldest">Oldest First</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</div>
</div>
<div className="flex items-center gap-2">
<Select value={sortOrder} onValueChange={setSortOrder}>
<SelectTrigger className="w-40">
<SelectValue placeholder="Sort order" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem value="newest">Newest First</SelectItem>
<SelectItem value="oldest">Oldest First</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</div>
</div>
{/* Status Messages */}
{isFetchingChats && (
<div className="flex items-center justify-center h-40">
<div className="flex flex-col items-center gap-2">
<div className="h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent"></div>
<p className="text-sm text-muted-foreground">Loading chats...</p>
</div>
</div>
)}
{/* Status Messages */}
{isFetchingChats && (
<div className="flex items-center justify-center h-40">
<div className="flex flex-col items-center gap-2">
<div className="h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent"></div>
<p className="text-sm text-muted-foreground">Loading chats...</p>
</div>
</div>
)}
{fetchError && !isFetchingChats && (
<div className="border border-destructive/50 text-destructive p-4 rounded-md">
<h3 className="font-medium">Error loading chats</h3>
<p className="text-sm">{fetchError.message}</p>
</div>
)}
{fetchError && !isFetchingChats && (
<div className="border border-destructive/50 text-destructive p-4 rounded-md">
<h3 className="font-medium">Error loading chats</h3>
<p className="text-sm">{fetchError.message}</p>
</div>
)}
{!isFetchingChats && !fetchError && filteredChats.length === 0 && (
<div className="flex flex-col items-center justify-center h-40 gap-2 text-center">
<MessageCircleMore className="h-8 w-8 text-muted-foreground" />
<h3 className="font-medium">No chats found</h3>
<p className="text-sm text-muted-foreground">
{searchQuery || selectedType !== "all"
? "Try adjusting your search filters"
: "Start a new chat to get started"}
</p>
</div>
)}
{!isFetchingChats && !fetchError && filteredChats.length === 0 && (
<div className="flex flex-col items-center justify-center h-40 gap-2 text-center">
<MessageCircleMore className="h-8 w-8 text-muted-foreground" />
<h3 className="font-medium">No chats found</h3>
<p className="text-sm text-muted-foreground">
{searchQuery || selectedType !== "all"
? "Try adjusting your search filters"
: "Start a new chat to get started"}
</p>
</div>
)}
{/* Chat Grid */}
{!isFetchingChats && !fetchError && filteredChats.length > 0 && (
<AnimatePresence mode="wait">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{currentChats.map((chat, index) => (
<MotionCard
key={chat.id}
variants={chatCardVariants}
initial="initial"
animate="animate"
exit="exit"
transition={{ duration: 0.2, delay: index * 0.05 }}
className="overflow-hidden hover:shadow-md transition-shadow"
>
<CardHeader className="pb-3">
<div className="flex justify-between items-start">
<div className="space-y-1">
<CardTitle className="line-clamp-1">
{chat.title || `Chat ${chat.id}`}
</CardTitle>
<CardDescription>
<span className="flex items-center gap-1">
<Calendar className="h-3.5 w-3.5" />
<span>
{format(new Date(chat.created_at), "MMM d, yyyy")}
</span>
</span>
</CardDescription>
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="icon"
className="h-8 w-8"
>
<MoreHorizontal className="h-4 w-4" />
<span className="sr-only">Open menu</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
onClick={() =>
router.push(
`/dashboard/${chat.search_space_id}/researcher/${chat.id}`
)
}
>
<ExternalLink className="mr-2 h-4 w-4" />
<span>View Chat</span>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
className="text-destructive focus:text-destructive"
onClick={(e) => {
e.stopPropagation();
setChatToDelete({
id: chat.id,
title: chat.title || `Chat ${chat.id}`,
});
setDeleteDialogOpen(true);
}}
>
<Trash2 className="mr-2 h-4 w-4" />
<span>Delete Chat</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</CardHeader>
{/* Chat Grid */}
{!isFetchingChats && !fetchError && filteredChats.length > 0 && (
<AnimatePresence mode="wait">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{currentChats.map((chat, index) => (
<MotionCard
key={chat.id}
variants={chatCardVariants}
initial="initial"
animate="animate"
exit="exit"
transition={{ duration: 0.2, delay: index * 0.05 }}
className="overflow-hidden hover:shadow-md transition-shadow"
>
<CardHeader className="pb-3">
<div className="flex justify-between items-start">
<div className="space-y-1">
<CardTitle className="line-clamp-1">
{chat.title || `Chat ${chat.id}`}
</CardTitle>
<CardDescription>
<span className="flex items-center gap-1">
<Calendar className="h-3.5 w-3.5" />
<span>{format(new Date(chat.created_at), "MMM d, yyyy")}</span>
</span>
</CardDescription>
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="h-8 w-8">
<MoreHorizontal className="h-4 w-4" />
<span className="sr-only">Open menu</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
onClick={() =>
router.push(
`/dashboard/${chat.search_space_id}/researcher/${chat.id}`
)
}
>
<ExternalLink className="mr-2 h-4 w-4" />
<span>View Chat</span>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
className="text-destructive focus:text-destructive"
onClick={(e) => {
e.stopPropagation();
setChatToDelete({
id: chat.id,
title: chat.title || `Chat ${chat.id}`,
});
setDeleteDialogOpen(true);
}}
>
<Trash2 className="mr-2 h-4 w-4" />
<span>Delete Chat</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</CardHeader>
<CardFooter className="flex items-center justify-between gap-2 w-full">
<Badge variant="secondary" className="text-xs">
<Tag className="mr-1 h-3 w-3" />
{chat.type || "Unknown"}
</Badge>
<Button
size="sm"
onClick={() =>
router.push(
`/dashboard/${chat.search_space_id}/researcher/${chat.id}`
)
}
>
<MessageCircleMore className="h-4 w-4" />
<span>View Chat</span>
</Button>
</CardFooter>
</MotionCard>
))}
</div>
</AnimatePresence>
)}
<CardFooter className="flex items-center justify-between gap-2 w-full">
<Badge variant="secondary" className="text-xs">
<Tag className="mr-1 h-3 w-3" />
{chat.type || "Unknown"}
</Badge>
<Button
size="sm"
onClick={() =>
router.push(`/dashboard/${chat.search_space_id}/researcher/${chat.id}`)
}
>
<MessageCircleMore className="h-4 w-4" />
<span>View Chat</span>
</Button>
</CardFooter>
</MotionCard>
))}
</div>
</AnimatePresence>
)}
{/* Pagination */}
{!isFetchingChats && !fetchError && totalPages > 1 && (
<Pagination className="mt-8">
<PaginationContent>
<PaginationItem>
<PaginationPrevious
href={`?page=${Math.max(1, currentPage - 1)}`}
onClick={(e) => {
e.preventDefault();
if (currentPage > 1) setCurrentPage(currentPage - 1);
}}
className={
currentPage <= 1 ? "pointer-events-none opacity-50" : ""
}
/>
</PaginationItem>
{/* Pagination */}
{!isFetchingChats && !fetchError && totalPages > 1 && (
<Pagination className="mt-8">
<PaginationContent>
<PaginationItem>
<PaginationPrevious
href={`?page=${Math.max(1, currentPage - 1)}`}
onClick={(e) => {
e.preventDefault();
if (currentPage > 1) setCurrentPage(currentPage - 1);
}}
className={currentPage <= 1 ? "pointer-events-none opacity-50" : ""}
/>
</PaginationItem>
{Array.from({ length: totalPages }).map((_, index) => {
const pageNumber = index + 1;
const isVisible =
pageNumber === 1 ||
pageNumber === totalPages ||
(pageNumber >= currentPage - 1 &&
pageNumber <= currentPage + 1);
{Array.from({ length: totalPages }).map((_, index) => {
const pageNumber = index + 1;
const isVisible =
pageNumber === 1 ||
pageNumber === totalPages ||
(pageNumber >= currentPage - 1 && pageNumber <= currentPage + 1);
if (!isVisible) {
// Show ellipsis at appropriate positions
if (pageNumber === 2 || pageNumber === totalPages - 1) {
return (
<PaginationItem key={pageNumber}>
<span className="flex h-9 w-9 items-center justify-center">
...
</span>
</PaginationItem>
);
}
return null;
}
if (!isVisible) {
// Show ellipsis at appropriate positions
if (pageNumber === 2 || pageNumber === totalPages - 1) {
return (
<PaginationItem key={pageNumber}>
<span className="flex h-9 w-9 items-center justify-center">...</span>
</PaginationItem>
);
}
return null;
}
return (
<PaginationItem key={pageNumber}>
<PaginationLink
href={`?page=${pageNumber}`}
onClick={(e) => {
e.preventDefault();
setCurrentPage(pageNumber);
}}
isActive={pageNumber === currentPage}
>
{pageNumber}
</PaginationLink>
</PaginationItem>
);
})}
return (
<PaginationItem key={pageNumber}>
<PaginationLink
href={`?page=${pageNumber}`}
onClick={(e) => {
e.preventDefault();
setCurrentPage(pageNumber);
}}
isActive={pageNumber === currentPage}
>
{pageNumber}
</PaginationLink>
</PaginationItem>
);
})}
<PaginationItem>
<PaginationNext
href={`?page=${Math.min(totalPages, currentPage + 1)}`}
onClick={(e) => {
e.preventDefault();
if (currentPage < totalPages)
setCurrentPage(currentPage + 1);
}}
className={
currentPage >= totalPages
? "pointer-events-none opacity-50"
: ""
}
/>
</PaginationItem>
</PaginationContent>
</Pagination>
)}
</div>
<PaginationItem>
<PaginationNext
href={`?page=${Math.min(totalPages, currentPage + 1)}`}
onClick={(e) => {
e.preventDefault();
if (currentPage < totalPages) setCurrentPage(currentPage + 1);
}}
className={currentPage >= totalPages ? "pointer-events-none opacity-50" : ""}
/>
</PaginationItem>
</PaginationContent>
</Pagination>
)}
</div>
{/* Delete Confirmation Dialog */}
<Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Trash2 className="h-5 w-5 text-destructive" />
<span>Delete Chat</span>
</DialogTitle>
<DialogDescription>
Are you sure you want to delete{" "}
<span className="font-medium">{chatToDelete?.title}</span>? This
action cannot be undone.
</DialogDescription>
</DialogHeader>
<DialogFooter className="flex gap-2 sm:justify-end">
<Button
variant="outline"
onClick={() => setDeleteDialogOpen(false)}
disabled={isDeletingChat}
>
Cancel
</Button>
<Button
variant="destructive"
onClick={handleDeleteChat}
disabled={isDeletingChat}
className="gap-2"
>
{isDeletingChat ? (
<>
<span className="h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent" />
Deleting...
</>
) : (
<>
<Trash2 className="h-4 w-4" />
Delete
</>
)}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</motion.div>
);
{/* Delete Confirmation Dialog */}
<Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Trash2 className="h-5 w-5 text-destructive" />
<span>Delete Chat</span>
</DialogTitle>
<DialogDescription>
Are you sure you want to delete{" "}
<span className="font-medium">{chatToDelete?.title}</span>? This action cannot be
undone.
</DialogDescription>
</DialogHeader>
<DialogFooter className="flex gap-2 sm:justify-end">
<Button
variant="outline"
onClick={() => setDeleteDialogOpen(false)}
disabled={isDeletingChat}
>
Cancel
</Button>
<Button
variant="destructive"
onClick={handleDeleteChat}
disabled={isDeletingChat}
className="gap-2"
>
{isDeletingChat ? (
<>
<span className="h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent" />
Deleting...
</>
) : (
<>
<Trash2 className="h-4 w-4" />
Delete
</>
)}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</motion.div>
);
}

View file

@ -12,19 +12,9 @@ import { DashboardBreadcrumb } from "@/components/dashboard-breadcrumb";
import { LanguageSwitcher } from "@/components/LanguageSwitcher";
import { AppSidebarProvider } from "@/components/sidebar/AppSidebarProvider";
import { ThemeTogglerComponent } from "@/components/theme/theme-toggle";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Separator } from "@/components/ui/separator";
import {
SidebarInset,
SidebarProvider,
SidebarTrigger,
} from "@/components/ui/sidebar";
import { SidebarInset, SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar";
import { useLLMPreferences } from "@/hooks/use-llm-configs";
import { cn } from "@/lib/utils";
import { activeChatIdAtom } from "@/atoms/chats/chat-queries.atom";
@ -32,255 +22,249 @@ import { chatUIAtom } from "@/atoms/chats/chat-uis.atom";
import { activeSearchSpaceIdAtom } from "@/atoms/seach-spaces/seach-space-queries.atom";
export function DashboardClientLayout({
children,
searchSpaceId,
navSecondary,
navMain,
children,
searchSpaceId,
navSecondary,
navMain,
}: {
children: React.ReactNode;
searchSpaceId: string;
navSecondary: any[];
navMain: any[];
children: React.ReactNode;
searchSpaceId: string;
navSecondary: any[];
navMain: any[];
}) {
const t = useTranslations("dashboard");
const router = useRouter();
const pathname = usePathname();
const searchSpaceIdNum = Number(searchSpaceId);
const { search_space_id, chat_id } = useParams();
const [chatUIState, setChatUIState] = useAtom(chatUIAtom);
const activeChatId = useAtomValue(activeChatIdAtom);
const setActiveSearchSpaceIdState = useSetAtom(activeSearchSpaceIdAtom);
const setActiveChatIdState = useSetAtom(activeChatIdAtom);
const [showIndicator, setShowIndicator] = useState(false);
const t = useTranslations("dashboard");
const router = useRouter();
const pathname = usePathname();
const searchSpaceIdNum = Number(searchSpaceId);
const { search_space_id, chat_id } = useParams();
const [chatUIState, setChatUIState] = useAtom(chatUIAtom);
const activeChatId = useAtomValue(activeChatIdAtom);
const setActiveSearchSpaceIdState = useSetAtom(activeSearchSpaceIdAtom);
const setActiveChatIdState = useSetAtom(activeChatIdAtom);
const [showIndicator, setShowIndicator] = useState(false);
const { isChatPannelOpen } = chatUIState;
const { isChatPannelOpen } = chatUIState;
// Check if we're on the researcher page
const isResearcherPage = pathname?.includes("/researcher");
// Check if we're on the researcher page
const isResearcherPage = pathname?.includes("/researcher");
// Show indicator when chat becomes active and panel is closed
useEffect(() => {
if (activeChatId && !isChatPannelOpen) {
setShowIndicator(true);
// Hide indicator after 5 seconds
const timer = setTimeout(() => setShowIndicator(false), 5000);
return () => clearTimeout(timer);
} else {
setShowIndicator(false);
}
}, [activeChatId, isChatPannelOpen]);
// Show indicator when chat becomes active and panel is closed
useEffect(() => {
if (activeChatId && !isChatPannelOpen) {
setShowIndicator(true);
// Hide indicator after 5 seconds
const timer = setTimeout(() => setShowIndicator(false), 5000);
return () => clearTimeout(timer);
} else {
setShowIndicator(false);
}
}, [activeChatId, isChatPannelOpen]);
const { loading, error, isOnboardingComplete } =
useLLMPreferences(searchSpaceIdNum);
const [hasCheckedOnboarding, setHasCheckedOnboarding] = useState(false);
const { loading, error, isOnboardingComplete } = useLLMPreferences(searchSpaceIdNum);
const [hasCheckedOnboarding, setHasCheckedOnboarding] = useState(false);
// Skip onboarding check if we're already on the onboarding page
const isOnboardingPage = pathname?.includes("/onboard");
// Skip onboarding check if we're already on the onboarding page
const isOnboardingPage = pathname?.includes("/onboard");
// Translate navigation items
const tNavMenu = useTranslations("nav_menu");
const translatedNavMain = useMemo(() => {
return navMain.map((item) => ({
...item,
title: tNavMenu(item.title.toLowerCase().replace(/ /g, "_")),
items: item.items?.map((subItem: any) => ({
...subItem,
title: tNavMenu(subItem.title.toLowerCase().replace(/ /g, "_")),
})),
}));
}, [navMain, tNavMenu]);
// Translate navigation items
const tNavMenu = useTranslations("nav_menu");
const translatedNavMain = useMemo(() => {
return navMain.map((item) => ({
...item,
title: tNavMenu(item.title.toLowerCase().replace(/ /g, "_")),
items: item.items?.map((subItem: any) => ({
...subItem,
title: tNavMenu(subItem.title.toLowerCase().replace(/ /g, "_")),
})),
}));
}, [navMain, tNavMenu]);
const translatedNavSecondary = useMemo(() => {
return navSecondary.map((item) => ({
...item,
title:
item.title === "All Search Spaces"
? tNavMenu("all_search_spaces")
: item.title,
}));
}, [navSecondary, tNavMenu]);
const translatedNavSecondary = useMemo(() => {
return navSecondary.map((item) => ({
...item,
title: item.title === "All Search Spaces" ? tNavMenu("all_search_spaces") : item.title,
}));
}, [navSecondary, tNavMenu]);
const [open, setOpen] = useState<boolean>(() => {
try {
const match = document.cookie.match(/(?:^|; )sidebar_state=([^;]+)/);
if (match) return match[1] === "true";
} catch {
// ignore
}
return true;
});
const [open, setOpen] = useState<boolean>(() => {
try {
const match = document.cookie.match(/(?:^|; )sidebar_state=([^;]+)/);
if (match) return match[1] === "true";
} catch {
// ignore
}
return true;
});
useEffect(() => {
// Skip check if already on onboarding page
if (isOnboardingPage) {
setHasCheckedOnboarding(true);
return;
}
useEffect(() => {
// Skip check if already on onboarding page
if (isOnboardingPage) {
setHasCheckedOnboarding(true);
return;
}
// Only check once after preferences have loaded
if (!loading && !hasCheckedOnboarding) {
const onboardingComplete = isOnboardingComplete();
// Only check once after preferences have loaded
if (!loading && !hasCheckedOnboarding) {
const onboardingComplete = isOnboardingComplete();
if (!onboardingComplete) {
router.push(`/dashboard/${searchSpaceId}/onboard`);
}
if (!onboardingComplete) {
router.push(`/dashboard/${searchSpaceId}/onboard`);
}
setHasCheckedOnboarding(true);
}
}, [
loading,
isOnboardingComplete,
isOnboardingPage,
router,
searchSpaceId,
hasCheckedOnboarding,
]);
setHasCheckedOnboarding(true);
}
}, [
loading,
isOnboardingComplete,
isOnboardingPage,
router,
searchSpaceId,
hasCheckedOnboarding,
]);
// Synchronize active search space and chat IDs with URL
useEffect(() => {
const activeSeacrhSpaceId =
typeof search_space_id === "string"
? search_space_id
: Array.isArray(search_space_id) && search_space_id.length > 0
? search_space_id[0]
: "";
if (!activeSeacrhSpaceId) return;
setActiveSearchSpaceIdState(activeSeacrhSpaceId);
}, [search_space_id]);
// Synchronize active search space and chat IDs with URL
useEffect(() => {
const activeSeacrhSpaceId =
typeof search_space_id === "string"
? search_space_id
: Array.isArray(search_space_id) && search_space_id.length > 0
? search_space_id[0]
: "";
if (!activeSeacrhSpaceId) return;
setActiveSearchSpaceIdState(activeSeacrhSpaceId);
}, [search_space_id]);
useEffect(() => {
const activeChatId =
typeof chat_id === "string" ? chat_id : Array.isArray(chat_id) && chat_id.length > 0 ? chat_id[0] : "";
if (!activeChatId) return;
setActiveChatIdState(activeChatId);
}, [chat_id, search_space_id]);
useEffect(() => {
const activeChatId =
typeof chat_id === "string"
? chat_id
: Array.isArray(chat_id) && chat_id.length > 0
? chat_id[0]
: "";
if (!activeChatId) return;
setActiveChatIdState(activeChatId);
}, [chat_id, search_space_id]);
// Show loading screen while checking onboarding status (only on first load)
if (!hasCheckedOnboarding && loading && !isOnboardingPage) {
return (
<div className="flex flex-col items-center justify-center min-h-screen space-y-4">
<Card className="w-[350px] bg-background/60 backdrop-blur-sm">
<CardHeader className="pb-2">
<CardTitle className="text-xl font-medium">
{t("loading_config")}
</CardTitle>
<CardDescription>{t("checking_llm_prefs")}</CardDescription>
</CardHeader>
<CardContent className="flex justify-center py-6">
<Loader2 className="h-12 w-12 text-primary animate-spin" />
</CardContent>
</Card>
</div>
);
}
// Show loading screen while checking onboarding status (only on first load)
if (!hasCheckedOnboarding && loading && !isOnboardingPage) {
return (
<div className="flex flex-col items-center justify-center min-h-screen space-y-4">
<Card className="w-[350px] bg-background/60 backdrop-blur-sm">
<CardHeader className="pb-2">
<CardTitle className="text-xl font-medium">{t("loading_config")}</CardTitle>
<CardDescription>{t("checking_llm_prefs")}</CardDescription>
</CardHeader>
<CardContent className="flex justify-center py-6">
<Loader2 className="h-12 w-12 text-primary animate-spin" />
</CardContent>
</Card>
</div>
);
}
// Show error screen if there's an error loading preferences (but not on onboarding page)
if (error && !hasCheckedOnboarding && !isOnboardingPage) {
return (
<div className="flex flex-col items-center justify-center min-h-screen space-y-4">
<Card className="w-[400px] bg-background/60 backdrop-blur-sm border-destructive/20">
<CardHeader className="pb-2">
<CardTitle className="text-xl font-medium text-destructive">
{t("config_error")}
</CardTitle>
<CardDescription>{t("failed_load_llm_config")}</CardDescription>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground">{error}</p>
</CardContent>
</Card>
</div>
);
}
// Show error screen if there's an error loading preferences (but not on onboarding page)
if (error && !hasCheckedOnboarding && !isOnboardingPage) {
return (
<div className="flex flex-col items-center justify-center min-h-screen space-y-4">
<Card className="w-[400px] bg-background/60 backdrop-blur-sm border-destructive/20">
<CardHeader className="pb-2">
<CardTitle className="text-xl font-medium text-destructive">
{t("config_error")}
</CardTitle>
<CardDescription>{t("failed_load_llm_config")}</CardDescription>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground">{error}</p>
</CardContent>
</Card>
</div>
);
}
return (
<SidebarProvider
className="h-full bg-red-600 overflow-hidden"
open={open}
onOpenChange={setOpen}
>
{/* Use AppSidebarProvider which fetches user, search space, and recent chats */}
<AppSidebarProvider
searchSpaceId={searchSpaceId}
navSecondary={translatedNavSecondary}
navMain={translatedNavMain}
/>
<SidebarInset className="h-full ">
<main className="flex h-full">
<div className="flex grow flex-col h-full border-r">
<header className="sticky top-0 z-50 flex h-16 shrink-0 items-center gap-2 bg-background/95 backdrop-blur supports-backdrop-filter:bg-background/60 border-b">
<div className="flex items-center justify-between w-full gap-2 px-4">
<div className="flex items-center gap-2">
<SidebarTrigger className="-ml-1" />
<Separator orientation="vertical" className="h-6" />
<DashboardBreadcrumb />
</div>
<div className="flex items-center gap-2">
<LanguageSwitcher />
<ThemeTogglerComponent />
{/* Only show artifacts toggle on researcher page */}
{isResearcherPage && (
<motion.div
className="relative"
animate={
showIndicator
? {
scale: [1, 1.05, 1],
}
: {}
}
transition={{
duration: 2,
repeat: showIndicator ? Number.POSITIVE_INFINITY : 0,
ease: "easeInOut",
}}
>
<motion.button
type="button"
onClick={() => {
setChatUIState((prev) => ({
...prev,
isChatPannelOpen: !isChatPannelOpen,
}));
setShowIndicator(false);
}}
className={cn(
"shrink-0 rounded-full p-2 transition-all duration-300 relative",
showIndicator
? "bg-primary/20 hover:bg-primary/30 shadow-lg shadow-primary/25"
: "hover:bg-muted",
activeChatId &&
!showIndicator &&
"hover:bg-primary/10"
)}
title="Toggle Artifacts Panel"
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
<motion.div
animate={
showIndicator
? {
rotate: [0, -10, 10, -10, 0],
}
: {}
}
transition={{
duration: 0.5,
repeat: showIndicator
? Number.POSITIVE_INFINITY
: 0,
repeatDelay: 2,
}}
>
<PanelRight
className={cn(
"h-4 w-4 transition-colors",
showIndicator && "text-primary"
)}
/>
</motion.div>
</motion.button>
return (
<SidebarProvider
className="h-full bg-red-600 overflow-hidden"
open={open}
onOpenChange={setOpen}
>
{/* Use AppSidebarProvider which fetches user, search space, and recent chats */}
<AppSidebarProvider
searchSpaceId={searchSpaceId}
navSecondary={translatedNavSecondary}
navMain={translatedNavMain}
/>
<SidebarInset className="h-full ">
<main className="flex h-full">
<div className="flex grow flex-col h-full border-r">
<header className="sticky top-0 z-50 flex h-16 shrink-0 items-center gap-2 bg-background/95 backdrop-blur supports-backdrop-filter:bg-background/60 border-b">
<div className="flex items-center justify-between w-full gap-2 px-4">
<div className="flex items-center gap-2">
<SidebarTrigger className="-ml-1" />
<Separator orientation="vertical" className="h-6" />
<DashboardBreadcrumb />
</div>
<div className="flex items-center gap-2">
<LanguageSwitcher />
<ThemeTogglerComponent />
{/* Only show artifacts toggle on researcher page */}
{isResearcherPage && (
<motion.div
className="relative"
animate={
showIndicator
? {
scale: [1, 1.05, 1],
}
: {}
}
transition={{
duration: 2,
repeat: showIndicator ? Number.POSITIVE_INFINITY : 0,
ease: "easeInOut",
}}
>
<motion.button
type="button"
onClick={() => {
setChatUIState((prev) => ({
...prev,
isChatPannelOpen: !isChatPannelOpen,
}));
setShowIndicator(false);
}}
className={cn(
"shrink-0 rounded-full p-2 transition-all duration-300 relative",
showIndicator
? "bg-primary/20 hover:bg-primary/30 shadow-lg shadow-primary/25"
: "hover:bg-muted",
activeChatId && !showIndicator && "hover:bg-primary/10"
)}
title="Toggle Artifacts Panel"
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
<motion.div
animate={
showIndicator
? {
rotate: [0, -10, 10, -10, 0],
}
: {}
}
transition={{
duration: 0.5,
repeat: showIndicator ? Number.POSITIVE_INFINITY : 0,
repeatDelay: 2,
}}
>
<PanelRight
className={cn(
"h-4 w-4 transition-colors",
showIndicator && "text-primary"
)}
/>
</motion.div>
</motion.button>
{/* Pulsing indicator badge */}
<AnimatePresence>

View file

@ -7,28 +7,31 @@ import { toast } from "sonner";
import { Chat } from "@/app/dashboard/[search_space_id]/chats/chats-client";
export const deleteChatMutationAtom = atomWithMutation((get) => {
const searchSpaceId = get(activeSearchSpaceIdAtom);
const authToken = localStorage.getItem("surfsense_bearer_token");
const searchSpaceId = get(activeSearchSpaceIdAtom);
const authToken = localStorage.getItem("surfsense_bearer_token");
return {
mutationKey: cacheKeys.activeSearchSpace.chats(searchSpaceId ?? ""),
enabled: !!searchSpaceId && !!authToken,
mutationFn: async (chatId: number) => {
if (!authToken) {
throw new Error("No authentication token found");
}
if (!searchSpaceId) {
throw new Error("No search space id found");
}
return {
mutationKey: cacheKeys.activeSearchSpace.chats(searchSpaceId ?? ""),
enabled: !!searchSpaceId && !!authToken,
mutationFn: async (chatId: number) => {
if (!authToken) {
throw new Error("No authentication token found");
}
if (!searchSpaceId) {
throw new Error("No search space id found");
}
return deleteChat(chatId, authToken);
},
return deleteChat(chatId, authToken);
},
onSuccess: (_, chatId) => {
toast.success("Chat deleted successfully");
queryClient.setQueryData(cacheKeys.activeSearchSpace.chats(searchSpaceId!), (oldData: Chat[]) => {
return oldData.filter((chat) => chat.id !== chatId);
});
},
};
onSuccess: (_, chatId) => {
toast.success("Chat deleted successfully");
queryClient.setQueryData(
cacheKeys.activeSearchSpace.chats(searchSpaceId!),
(oldData: Chat[]) => {
return oldData.filter((chat) => chat.id !== chatId);
}
);
},
};
});

View file

@ -2,63 +2,60 @@ import { atom } from "jotai";
import { atomWithQuery } from "jotai-tanstack-query";
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 {
fetchChatDetails,
fetchChatsBySearchSpace,
} from "@/lib/apis/chats.api";
import { fetchChatDetails, fetchChatsBySearchSpace } from "@/lib/apis/chats.api";
import { getPodcastByChatId } from "@/lib/apis/podcasts.api";
import { cacheKeys } from "@/lib/query-client/cache-keys";
import { activeSearchSpaceIdAtom } from "@/atoms/seach-spaces/seach-space-queries.atom";
type ActiveChatState = {
chatId: string | null;
chatDetails: ChatDetails | null;
podcast: PodcastItem | null;
chatId: string | null;
chatDetails: ChatDetails | null;
podcast: PodcastItem | null;
};
export const activeChatIdAtom = atom<string | null>(null);
export const activeChatAtom = atomWithQuery<ActiveChatState>((get) => {
const activeChatId = get(activeChatIdAtom);
const authToken = localStorage.getItem("surfsense_bearer_token");
const activeChatId = get(activeChatIdAtom);
const authToken = localStorage.getItem("surfsense_bearer_token");
return {
queryKey: cacheKeys.activeSearchSpace.activeChat(activeChatId ?? ""),
enabled: !!activeChatId && !!authToken,
queryFn: async () => {
if (!authToken) {
throw new Error("No authentication token found");
}
if (!activeChatId) {
throw new Error("No active chat id found");
}
return {
queryKey: cacheKeys.activeSearchSpace.activeChat(activeChatId ?? ""),
enabled: !!activeChatId && !!authToken,
queryFn: async () => {
if (!authToken) {
throw new Error("No authentication token found");
}
if (!activeChatId) {
throw new Error("No active chat id found");
}
const [podcast, chatDetails] = await Promise.all([
getPodcastByChatId(activeChatId, authToken),
fetchChatDetails(activeChatId, authToken),
]);
const [podcast, chatDetails] = await Promise.all([
getPodcastByChatId(activeChatId, authToken),
fetchChatDetails(activeChatId, authToken),
]);
return { chatId: activeChatId, chatDetails, podcast };
},
};
return { chatId: activeChatId, chatDetails, podcast };
},
};
});
export const activeSearchSpaceChatsAtom = atomWithQuery((get) => {
const searchSpaceId = get(activeSearchSpaceIdAtom);
const authToken = localStorage.getItem("surfsense_bearer_token");
const searchSpaceId = get(activeSearchSpaceIdAtom);
const authToken = localStorage.getItem("surfsense_bearer_token");
return {
queryKey: cacheKeys.activeSearchSpace.chats(searchSpaceId ?? ""),
enabled: !!searchSpaceId && !!authToken,
queryFn: async () => {
if (!authToken) {
throw new Error("No authentication token found");
}
if (!searchSpaceId) {
throw new Error("No search space id found");
}
return {
queryKey: cacheKeys.activeSearchSpace.chats(searchSpaceId ?? ""),
enabled: !!searchSpaceId && !!authToken,
queryFn: async () => {
if (!authToken) {
throw new Error("No authentication token found");
}
if (!searchSpaceId) {
throw new Error("No search space id found");
}
return fetchChatsBySearchSpace(searchSpaceId, authToken);
},
};
return fetchChatsBySearchSpace(searchSpaceId, authToken);
},
};
});

View file

@ -1,3 +1,3 @@
import { atom } from "jotai";
export const activeSearchSpaceIdAtom = atom<string | null>(null);
export const activeSearchSpaceIdAtom = atom<string | null>(null);

View file

@ -1,38 +1,35 @@
"use client";
import {
type ChatHandler,
ChatSection as LlamaIndexChatSection,
} from "@llamaindex/chat-ui";
import { type ChatHandler, ChatSection as LlamaIndexChatSection } from "@llamaindex/chat-ui";
import { useParams } from "next/navigation";
import { ChatInputUI } from "@/components/chat/ChatInputGroup";
import { ChatMessagesUI } from "@/components/chat/ChatMessages";
import type { Document } from "@/hooks/use-documents";
interface ChatInterfaceProps {
handler: ChatHandler;
onDocumentSelectionChange?: (documents: Document[]) => void;
selectedDocuments?: Document[];
onConnectorSelectionChange?: (connectorTypes: string[]) => void;
selectedConnectors?: string[];
searchMode?: "DOCUMENTS" | "CHUNKS";
onSearchModeChange?: (mode: "DOCUMENTS" | "CHUNKS") => void;
topK?: number;
onTopKChange?: (topK: number) => void;
handler: ChatHandler;
onDocumentSelectionChange?: (documents: Document[]) => void;
selectedDocuments?: Document[];
onConnectorSelectionChange?: (connectorTypes: string[]) => void;
selectedConnectors?: string[];
searchMode?: "DOCUMENTS" | "CHUNKS";
onSearchModeChange?: (mode: "DOCUMENTS" | "CHUNKS") => void;
topK?: number;
onTopKChange?: (topK: number) => void;
}
export default function ChatInterface({
handler,
onDocumentSelectionChange,
selectedDocuments = [],
onConnectorSelectionChange,
selectedConnectors = [],
searchMode,
onSearchModeChange,
topK = 10,
onTopKChange,
handler,
onDocumentSelectionChange,
selectedDocuments = [],
onConnectorSelectionChange,
selectedConnectors = [],
searchMode,
onSearchModeChange,
topK = 10,
onTopKChange,
}: ChatInterfaceProps) {
const { chat_id, search_space_id } = useParams();
const { chat_id, search_space_id } = useParams();
return (
<LlamaIndexChatSection handler={handler} className="flex h-full max-w-7xl mx-auto">

View file

@ -1,32 +1,32 @@
import { z } from "zod";
export const loginRequest = z.object({
email: z.string().email(),
password: z.string().min(1),
grant_type: z.string().optional(),
email: z.string().email(),
password: z.string().min(1),
grant_type: z.string().optional(),
});
export const loginResponse = z.object({
access_token: z.string(),
token_type: z.string(),
access_token: z.string(),
token_type: z.string(),
});
export const registerRequest = z.object({
email: z.string().email(),
password: z.string().min(1),
is_active: z.boolean().optional(),
is_superuser: z.boolean().optional(),
is_verified: z.boolean().optional(),
email: z.string().email(),
password: z.string().min(1),
is_active: z.boolean().optional(),
is_superuser: z.boolean().optional(),
is_verified: z.boolean().optional(),
});
export const registerResponse = z.object({
id: z.number(),
email: z.string().email(),
is_active: z.boolean(),
is_superuser: z.boolean(),
is_verified: z.boolean(),
pages_limit: z.number(),
pages_used: z.number(),
id: z.number(),
email: z.string().email(),
is_active: z.boolean(),
is_superuser: z.boolean(),
is_verified: z.boolean(),
pages_limit: z.number(),
pages_used: z.number(),
});
export type LoginRequest = z.infer<typeof loginRequest>;

View file

@ -5,43 +5,45 @@ import { paginationQueryParams } from ".";
export const chatTypeEnum = z.enum(["QNA"]);
export const chatSummary = z.object({
created_at: z.string(),
id: z.number(),
type: chatTypeEnum,
title: z.string(),
search_space_id: z.number(),
state_version: z.number(),
created_at: z.string(),
id: z.number(),
type: chatTypeEnum,
title: z.string(),
search_space_id: z.number(),
state_version: z.number(),
});
export const chatDetails = chatSummary.extend({
initial_connectors: z.array(z.string()),
messages: z.array(z.any()),
initial_connectors: z.array(z.string()),
messages: z.array(z.any()),
});
export const getChatDetailsRequest = chatSummary.pick({ id: true });
export const getChatsBySearchSpaceRequest = chatSummary.pick({
search_space_id: true,
}).merge(paginationQueryParams);
export const getChatsBySearchSpaceRequest = chatSummary
.pick({
search_space_id: true,
})
.merge(paginationQueryParams);
export const deleteChatResponse = z.object({
message: z.literal("Chat deleted successfully"),
message: z.literal("Chat deleted successfully"),
});
export const deleteChatRequest = chatSummary.pick({ id: true });
export const createChatRequest = chatDetails
.omit({ created_at: true, id: true, state_version: true });
export const createChatRequest = chatDetails.omit({
created_at: true,
id: true,
state_version: true,
});
export const updateChatRequest = chatDetails
.omit({ created_at: true, state_version: true });
export const updateChatRequest = chatDetails.omit({ created_at: true, state_version: true });
export type ChatSummary = z.infer<typeof chatSummary>;
export type ChatDetails = z.infer<typeof chatDetails> & { messages: Message[] };
export type GetChatDetailsRequest = z.infer<typeof getChatDetailsRequest>;
export type GetChatsBySearchSpaceRequest = z.infer<
typeof getChatsBySearchSpaceRequest
>;
export type GetChatsBySearchSpaceRequest = z.infer<typeof getChatsBySearchSpaceRequest>;
export type DeleteChatResponse = z.infer<typeof deleteChatResponse>;
export type DeleteChatRequest = z.infer<typeof deleteChatRequest>;
export type CreateChatRequest = z.infer<typeof createChatRequest>;

View file

@ -1,8 +1,8 @@
import { z } from "zod";
export const paginationQueryParams = z.object({
limit: z.number().optional(),
skip: z.number().optional(),
limit: z.number().optional(),
skip: z.number().optional(),
});
export type PaginationQueryParams = z.infer<typeof paginationQueryParams>;

View file

@ -1,44 +1,35 @@
import {
loginRequest,
LoginRequest,
loginResponse,
registerRequest,
RegisterRequest,
registerResponse,
loginRequest,
LoginRequest,
loginResponse,
registerRequest,
RegisterRequest,
registerResponse,
} from "@/contracts/types/auth.types";
import { baseApiService } from "./base-api.service";
export class AuthApiService {
login = async (request: LoginRequest) => {
// Validate the request
const parsedRequest = loginRequest.safeParse(request);
login = async (request: LoginRequest) => {
// Validate the request
const parsedRequest = loginRequest.safeParse(request);
if (!parsedRequest.success) {
throw new Error(`Invalid request: ${parsedRequest.error.message}`);
}
if (!parsedRequest.success) {
throw new Error(`Invalid request: ${parsedRequest.error.message}`);
}
return baseApiService.post(
`/auth/jwt/login`,
parsedRequest.data,
loginResponse,
{
contentType: "application/x-www-form-urlencoded",
}
);
};
return baseApiService.post(`/auth/jwt/login`, parsedRequest.data, loginResponse, {
contentType: "application/x-www-form-urlencoded",
});
};
register = async (request: RegisterRequest) => {
// Validate the request
const parsedRequest = registerRequest.safeParse(request);
register = async (request: RegisterRequest) => {
// Validate the request
const parsedRequest = registerRequest.safeParse(request);
if (!parsedRequest.success) {
throw new Error(`Invalid request: ${parsedRequest.error.message}`);
}
if (!parsedRequest.success) {
throw new Error(`Invalid request: ${parsedRequest.error.message}`);
}
return baseApiService.post(
`/auth/register`,
parsedRequest.data,
registerResponse
);
};
return baseApiService.post(`/auth/register`, parsedRequest.data, registerResponse);
};
}

View file

@ -1,179 +1,163 @@
import z from "zod";
import {
AppError,
AuthenticationError,
AuthorizationError,
ValidationError,
} from "../error";
import { AppError, AuthenticationError, AuthorizationError, ValidationError } from "../error";
export type RequestOptions = {
method: "GET" | "POST" | "PUT" | "DELETE";
headers?: Record<string, string>;
contentType?: "application/json" | "application/x-www-form-urlencoded";
signal?: AbortSignal;
body?: any;
// Add more options as needed
method: "GET" | "POST" | "PUT" | "DELETE";
headers?: Record<string, string>;
contentType?: "application/json" | "application/x-www-form-urlencoded";
signal?: AbortSignal;
body?: any;
// Add more options as needed
};
export class BaseApiService {
bearerToken: string;
baseUrl: string;
bearerToken: string;
baseUrl: string;
constructor(bearerToken: string, baseUrl: string) {
this.bearerToken = bearerToken;
this.baseUrl = baseUrl;
}
constructor(bearerToken: string, baseUrl: string) {
this.bearerToken = bearerToken;
this.baseUrl = baseUrl;
}
setBearerToken(bearerToken: string) {
this.bearerToken = bearerToken;
}
setBearerToken(bearerToken: string) {
this.bearerToken = bearerToken;
}
async request<T>(
url: string,
body?: any,
responseSchema?: z.ZodSchema<T>,
options?: RequestOptions
) : Promise<T> {
const defaultOptions: RequestOptions = {
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${this.bearerToken}`,
},
method: "GET",
};
async request<T>(
url: string,
body?: any,
responseSchema?: z.ZodSchema<T>,
options?: RequestOptions
): Promise<T> {
const defaultOptions: RequestOptions = {
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${this.bearerToken}`,
},
method: "GET",
};
const mergedOptions: RequestOptions = {
...defaultOptions,
...(options ?? {}),
headers: {
...defaultOptions.headers,
...(options?.headers ?? {}),
},
};
const mergedOptions: RequestOptions = {
...defaultOptions,
...(options ?? {}),
headers: {
...defaultOptions.headers,
...(options?.headers ?? {}),
},
};
let requestBody;
let requestBody;
// Serialize body
if (body) {
if (
mergedOptions.headers?.["Content-Type"].toLocaleLowerCase() ===
"application/json"
) {
requestBody = JSON.stringify(body);
}
// Serialize body
if (body) {
if (mergedOptions.headers?.["Content-Type"].toLocaleLowerCase() === "application/json") {
requestBody = JSON.stringify(body);
}
if (
mergedOptions.headers?.["Content-Type"].toLocaleLowerCase() ===
"application/x-www-form-urlencoded"
) {
requestBody = new URLSearchParams(body);
}
if (
mergedOptions.headers?.["Content-Type"].toLocaleLowerCase() ===
"application/x-www-form-urlencoded"
) {
requestBody = new URLSearchParams(body);
}
mergedOptions.body = requestBody;
}
mergedOptions.body = requestBody;
}
if (!this.baseUrl) {
throw new AppError("Base URL is not set.");
}
if (!this.baseUrl) {
throw new AppError("Base URL is not set.");
}
if (!this.bearerToken) {
throw new AuthenticationError(
"You are not authenticated. Please login again."
);
}
if (!this.bearerToken) {
throw new AuthenticationError("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.status === 401) {
throw new AuthenticationError(
"You are not authenticated. Please login again."
);
}
if (!response.ok) {
if (response.status === 401) {
throw new AuthenticationError("You are not authenticated. Please login again.");
}
if (response.status === 403) {
throw new AuthorizationError(
"You don't have permission to access this resource."
);
}
if (response.status === 403) {
throw new AuthorizationError("You don't have permission to access this resource.");
}
throw new AppError(`API Error: ${response.statusText}`);
}
throw new AppError(`API Error: ${response.statusText}`);
}
let data;
let data;
try {
data = await response.json();
} catch (error) {
throw new AppError(`Failed to parse response as JSON: ${error}`);
}
try {
data = await response.json();
} catch (error) {
throw new AppError(`Failed to parse response as JSON: ${error}`);
}
if (!responseSchema) {
return data;
}
if (!responseSchema) {
return data;
}
const parsedData = responseSchema.safeParse(data);
const parsedData = responseSchema.safeParse(data);
if (!parsedData.success) {
throw new ValidationError(
`Invalid response: ${parsedData.error.message}`
);
}
if (!parsedData.success) {
throw new ValidationError(`Invalid response: ${parsedData.error.message}`);
}
return parsedData.data;
}
return parsedData.data;
}
async get<T>(
url: string,
responseSchema?: z.ZodSchema<T>,
options?: Omit<RequestOptions, "method">
) {
return this.request(url, undefined, responseSchema, {
...options,
method: "GET",
});
}
async get<T>(
url: string,
responseSchema?: z.ZodSchema<T>,
options?: Omit<RequestOptions, "method">
) {
return this.request(url, undefined, responseSchema, {
...options,
method: "GET",
});
}
async post<T>(
url: string,
body?: any,
responseSchema?: z.ZodSchema<T>,
options?: Omit<RequestOptions, "method">
) {
return this.request(url, body, responseSchema, {
...options,
method: "POST",
});
}
async post<T>(
url: string,
body?: any,
responseSchema?: z.ZodSchema<T>,
options?: Omit<RequestOptions, "method">
) {
return this.request(url, body, responseSchema, {
...options,
method: "POST",
});
}
async put<T>(
url: string,
body?: any,
responseSchema?: z.ZodSchema<T>,
options?: Omit<RequestOptions, "method">
) {
return this.request(url, body, responseSchema, {
...options,
method: "PUT",
});
}
async put<T>(
url: string,
body?: any,
responseSchema?: z.ZodSchema<T>,
options?: Omit<RequestOptions, "method">
) {
return this.request(url, body, responseSchema, {
...options,
method: "PUT",
});
}
async delete<T>(
url: string,
body?: any,
responseSchema?: z.ZodSchema<T>,
options?: Omit<RequestOptions, "method">
) {
return this.request(url, body, responseSchema, {
...options,
method: "DELETE",
});
}
async delete<T>(
url: string,
body?: any,
responseSchema?: z.ZodSchema<T>,
options?: Omit<RequestOptions, "method">
) {
return this.request(url, body, responseSchema, {
...options,
method: "DELETE",
});
}
}
export const baseApiService = new BaseApiService(
typeof window !== "undefined" ? localStorage.getItem("surfsense_bearer_token") || "" : "",
process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL || ""
typeof window !== "undefined" ? localStorage.getItem("surfsense_bearer_token") || "" : "",
process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL || ""
);

View file

@ -1,114 +1,103 @@
import { ResearchMode } from "@/components/chat/types";
import { Message } from "@ai-sdk/react";
import {
chatDetails,
chatSummary,
createChatRequest,
CreateChatRequest,
deleteChatRequest,
DeleteChatRequest,
getChatDetailsRequest,
GetChatDetailsRequest,
getChatsBySearchSpaceRequest,
GetChatsBySearchSpaceRequest,
deleteChatResponse,
UpdateChatRequest,
updateChatRequest,
chatDetails,
chatSummary,
createChatRequest,
CreateChatRequest,
deleteChatRequest,
DeleteChatRequest,
getChatDetailsRequest,
GetChatDetailsRequest,
getChatsBySearchSpaceRequest,
GetChatsBySearchSpaceRequest,
deleteChatResponse,
UpdateChatRequest,
updateChatRequest,
} from "@/contracts/types/chat.types";
import { z } from "zod";
import { baseApiService } from "./base-api.service";
export class ChatApiService {
fetchChatDetails = async (
request: GetChatDetailsRequest
) => {
// Validate the request
const parsedRequest = getChatDetailsRequest.safeParse(request);
fetchChatDetails = async (request: GetChatDetailsRequest) => {
// Validate the request
const parsedRequest = getChatDetailsRequest.safeParse(request);
if (!parsedRequest.success) {
throw new Error(`Invalid request: ${parsedRequest.error.message}`);
}
if (!parsedRequest.success) {
throw new Error(`Invalid request: ${parsedRequest.error.message}`);
}
return baseApiService.get(`/api/v1/chats/${request.id}`, chatDetails);
};
return baseApiService.get(`/api/v1/chats/${request.id}`, chatDetails);
};
fetchChatsBySearchSpace = async (
request: GetChatsBySearchSpaceRequest
)=> {
// Validate the request
const parsedRequest = getChatsBySearchSpaceRequest.safeParse(request);
fetchChatsBySearchSpace = async (request: GetChatsBySearchSpaceRequest) => {
// Validate the request
const parsedRequest = getChatsBySearchSpaceRequest.safeParse(request);
if (!parsedRequest.success) {
throw new Error(`Invalid request: ${parsedRequest.error.message}`);
}
if (!parsedRequest.success) {
throw new Error(`Invalid request: ${parsedRequest.error.message}`);
}
return baseApiService.get(
`/api/v1/chats?search_space_id=${request.search_space_id}`,
z.array(chatSummary)
);
};
return baseApiService.get(
`/api/v1/chats?search_space_id=${request.search_space_id}`,
z.array(chatSummary)
);
};
deleteChat = async (request: DeleteChatRequest) => {
// Validate the request
const parsedRequest = deleteChatRequest.safeParse(request);
deleteChat = async (request: DeleteChatRequest) => {
// Validate the request
const parsedRequest = deleteChatRequest.safeParse(request);
if (!parsedRequest.success) {
throw new Error(`Invalid request: ${parsedRequest.error.message}`);
}
return baseApiService.delete(`/api/v1/chats/${request.id}`, undefined, deleteChatResponse);
};
if (!parsedRequest.success) {
throw new Error(`Invalid request: ${parsedRequest.error.message}`);
}
createChat = async (
request: CreateChatRequest
) => {
// Validate the request
const parsedRequest = createChatRequest.safeParse(request);
return baseApiService.delete(`/api/v1/chats/${request.id}`, undefined, deleteChatResponse);
};
if (!parsedRequest.success) {
throw new Error(`Invalid request: ${parsedRequest.error.message}`);
}
createChat = async (request: CreateChatRequest) => {
// Validate the request
const parsedRequest = createChatRequest.safeParse(request);
const { type, title, initial_connectors, messages, search_space_id } =
parsedRequest.data;
if (!parsedRequest.success) {
throw new Error(`Invalid request: ${parsedRequest.error.message}`);
}
return baseApiService.post(
`/api/v1/chats`,
{
type,
title,
initial_connectors,
messages,
search_space_id,
},
chatSummary
);
};
const { type, title, initial_connectors, messages, search_space_id } = parsedRequest.data;
updateChat = async (
request: UpdateChatRequest
) => {
// Validate the request
const parsedRequest = updateChatRequest.safeParse(request);
return baseApiService.post(
`/api/v1/chats`,
{
type,
title,
initial_connectors,
messages,
search_space_id,
},
chatSummary
);
};
if (!parsedRequest.success) {
throw new Error(`Invalid request: ${parsedRequest.error.message}`);
}
updateChat = async (request: UpdateChatRequest) => {
// Validate the request
const parsedRequest = updateChatRequest.safeParse(request);
const { type, title, initial_connectors, messages, search_space_id, id } =
parsedRequest.data;
if (!parsedRequest.success) {
throw new Error(`Invalid request: ${parsedRequest.error.message}`);
}
return baseApiService.put(
`/api/v1/chats/${id}`,
{
type,
title,
initial_connectors,
messages,
search_space_id,
},
chatSummary
);
};
const { type, title, initial_connectors, messages, search_space_id, id } = parsedRequest.data;
return baseApiService.put(
`/api/v1/chats/${id}`,
{
type,
title,
initial_connectors,
messages,
search_space_id,
},
chatSummary
);
};
}

View file

@ -1,167 +1,157 @@
import type {
Chat,
ChatDetails,
} from "@/app/dashboard/[search_space_id]/chats/chats-client";
import type { Chat, ChatDetails } from "@/app/dashboard/[search_space_id]/chats/chats-client";
import { ResearchMode } from "@/components/chat/types";
import { Message } from "@ai-sdk/react";
export const fetchChatDetails = async (
chatId: string,
authToken: string
chatId: string,
authToken: string
): Promise<ChatDetails | null> => {
const response = await fetch(
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/chats/${Number(
chatId
)}`,
{
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${authToken}`,
},
}
);
const response = await fetch(
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/chats/${Number(chatId)}`,
{
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${authToken}`,
},
}
);
if (!response.ok) {
throw new Error(`Failed to fetch chat details: ${response.statusText}`);
}
if (!response.ok) {
throw new Error(`Failed to fetch chat details: ${response.statusText}`);
}
return await response.json();
return await response.json();
};
export const fetchChatsBySearchSpace = async (
searchSpaceId: string,
authToken: string
searchSpaceId: string,
authToken: string
): Promise<ChatDetails[] | null> => {
const response = await fetch(
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/chats?search_space_id=${searchSpaceId}`,
{
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${authToken}`,
},
}
);
if (!response.ok) {
throw new Error(`Failed to fetch chats: ${response.statusText}`);
}
const response = await fetch(
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/chats?search_space_id=${searchSpaceId}`,
{
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${authToken}`,
},
}
);
if (!response.ok) {
throw new Error(`Failed to fetch chats: ${response.statusText}`);
}
return await response.json();
return await response.json();
};
export const deleteChat = async (chatId: number, authToken: string) => {
const response = await fetch(
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/chats/${chatId}`,
{
method: "DELETE",
headers: {
Authorization: `Bearer ${authToken}`,
},
}
);
const response = await fetch(
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/chats/${chatId}`,
{
method: "DELETE",
headers: {
Authorization: `Bearer ${authToken}`,
},
}
);
if (!response.ok) {
throw new Error(`Failed to delete chat: ${response.statusText}`);
}
if (!response.ok) {
throw new Error(`Failed to delete chat: ${response.statusText}`);
}
return await response.json();
return await response.json();
};
export const createChat = async (
initialMessage: string,
researchMode: ResearchMode,
selectedConnectors: string[],
authToken: string,
searchSpaceId: number
initialMessage: string,
researchMode: ResearchMode,
selectedConnectors: string[],
authToken: string,
searchSpaceId: number
): Promise<Chat | null> => {
const response = await fetch(
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/chats`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${authToken}`,
},
body: JSON.stringify({
type: researchMode,
title: "Untitled Chat",
initial_connectors: selectedConnectors,
messages: [
{
role: "user",
content: initialMessage,
},
],
search_space_id: searchSpaceId,
}),
}
);
const response = await fetch(`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/chats`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${authToken}`,
},
body: JSON.stringify({
type: researchMode,
title: "Untitled Chat",
initial_connectors: selectedConnectors,
messages: [
{
role: "user",
content: initialMessage,
},
],
search_space_id: searchSpaceId,
}),
});
if (!response.ok) {
throw new Error(`Failed to create chat: ${response.statusText}`);
}
if (!response.ok) {
throw new Error(`Failed to create chat: ${response.statusText}`);
}
return await response.json();
return await response.json();
};
export const updateChat = async (
chatId: string,
messages: Message[],
researchMode: ResearchMode,
selectedConnectors: string[],
authToken: string,
searchSpaceId: number
chatId: string,
messages: Message[],
researchMode: ResearchMode,
selectedConnectors: string[],
authToken: string,
searchSpaceId: number
) => {
const userMessages = messages.filter((msg) => msg.role === "user");
if (userMessages.length === 0) return;
const userMessages = messages.filter((msg) => msg.role === "user");
if (userMessages.length === 0) return;
const title = userMessages[0].content;
const title = userMessages[0].content;
const response = await fetch(
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/chats/${Number(
chatId
)}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${authToken}`,
},
body: JSON.stringify({
type: researchMode,
title: title,
initial_connectors: selectedConnectors,
messages: messages,
search_space_id: searchSpaceId,
}),
}
);
const response = await fetch(
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/chats/${Number(chatId)}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${authToken}`,
},
body: JSON.stringify({
type: researchMode,
title: title,
initial_connectors: selectedConnectors,
messages: messages,
search_space_id: searchSpaceId,
}),
}
);
if (!response.ok) {
throw new Error(`Failed to update chat: ${response.statusText}`);
}
if (!response.ok) {
throw new Error(`Failed to update chat: ${response.statusText}`);
}
};
export const fetchChats = async (
searchSpaceId: string,
limit: number,
skip: number,
authToken: string
searchSpaceId: string,
limit: number,
skip: number,
authToken: string
) => {
const response = await fetch(
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/chats?limit=${limit}&skip=${skip}&search_space_id=${searchSpaceId}`,
{
headers: {
Authorization: `Bearer ${authToken}`,
},
method: "GET",
}
);
const response = await fetch(
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/chats?limit=${limit}&skip=${skip}&search_space_id=${searchSpaceId}`,
{
headers: {
Authorization: `Bearer ${authToken}`,
},
method: "GET",
}
);
if (!response.ok) {
throw new Error(`Failed to fetch chats: ${response.status}`);
}
if (!response.ok) {
throw new Error(`Failed to fetch chats: ${response.status}`);
}
return await response.json();
return await response.json();
};

View file

@ -3,273 +3,258 @@ import { DocumentTypeCount } from "@/hooks/use-document-types";
import { normalizeListResponse } from "../pagination";
export const uploadDocument = async (formData: FormData, authToken: string) => {
const response = await fetch(
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/documents/fileupload`,
{
method: "POST",
headers: {
Authorization: `Bearer ${authToken}`,
},
body: formData,
}
);
const response = await fetch(
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/documents/fileupload`,
{
method: "POST",
headers: {
Authorization: `Bearer ${authToken}`,
},
body: formData,
}
);
if (!response.ok) {
throw new Error("Failed to upload document");
}
if (!response.ok) {
throw new Error("Failed to upload document");
}
return await response.json();
return await response.json();
};
export const createDocument = async (request: {
documentType: string;
content: any;
searchSpaceId: number;
authToken: string;
documentType: string;
content: any;
searchSpaceId: number;
authToken: string;
}) => {
const response = await fetch(
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/documents`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${request.authToken}`,
},
body: JSON.stringify(request),
}
);
const response = await fetch(`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/documents`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${request.authToken}`,
},
body: JSON.stringify(request),
});
if (!response.ok) {
throw new Error("Failed to process document");
}
if (!response.ok) {
throw new Error("Failed to process document");
}
return await response.json();
return await response.json();
};
export const fetchDocumentByChunk = async (
chunkId: number,
authToken: string
) => {
const response = await fetch(
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/documents/by-chunk/${chunkId}`,
{
headers: {
Authorization: `Bearer ${authToken}`,
"Content-Type": "application/json",
},
method: "GET",
}
);
export const fetchDocumentByChunk = async (chunkId: number, authToken: string) => {
const response = await fetch(
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/documents/by-chunk/${chunkId}`,
{
headers: {
Authorization: `Bearer ${authToken}`,
"Content-Type": "application/json",
},
method: "GET",
}
);
if (!response.ok) {
const errorText = await response.text();
let errorMessage = "Failed to fetch document";
if (!response.ok) {
const errorText = await response.text();
let errorMessage = "Failed to fetch document";
try {
const errorData = JSON.parse(errorText);
errorMessage = errorData.detail || errorMessage;
} catch {
// If parsing fails, use default message
}
try {
const errorData = JSON.parse(errorText);
errorMessage = errorData.detail || errorMessage;
} catch {
// If parsing fails, use default message
}
if (response.status === 404) {
errorMessage = "Chunk not found or you don't have access to it";
}
throw new Error(errorMessage);
}
if (response.status === 404) {
errorMessage = "Chunk not found or you don't have access to it";
}
throw new Error(errorMessage);
}
const data: DocumentWithChunks = await response.json();
const data: DocumentWithChunks = await response.json();
return data;
return data;
};
export const fetchDocumentTypes = async (authToken: string) => {
if (!authToken) {
throw new Error("No authentication token found");
}
if (!authToken) {
throw new Error("No authentication token found");
}
// Build URL with optional search_space_id query parameter
const url = new URL(
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/documents/type-counts`
);
// Build URL with optional search_space_id query parameter
const url = new URL(
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/documents/type-counts`
);
const response = await fetch(url.toString(), {
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${authToken}`,
},
});
const response = await fetch(url.toString(), {
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${authToken}`,
},
});
if (!response.ok) {
throw new Error(`Failed to fetch document types: ${response.statusText}`);
}
if (!response.ok) {
throw new Error(`Failed to fetch document types: ${response.statusText}`);
}
const data = await response.json();
const data = await response.json();
// Convert the object to an array of DocumentTypeCount
const typeCounts: DocumentTypeCount[] = Object.entries(data).map(
([type, count]) => ({
type,
count: count as number,
})
);
// Convert the object to an array of DocumentTypeCount
const typeCounts: DocumentTypeCount[] = Object.entries(data).map(([type, count]) => ({
type,
count: count as number,
}));
return typeCounts;
return typeCounts;
};
export const fetchDocuments = async (
searchSpaceId: number,
authToken: string,
fetchPage?: number,
fetchPageSize?: number,
fetchDocumentTypes?: string[]
searchSpaceId: number,
authToken: string,
fetchPage?: number,
fetchPageSize?: number,
fetchDocumentTypes?: string[]
) => {
// Build query params
const params = new URLSearchParams({
search_space_id: searchSpaceId.toString(),
});
// Build query params
const params = new URLSearchParams({
search_space_id: searchSpaceId.toString(),
});
// // Use passed parameters or fall back to state/options
// const effectivePage = fetchPage !== undefined ? fetchPage : page;
// const effectivePageSize =
// fetchPageSize !== undefined ? fetchPageSize : pageSize;
// const effectiveDocumentTypes =
// fetchDocumentTypes !== undefined ? fetchDocumentTypes : documentTypes;
// // Use passed parameters or fall back to state/options
// const effectivePage = fetchPage !== undefined ? fetchPage : page;
// const effectivePageSize =
// fetchPageSize !== undefined ? fetchPageSize : pageSize;
// const effectiveDocumentTypes =
// fetchDocumentTypes !== undefined ? fetchDocumentTypes : documentTypes;
// if (effectivePage !== undefined) {
// params.append("page", effectivePage.toString());
// }
// if (effectivePageSize !== undefined) {
// params.append("page_size", effectivePageSize.toString());
// }
// if (effectiveDocumentTypes && effectiveDocumentTypes.length > 0) {
// params.append("document_types", effectiveDocumentTypes.join(","));
// }
// if (effectivePage !== undefined) {
// params.append("page", effectivePage.toString());
// }
// if (effectivePageSize !== undefined) {
// params.append("page_size", effectivePageSize.toString());
// }
// if (effectiveDocumentTypes && effectiveDocumentTypes.length > 0) {
// params.append("document_types", effectiveDocumentTypes.join(","));
// }
const response = await fetch(
`${
process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL
}/api/v1/documents?${params.toString()}`,
{
headers: {
Authorization: `Bearer ${authToken}`,
},
method: "GET",
}
);
const response = await fetch(
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/documents?${params.toString()}`,
{
headers: {
Authorization: `Bearer ${authToken}`,
},
method: "GET",
}
);
if (!response.ok) {
throw new Error("Failed to fetch documents");
}
if (!response.ok) {
throw new Error("Failed to fetch documents");
}
const data = await response.json();
return normalizeListResponse<Document>(data);
const data = await response.json();
return normalizeListResponse<Document>(data);
};
export const searchDocuments = async (
searchSpaceId: number,
authToken: string,
searchQuery: string,
fetchPage?: number,
fetchPageSize?: number,
fetchDocumentTypes?: string[]
searchSpaceId: number,
authToken: string,
searchQuery: string,
fetchPage?: number,
fetchPageSize?: number,
fetchDocumentTypes?: string[]
) => {
// if (!searchQuery.trim()) {
// // If search is empty, fetch all documents
// // return fetchDocuments(fetchPage, fetchPageSize, fetchDocumentTypes);
// }
// if (!searchQuery.trim()) {
// // If search is empty, fetch all documents
// // return fetchDocuments(fetchPage, fetchPageSize, fetchDocumentTypes);
// }
// Build query params
const params = new URLSearchParams({
search_space_id: searchSpaceId.toString(),
title: searchQuery,
});
// Build query params
const params = new URLSearchParams({
search_space_id: searchSpaceId.toString(),
title: searchQuery,
});
// // Use passed parameters or fall back to state/options
// const effectivePage = fetchPage !== undefined ? fetchPage : page;
// const effectivePageSize = fetchPageSize !== undefined ? fetchPageSize : pageSize;
// const effectiveDocumentTypes =
// fetchDocumentTypes !== undefined ? fetchDocumentTypes : documentTypes;
// // Use passed parameters or fall back to state/options
// const effectivePage = fetchPage !== undefined ? fetchPage : page;
// const effectivePageSize = fetchPageSize !== undefined ? fetchPageSize : pageSize;
// const effectiveDocumentTypes =
// fetchDocumentTypes !== undefined ? fetchDocumentTypes : documentTypes;
// if (effectivePage !== undefined) {
// params.append("page", effectivePage.toString());
// }
// if (effectivePageSize !== undefined) {
// params.append("page_size", effectivePageSize.toString());
// }
// if (effectiveDocumentTypes && effectiveDocumentTypes.length > 0) {
// params.append("document_types", effectiveDocumentTypes.join(","));
// }
// if (effectivePage !== undefined) {
// params.append("page", effectivePage.toString());
// }
// if (effectivePageSize !== undefined) {
// params.append("page_size", effectivePageSize.toString());
// }
// if (effectiveDocumentTypes && effectiveDocumentTypes.length > 0) {
// params.append("document_types", effectiveDocumentTypes.join(","));
// }
const response = await fetch(
`${
process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL
}/api/v1/documents/search?${params.toString()}`,
{
headers: {
Authorization: `Bearer ${authToken}`,
},
method: "GET",
}
);
const response = await fetch(
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/documents/search?${params.toString()}`,
{
headers: {
Authorization: `Bearer ${authToken}`,
},
method: "GET",
}
);
if (!response.ok) {
throw new Error("Failed to search documents");
}
if (!response.ok) {
throw new Error("Failed to search documents");
}
const data = await response.json();
const normalized = normalizeListResponse<Document>(data);
return normalized;
const data = await response.json();
const normalized = normalizeListResponse<Document>(data);
return normalized;
};
export const deleteDocument = async (documentId: number, authToken: string) => {
const response = await fetch(
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/documents/${documentId}`,
{
headers: {
Authorization: `Bearer ${authToken}`,
},
method: "DELETE",
}
);
const response = await fetch(
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/documents/${documentId}`,
{
headers: {
Authorization: `Bearer ${authToken}`,
},
method: "DELETE",
}
);
if (!response.ok) {
throw new Error("Failed to delete document");
}
if (!response.ok) {
throw new Error("Failed to delete document");
}
return await response.json();
return await response.json();
};
export const getDocumentTypeCounts = async (
searchSpaceId: number,
authToken: string
) => {
try {
const params = new URLSearchParams({
search_space_id: searchSpaceId.toString(),
});
export const getDocumentTypeCounts = async (searchSpaceId: number, authToken: string) => {
try {
const params = new URLSearchParams({
search_space_id: searchSpaceId.toString(),
});
const response = await fetch(
`${
process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL
}/api/v1/documents/type-counts?${params.toString()}`,
{
headers: {
Authorization: `Bearer ${authToken}`,
},
method: "GET",
}
);
const response = await fetch(
`${
process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL
}/api/v1/documents/type-counts?${params.toString()}`,
{
headers: {
Authorization: `Bearer ${authToken}`,
},
method: "GET",
}
);
if (!response.ok) {
throw new Error("Failed to fetch document type counts");
}
if (!response.ok) {
throw new Error("Failed to fetch document type counts");
}
const counts = await response.json();
return counts as Record<string, number>;
} catch (err: any) {
console.error("Error fetching document type counts:", err);
return {};
}
const counts = await response.json();
return counts as Record<string, number>;
} catch (err: any) {
console.error("Error fetching document type counts:", err);
return {};
}
};

View file

@ -1,98 +1,90 @@
import { CreateLLMConfig, LLMConfig, UpdateLLMConfig } from "@/hooks/use-llm-configs";
export const fetchLLMConfigs = async (
searchSpaceId: number,
authToken: string
) => {
const response = await fetch(
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/llm-configs?search_space_id=${searchSpaceId}`,
{
headers: {
Authorization: `Bearer ${authToken}`,
},
method: "GET",
}
);
export const fetchLLMConfigs = async (searchSpaceId: number, authToken: string) => {
const response = await fetch(
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/llm-configs?search_space_id=${searchSpaceId}`,
{
headers: {
Authorization: `Bearer ${authToken}`,
},
method: "GET",
}
);
if (!response.ok) {
throw new Error("Failed to fetch LLM configurations");
}
if (!response.ok) {
throw new Error("Failed to fetch LLM configurations");
}
return await response.json();
return await response.json();
};
export const createLLMConfig = async (
config: CreateLLMConfig,
authToken: string
config: CreateLLMConfig,
authToken: string
): Promise<LLMConfig | null> => {
const response = await fetch(
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/llm-configs`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${authToken}`,
},
body: JSON.stringify(config),
}
);
const response = await fetch(
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/llm-configs`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${authToken}`,
},
body: JSON.stringify(config),
}
);
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.detail || "Failed to create LLM configuration");
}
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.detail || "Failed to create LLM configuration");
}
const newConfig = await response.json();
const newConfig = await response.json();
return newConfig;
return newConfig;
};
export const deleteLLMConfig = async (
id: number,
authToken: string
): Promise<boolean> => {
const response = await fetch(
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/llm-configs/${id}`,
{
method: "DELETE",
headers: {
Authorization: `Bearer ${authToken}`,
},
}
);
export const deleteLLMConfig = async (id: number, authToken: string): Promise<boolean> => {
const response = await fetch(
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/llm-configs/${id}`,
{
method: "DELETE",
headers: {
Authorization: `Bearer ${authToken}`,
},
}
);
if (!response.ok) {
throw new Error("Failed to delete LLM configuration");
}
if (!response.ok) {
throw new Error("Failed to delete LLM configuration");
}
return await response.json();
return await response.json();
};
export const updateLLMConfig = async (
id: number,
config: UpdateLLMConfig,
authToken: string
id: number,
config: UpdateLLMConfig,
authToken: string
): Promise<LLMConfig | null> => {
const response = await fetch(
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/llm-configs/${id}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${authToken}`,
},
body: JSON.stringify(config),
}
);
const response = await fetch(
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/llm-configs/${id}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${authToken}`,
},
body: JSON.stringify(config),
}
);
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.detail || "Failed to update LLM configuration");
}
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.detail || "Failed to update LLM configuration");
}
const updatedConfig = await response.json();
return updatedConfig;
const updatedConfig = await response.json();
return updatedConfig;
};

View file

@ -2,78 +2,73 @@ import type { PodcastItem } from "@/app/dashboard/[search_space_id]/podcasts/pod
import type { GeneratePodcastRequest } from "@/components/chat/ChatPanel/ChatPanelContainer";
export const getPodcastByChatId = async (chatId: string, authToken: string) => {
const response = await fetch(
`${
process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL
}/api/v1/podcasts/by-chat/${Number(chatId)}`,
{
headers: {
Authorization: `Bearer ${authToken}`,
},
method: "GET",
}
);
const response = await fetch(
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/podcasts/by-chat/${Number(chatId)}`,
{
headers: {
Authorization: `Bearer ${authToken}`,
},
method: "GET",
}
);
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.detail || "Failed to fetch podcast");
}
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.detail || "Failed to fetch podcast");
}
return (await response.json()) as PodcastItem | null;
return (await response.json()) as PodcastItem | null;
};
export const generatePodcast = async (
request: GeneratePodcastRequest,
authToken: string
) => {
const response = await fetch(
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/podcasts/generate/`,
{
method: "POST",
headers: {
Authorization: `Bearer ${authToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify(request),
}
);
export const generatePodcast = async (request: GeneratePodcastRequest, authToken: string) => {
const response = await fetch(
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/podcasts/generate/`,
{
method: "POST",
headers: {
Authorization: `Bearer ${authToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify(request),
}
);
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.detail || "Failed to generate podcast");
}
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.detail || "Failed to generate podcast");
}
return await response.json();
return await response.json();
};
export const loadPodcast = async (podcast: PodcastItem, authToken: string) => {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 30000);
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 30000);
try {
const response = await fetch(
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/podcasts/${podcast.id}/stream`,
{
headers: {
Authorization: `Bearer ${authToken}`,
},
signal: controller.signal,
}
);
try {
const response = await fetch(
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/podcasts/${podcast.id}/stream`,
{
headers: {
Authorization: `Bearer ${authToken}`,
},
signal: controller.signal,
}
);
if (!response.ok) {
throw new Error(`Failed to fetch audio stream: ${response.statusText}`);
}
if (!response.ok) {
throw new Error(`Failed to fetch audio stream: ${response.statusText}`);
}
const blob = await response.blob();
const objectUrl = URL.createObjectURL(blob);
return objectUrl;
} catch (error) {
if (error instanceof DOMException && error.name === "AbortError") {
throw new Error("Request timed out. Please try again.");
}
throw error;
} finally {
clearTimeout(timeoutId);
}
const blob = await response.blob();
const objectUrl = URL.createObjectURL(blob);
return objectUrl;
} catch (error) {
if (error instanceof DOMException && error.name === "AbortError") {
throw new Error("Request timed out. Please try again.");
}
throw error;
} finally {
clearTimeout(timeoutId);
}
};

View file

@ -1,113 +1,107 @@
import { Connector, CreateConnectorRequest } from "@/hooks/use-connectors";
export const createConnector = async (
data: CreateConnectorRequest,
authToken: string
data: CreateConnectorRequest,
authToken: string
): Promise<Connector> => {
const response = await fetch(
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/search-source-connectors`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${authToken}`,
},
body: JSON.stringify(data),
}
);
const response = await fetch(
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/search-source-connectors`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${authToken}`,
},
body: JSON.stringify(data),
}
);
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.detail || "Failed to create connector");
}
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.detail || "Failed to create connector");
}
return response.json();
return response.json();
};
export const getConnectors = async (
skip = 0,
limit = 100,
authToken: string
skip = 0,
limit = 100,
authToken: string
): Promise<Connector[]> => {
const response = await fetch(
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/search-source-connectors?skip=${skip}&limit=${limit}`,
{
headers: {
Authorization: `Bearer ${authToken}`,
},
}
);
const response = await fetch(
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/search-source-connectors?skip=${skip}&limit=${limit}`,
{
headers: {
Authorization: `Bearer ${authToken}`,
},
}
);
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.detail || "Failed to fetch connectors");
}
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.detail || "Failed to fetch connectors");
}
return response.json();
return response.json();
};
export const getConnector = async (
connectorId: number,
authToken: string
): Promise<Connector> => {
const response = await fetch(
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/search-source-connectors/${connectorId}`,
{
headers: {
Authorization: `Bearer ${authToken}`,
},
}
);
export const getConnector = async (connectorId: number, authToken: string): Promise<Connector> => {
const response = await fetch(
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/search-source-connectors/${connectorId}`,
{
headers: {
Authorization: `Bearer ${authToken}`,
},
}
);
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.detail || "Failed to fetch connector");
}
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.detail || "Failed to fetch connector");
}
return response.json();
return response.json();
};
export const updateConnector = async (
connectorId: number,
data: CreateConnectorRequest,
authToken: string
connectorId: number,
data: CreateConnectorRequest,
authToken: string
): Promise<Connector> => {
const response = await fetch(
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/search-source-connectors/${connectorId}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${authToken}`,
},
body: JSON.stringify(data),
}
);
const response = await fetch(
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/search-source-connectors/${connectorId}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${authToken}`,
},
body: JSON.stringify(data),
}
);
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.detail || "Failed to update connector");
}
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.detail || "Failed to update connector");
}
return response.json();
return response.json();
};
export const deleteConnector = async (
connectorId: number,
authToken: string
): Promise<void> => {
const response = await fetch(
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/search-source-connectors/${connectorId}`,
{
method: "DELETE",
headers: {
Authorization: `Bearer ${authToken}`,
},
}
);
export const deleteConnector = async (connectorId: number, authToken: string): Promise<void> => {
const response = await fetch(
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/search-source-connectors/${connectorId}`,
{
method: "DELETE",
headers: {
Authorization: `Bearer ${authToken}`,
},
}
);
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.detail || "Failed to delete connector");
}
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.detail || "Failed to delete connector");
}
};

View file

@ -1,112 +1,98 @@
export const fetchSearchSpaces = async () => {
const response = await fetch(
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/searchspaces`,
{
headers: {
Authorization: `Bearer ${localStorage.getItem(
"surfsense_bearer_token"
)}`,
},
method: "GET",
}
);
const response = await fetch(
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/searchspaces`,
{
headers: {
Authorization: `Bearer ${localStorage.getItem("surfsense_bearer_token")}`,
},
method: "GET",
}
);
if (!response.ok) {
throw new Error("Not authenticated");
}
if (!response.ok) {
throw new Error("Not authenticated");
}
return await response.json();
return await response.json();
};
export const deleteSearchSpace = async (id: number) => {
const response = await fetch(
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/searchspaces/${id}`,
{
method: "DELETE",
headers: {
Authorization: `Bearer ${localStorage.getItem(
"surfsense_bearer_token"
)}`,
},
}
);
const response = await fetch(
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/searchspaces/${id}`,
{
method: "DELETE",
headers: {
Authorization: `Bearer ${localStorage.getItem("surfsense_bearer_token")}`,
},
}
);
if (!response.ok) {
throw new Error("Failed to delete search space");
}
if (!response.ok) {
throw new Error("Failed to delete search space");
}
return await response.json();
return await response.json();
};
export const createSearchSpace = async (data: {
name: string;
description: string;
}) => {
const response = await fetch(
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/searchspaces`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${localStorage.getItem(
"surfsense_bearer_token"
)}`,
},
body: JSON.stringify(data),
}
);
export const createSearchSpace = async (data: { name: string; description: string }) => {
const response = await fetch(
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/searchspaces`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${localStorage.getItem("surfsense_bearer_token")}`,
},
body: JSON.stringify(data),
}
);
if (!response.ok) {
throw new Error("Failed to create search space");
}
if (!response.ok) {
throw new Error("Failed to create search space");
}
return await response.json();
return await response.json();
};
export const fetchSearchSpace = async (searchSpaceId: string) => {
const response = await fetch(
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/searchspaces/${searchSpaceId}`,
{
headers: {
Authorization: `Bearer ${localStorage.getItem(
"surfsense_bearer_token"
)}`,
},
method: "GET",
}
);
const response = await fetch(
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/searchspaces/${searchSpaceId}`,
{
headers: {
Authorization: `Bearer ${localStorage.getItem("surfsense_bearer_token")}`,
},
method: "GET",
}
);
if (response.status === 401) {
// Clear token and redirect to home
localStorage.removeItem("surfsense_bearer_token");
window.location.href = "/";
throw new Error("Unauthorized: Redirecting to login page");
}
if (response.status === 401) {
// Clear token and redirect to home
localStorage.removeItem("surfsense_bearer_token");
window.location.href = "/";
throw new Error("Unauthorized: Redirecting to login page");
}
if (!response.ok) {
throw new Error(`Failed to fetch search space: ${response.status}`);
}
if (!response.ok) {
throw new Error(`Failed to fetch search space: ${response.status}`);
}
return await response.json();
return await response.json();
};
export const fetchSearchSpacePreferences = async (
searchSpaceId: number,
authToken: string
) => {
const response = await fetch(
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/search-spaces/${searchSpaceId}/llm-preferences`,
{
headers: {
Authorization: `Bearer ${authToken}`,
},
method: "GET",
}
);
export const fetchSearchSpacePreferences = async (searchSpaceId: number, authToken: string) => {
const response = await fetch(
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/search-spaces/${searchSpaceId}/llm-preferences`,
{
headers: {
Authorization: `Bearer ${authToken}`,
},
method: "GET",
}
);
if (!response.ok) {
throw new Error("Failed to fetch LLM preferences");
}
if (!response.ok) {
throw new Error("Failed to fetch LLM preferences");
}
return await response.json();
return await response.json();
};

View file

@ -1,30 +1,30 @@
export class AppError extends Error {
constructor(message: string) {
super(message);
this.name = this.constructor.name;
}
constructor(message: string) {
super(message);
this.name = this.constructor.name;
}
}
export class NetworkError extends AppError {
constructor(message: string) {
super(message);
}
constructor(message: string) {
super(message);
}
}
export class ValidationError extends AppError {
constructor(message: string) {
super(message);
}
constructor(message: string) {
super(message);
}
}
export class AuthenticationError extends AppError {
constructor(message: string) {
super(message);
}
constructor(message: string) {
super(message);
}
}
export class AuthorizationError extends AppError {
constructor(message: string) {
super(message);
}
constructor(message: string) {
super(message);
}
}

View file

@ -1,6 +1,6 @@
export const cacheKeys = {
activeSearchSpace: {
chats : (searchSpaceId: string) => ["active-search-space", "chats", searchSpaceId] as const,
activeChat : (chatId: string) => ["active-search-space", "active-chat", chatId] as const,
chats: (searchSpaceId: string) => ["active-search-space", "chats", searchSpaceId] as const,
activeChat: (chatId: string) => ["active-search-space", "active-chat", chatId] as const,
},
};

View file

@ -6,9 +6,8 @@ export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
export function getChatTitleFromMessages(messages: Message[]) {
const userMessages = messages.filter((msg) => msg.role === "user");
if (userMessages.length === 0) return "Untitled Chat";
return userMessages[0].content;
}
}