mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-25 19:15:18 +02:00
format with biome
This commit is contained in:
parent
77d49ca11c
commit
81ee04c2a5
21 changed files with 1602 additions and 1752 deletions
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
);
|
||||
},
|
||||
};
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
},
|
||||
};
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
import { atom } from "jotai";
|
||||
|
||||
export const activeSearchSpaceIdAtom = atom<string | null>(null);
|
||||
export const activeSearchSpaceIdAtom = atom<string | null>(null);
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 || ""
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 {};
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue