mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-04-25 00:36:31 +02:00
chore: Updated UserRead schema to include pages_limit and pages_used fields
- Expanded installation options in README to include SurfSense Cloud as a new method. - Updated UserRead schema to include pages_limit and pages_used fields. - Added AnnouncementBanner component to the dashboard layout for improved user notifications. - Refactored DashboardPage to utilize useUser hook for user state management. - Integrated page usage display in AppSidebar to show user-specific page limits and usage. - Removed deprecated apiClient code and replaced it with hooks for better API interaction.
This commit is contained in:
parent
a3a5b13f48
commit
4dd7e8fc1f
13 changed files with 456 additions and 339 deletions
15
README.md
15
README.md
|
|
@ -141,19 +141,24 @@ Check out our public roadmap and contribute your ideas or feedback:
|
|||
|
||||
### Installation Options
|
||||
|
||||
SurfSense provides two installation methods:
|
||||
SurfSense provides three options to get started:
|
||||
|
||||
1. **[Docker Installation](https://www.surfsense.net/docs/docker-installation)** - The easiest way to get SurfSense up and running with all dependencies containerized.
|
||||
1. **[SurfSense Cloud](https://www.surfsense.com/login)** - The easiest way to try SurfSense without any setup.
|
||||
- No installation required
|
||||
- Instant access to all features
|
||||
- Perfect for getting started quickly
|
||||
|
||||
2. **[Docker Installation (Recommended for Self-Hosting)](https://www.surfsense.net/docs/docker-installation)** - Easy way to get SurfSense up and running with all dependencies containerized.
|
||||
- Includes pgAdmin for database management through a web UI
|
||||
- Supports environment variable customization via `.env` file
|
||||
- Flexible deployment options (full stack or core services only)
|
||||
- No need to manually edit configuration files between environments
|
||||
|
||||
2. **[Manual Installation (Recommended)](https://www.surfsense.net/docs/manual-installation)** - For users who prefer more control over their setup or need to customize their deployment.
|
||||
3. **[Manual Installation](https://www.surfsense.net/docs/manual-installation)** - For users who prefer more control over their setup or need to customize their deployment.
|
||||
|
||||
Both installation guides include detailed OS-specific instructions for Windows, macOS, and Linux.
|
||||
Docker and manual installation guides include detailed OS-specific instructions for Windows, macOS, and Linux.
|
||||
|
||||
Before installation, make sure to complete the [prerequisite setup steps](https://www.surfsense.net/docs/) including:
|
||||
Before self-hosting installation, make sure to complete the [prerequisite setup steps](https://www.surfsense.net/docs/) including:
|
||||
- Auth setup
|
||||
- **File Processing ETL Service** (choose one):
|
||||
- Unstructured.io API key (supports 34+ formats)
|
||||
|
|
|
|||
|
|
@ -4,7 +4,8 @@ from fastapi_users import schemas
|
|||
|
||||
|
||||
class UserRead(schemas.BaseUser[uuid.UUID]):
|
||||
pass
|
||||
pages_limit: int
|
||||
pages_used: int
|
||||
|
||||
|
||||
class UserCreate(schemas.BaseUserCreate):
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
import { Loader2 } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
import { AnnouncementBanner } from "@/components/announcement-banner";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
|
||||
interface DashboardLayoutProps {
|
||||
|
|
@ -40,5 +41,10 @@ export default function DashboardLayout({ children }: DashboardLayoutProps) {
|
|||
);
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
return (
|
||||
<>
|
||||
<AnnouncementBanner />
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import Image from "next/image";
|
|||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useEffect, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { Logo } from "@/components/Logo";
|
||||
import { ThemeTogglerComponent } from "@/components/theme/theme-toggle";
|
||||
|
|
@ -34,16 +33,8 @@ import {
|
|||
} from "@/components/ui/card";
|
||||
import { Spotlight } from "@/components/ui/spotlight";
|
||||
import { Tilt } from "@/components/ui/tilt";
|
||||
import { useUser } from "@/hooks";
|
||||
import { useSearchSpaces } from "@/hooks/use-search-spaces";
|
||||
import { apiClient } from "@/lib/api";
|
||||
|
||||
interface User {
|
||||
id: string;
|
||||
email: string;
|
||||
is_active: boolean;
|
||||
is_superuser: boolean;
|
||||
is_verified: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a date string into a readable format
|
||||
|
|
@ -163,35 +154,8 @@ const DashboardPage = () => {
|
|||
|
||||
const { searchSpaces, loading, error, refreshSearchSpaces } = useSearchSpaces();
|
||||
|
||||
// User state management
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
const [isLoadingUser, setIsLoadingUser] = useState(true);
|
||||
const [userError, setUserError] = useState<string | null>(null);
|
||||
|
||||
// Fetch user details
|
||||
useEffect(() => {
|
||||
const fetchUser = async () => {
|
||||
try {
|
||||
if (typeof window === "undefined") return;
|
||||
|
||||
try {
|
||||
const userData = await apiClient.get<User>("users/me");
|
||||
setUser(userData);
|
||||
setUserError(null);
|
||||
} catch (error) {
|
||||
console.error("Error fetching user:", error);
|
||||
setUserError(error instanceof Error ? error.message : "Unknown error occurred");
|
||||
} finally {
|
||||
setIsLoadingUser(false);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error in fetchUser:", error);
|
||||
setIsLoadingUser(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchUser();
|
||||
}, []);
|
||||
const { user, loading: isLoadingUser, error: userError } = useUser();
|
||||
|
||||
// Create user object for UserDropdown
|
||||
const customUser = {
|
||||
|
|
|
|||
42
surfsense_web/components/announcement-banner.tsx
Normal file
42
surfsense_web/components/announcement-banner.tsx
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
"use client";
|
||||
|
||||
import { ExternalLink, Info, X } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
export function AnnouncementBanner() {
|
||||
const [isVisible, setIsVisible] = useState(true);
|
||||
|
||||
if (!isVisible) return null;
|
||||
|
||||
return (
|
||||
<div className="relative bg-gradient-to-r from-blue-600 to-blue-500 dark:from-blue-700 dark:to-blue-600 border-b border-blue-700 dark:border-blue-800">
|
||||
<div className="container mx-auto px-4">
|
||||
<div className="flex items-center justify-center gap-3 py-2.5">
|
||||
<Info className="h-4 w-4 text-blue-50 flex-shrink-0" />
|
||||
<p className="text-sm text-blue-50 text-center font-medium">
|
||||
SurfSense is a work in progress.{" "}
|
||||
<a
|
||||
href="https://github.com/MODSetter/SurfSense/issues"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-1 underline decoration-blue-200 underline-offset-2 hover:decoration-white transition-colors"
|
||||
>
|
||||
Report issues on GitHub
|
||||
<ExternalLink className="h-3 w-3" />
|
||||
</a>
|
||||
</p>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-7 w-7 p-0 shrink-0 text-blue-100 hover:text-white hover:bg-blue-700/50 dark:hover:bg-blue-800/50 absolute right-4"
|
||||
onClick={() => setIsVisible(false)}
|
||||
>
|
||||
<X className="h-3.5 w-3.5" />
|
||||
<span className="sr-only">Dismiss</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -13,24 +13,7 @@ import {
|
|||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { apiClient } from "@/lib/api";
|
||||
|
||||
interface Chat {
|
||||
created_at: string;
|
||||
id: number;
|
||||
type: string;
|
||||
title: string;
|
||||
messages: string[];
|
||||
search_space_id: number;
|
||||
}
|
||||
|
||||
interface SearchSpace {
|
||||
created_at: string;
|
||||
id: number;
|
||||
name: string;
|
||||
description: string;
|
||||
user_id: string;
|
||||
}
|
||||
import { useChats, useSearchSpace, useUser } from "@/hooks";
|
||||
|
||||
interface AppSidebarProviderProps {
|
||||
searchSpaceId: string;
|
||||
|
|
@ -58,21 +41,25 @@ export function AppSidebarProvider({
|
|||
}: AppSidebarProviderProps) {
|
||||
const t = useTranslations("dashboard");
|
||||
const tCommon = useTranslations("common");
|
||||
const [recentChats, setRecentChats] = useState<
|
||||
{
|
||||
name: string;
|
||||
url: string;
|
||||
icon: string;
|
||||
id: number;
|
||||
search_space_id: number;
|
||||
actions: { name: string; icon: string; onClick: () => void }[];
|
||||
}[]
|
||||
>([]);
|
||||
const [searchSpace, setSearchSpace] = useState<SearchSpace | null>(null);
|
||||
const [isLoadingChats, setIsLoadingChats] = useState(true);
|
||||
const [isLoadingSearchSpace, setIsLoadingSearchSpace] = useState(true);
|
||||
const [chatError, setChatError] = useState<string | null>(null);
|
||||
const [searchSpaceError, setSearchSpaceError] = useState<string | null>(null);
|
||||
|
||||
// Use the new hooks
|
||||
const {
|
||||
chats,
|
||||
loading: isLoadingChats,
|
||||
error: chatError,
|
||||
fetchChats: fetchRecentChats,
|
||||
deleteChat,
|
||||
} = useChats({ searchSpaceId, limit: 5, skip: 0 });
|
||||
|
||||
const {
|
||||
searchSpace,
|
||||
loading: isLoadingSearchSpace,
|
||||
error: searchSpaceError,
|
||||
fetchSearchSpace,
|
||||
} = useSearchSpace({ searchSpaceId });
|
||||
|
||||
const { user } = useUser();
|
||||
|
||||
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
||||
const [chatToDelete, setChatToDelete] = useState<{ id: number; name: string } | null>(null);
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
|
|
@ -83,95 +70,32 @@ export function AppSidebarProvider({
|
|||
setIsClient(true);
|
||||
}, []);
|
||||
|
||||
// Memoized fetch function for chats
|
||||
const fetchRecentChats = useCallback(async () => {
|
||||
try {
|
||||
// Only run on client-side
|
||||
if (typeof window === "undefined") return;
|
||||
|
||||
const chats: Chat[] = await apiClient.get<Chat[]>(
|
||||
`api/v1/chats?limit=5&skip=0&search_space_id=${searchSpaceId}`
|
||||
);
|
||||
|
||||
// Sort chats by created_at in descending order (newest first)
|
||||
const sortedChats = chats.sort(
|
||||
(a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()
|
||||
);
|
||||
|
||||
// Transform API response to the format expected by AppSidebar
|
||||
const formattedChats = sortedChats.map((chat) => ({
|
||||
name: chat.title || `Chat ${chat.id}`,
|
||||
url: `/dashboard/${chat.search_space_id}/researcher/${chat.id}`,
|
||||
icon: "MessageCircleMore",
|
||||
id: chat.id,
|
||||
search_space_id: chat.search_space_id,
|
||||
actions: [
|
||||
{
|
||||
name: "Delete",
|
||||
icon: "Trash2",
|
||||
onClick: () => {
|
||||
setChatToDelete({ id: chat.id, name: chat.title || `Chat ${chat.id}` });
|
||||
setShowDeleteDialog(true);
|
||||
},
|
||||
},
|
||||
],
|
||||
}));
|
||||
|
||||
setRecentChats(formattedChats);
|
||||
setChatError(null);
|
||||
} catch (error) {
|
||||
console.error("Error fetching chats:", error);
|
||||
setChatError(error instanceof Error ? error.message : "Unknown error occurred");
|
||||
setRecentChats([]);
|
||||
} finally {
|
||||
setIsLoadingChats(false);
|
||||
}
|
||||
}, [searchSpaceId]);
|
||||
|
||||
// Memoized fetch function for search space
|
||||
const fetchSearchSpace = useCallback(async () => {
|
||||
try {
|
||||
// Only run on client-side
|
||||
if (typeof window === "undefined") return;
|
||||
|
||||
const data: SearchSpace = await apiClient.get<SearchSpace>(
|
||||
`api/v1/searchspaces/${searchSpaceId}`
|
||||
);
|
||||
setSearchSpace(data);
|
||||
setSearchSpaceError(null);
|
||||
} catch (error) {
|
||||
console.error("Error fetching search space:", error);
|
||||
setSearchSpaceError(error instanceof Error ? error.message : "Unknown error occurred");
|
||||
} finally {
|
||||
setIsLoadingSearchSpace(false);
|
||||
}
|
||||
}, [searchSpaceId]);
|
||||
|
||||
// Retry function
|
||||
const retryFetch = useCallback(() => {
|
||||
setChatError(null);
|
||||
setSearchSpaceError(null);
|
||||
setIsLoadingChats(true);
|
||||
setIsLoadingSearchSpace(true);
|
||||
fetchRecentChats();
|
||||
fetchSearchSpace();
|
||||
}, [fetchRecentChats, fetchSearchSpace]);
|
||||
|
||||
// Fetch recent chats
|
||||
useEffect(() => {
|
||||
fetchRecentChats();
|
||||
|
||||
// Set up a refresh interval (every 5 minutes)
|
||||
const intervalId = setInterval(fetchRecentChats, 5 * 60 * 1000);
|
||||
|
||||
// Clean up interval on component unmount
|
||||
return () => clearInterval(intervalId);
|
||||
}, [fetchRecentChats]);
|
||||
|
||||
// Fetch search space details
|
||||
useEffect(() => {
|
||||
fetchSearchSpace();
|
||||
}, [fetchSearchSpace]);
|
||||
// Transform API response to the format expected by AppSidebar
|
||||
const recentChats = useMemo(() => {
|
||||
return chats.map((chat) => ({
|
||||
name: chat.title || `Chat ${chat.id}`,
|
||||
url: `/dashboard/${chat.search_space_id}/researcher/${chat.id}`,
|
||||
icon: "MessageCircleMore",
|
||||
id: chat.id,
|
||||
search_space_id: chat.search_space_id,
|
||||
actions: [
|
||||
{
|
||||
name: "Delete",
|
||||
icon: "Trash2",
|
||||
onClick: () => {
|
||||
setChatToDelete({ id: chat.id, name: chat.title || `Chat ${chat.id}` });
|
||||
setShowDeleteDialog(true);
|
||||
},
|
||||
},
|
||||
],
|
||||
}));
|
||||
}, [chats]);
|
||||
|
||||
// Handle delete chat with better error handling
|
||||
const handleDeleteChat = useCallback(async () => {
|
||||
|
|
@ -179,11 +103,7 @@ export function AppSidebarProvider({
|
|||
|
||||
try {
|
||||
setIsDeleting(true);
|
||||
|
||||
await apiClient.delete(`api/v1/chats/${chatToDelete.id}`);
|
||||
|
||||
// Update local state
|
||||
setRecentChats((prev) => prev.filter((chat) => chat.id !== chatToDelete.id));
|
||||
await deleteChat(chatToDelete.id);
|
||||
} catch (error) {
|
||||
console.error("Error deleting chat:", error);
|
||||
// You could show a toast notification here
|
||||
|
|
@ -192,7 +112,7 @@ export function AppSidebarProvider({
|
|||
setShowDeleteDialog(false);
|
||||
setChatToDelete(null);
|
||||
}
|
||||
}, [chatToDelete]);
|
||||
}, [chatToDelete, deleteChat]);
|
||||
|
||||
// Memoized fallback chats
|
||||
const fallbackChats = useMemo(() => {
|
||||
|
|
@ -260,14 +180,34 @@ export function AppSidebarProvider({
|
|||
tCommon,
|
||||
]);
|
||||
|
||||
// Prepare page usage data
|
||||
const pageUsage = user
|
||||
? {
|
||||
pagesUsed: user.pages_used,
|
||||
pagesLimit: user.pages_limit,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
// Show loading state if not client-side
|
||||
if (!isClient) {
|
||||
return <AppSidebar navSecondary={navSecondary} navMain={navMain} RecentChats={[]} />;
|
||||
return (
|
||||
<AppSidebar
|
||||
navSecondary={navSecondary}
|
||||
navMain={navMain}
|
||||
RecentChats={[]}
|
||||
pageUsage={pageUsage}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<AppSidebar navSecondary={updatedNavSecondary} navMain={navMain} RecentChats={displayChats} />
|
||||
<AppSidebar
|
||||
navSecondary={updatedNavSecondary}
|
||||
navMain={navMain}
|
||||
RecentChats={displayChats}
|
||||
pageUsage={pageUsage}
|
||||
/>
|
||||
|
||||
{/* Delete Confirmation Dialog */}
|
||||
<Dialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import { Logo } from "@/components/Logo";
|
|||
import { NavMain } from "@/components/sidebar/nav-main";
|
||||
import { NavProjects } from "@/components/sidebar/nav-projects";
|
||||
import { NavSecondary } from "@/components/sidebar/nav-secondary";
|
||||
import { PageUsageDisplay } from "@/components/sidebar/page-usage-display";
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
|
|
@ -175,6 +176,10 @@ interface AppSidebarProps extends React.ComponentProps<typeof Sidebar> {
|
|||
email: string;
|
||||
avatar: string;
|
||||
};
|
||||
pageUsage?: {
|
||||
pagesUsed: number;
|
||||
pagesLimit: number;
|
||||
};
|
||||
}
|
||||
|
||||
// Memoized AppSidebar component for better performance
|
||||
|
|
@ -182,6 +187,7 @@ export const AppSidebar = memo(function AppSidebar({
|
|||
navMain = defaultData.navMain,
|
||||
navSecondary = defaultData.navSecondary,
|
||||
RecentChats = defaultData.RecentChats,
|
||||
pageUsage,
|
||||
...props
|
||||
}: AppSidebarProps) {
|
||||
// Process navMain to resolve icon names to components
|
||||
|
|
@ -246,6 +252,9 @@ export const AppSidebar = memo(function AppSidebar({
|
|||
)}
|
||||
</SidebarContent>
|
||||
<SidebarFooter>
|
||||
{pageUsage && (
|
||||
<PageUsageDisplay pagesUsed={pageUsage.pagesUsed} pagesLimit={pageUsage.pagesLimit} />
|
||||
)}
|
||||
<NavSecondary items={processedNavSecondary} className="mt-auto" />
|
||||
</SidebarFooter>
|
||||
</Sidebar>
|
||||
|
|
|
|||
63
surfsense_web/components/sidebar/page-usage-display.tsx
Normal file
63
surfsense_web/components/sidebar/page-usage-display.tsx
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
"use client";
|
||||
|
||||
import { Mail } from "lucide-react";
|
||||
import { Progress } from "@/components/ui/progress";
|
||||
import {
|
||||
SidebarGroup,
|
||||
SidebarGroupContent,
|
||||
SidebarGroupLabel,
|
||||
useSidebar,
|
||||
} from "@/components/ui/sidebar";
|
||||
|
||||
interface PageUsageDisplayProps {
|
||||
pagesUsed: number;
|
||||
pagesLimit: number;
|
||||
}
|
||||
|
||||
export function PageUsageDisplay({ pagesUsed, pagesLimit }: PageUsageDisplayProps) {
|
||||
const { state } = useSidebar();
|
||||
const usagePercentage = (pagesUsed / pagesLimit) * 100;
|
||||
const isCollapsed = state === "collapsed";
|
||||
|
||||
return (
|
||||
<SidebarGroup>
|
||||
<SidebarGroupLabel className="group-data-[collapsible=icon]:hidden">
|
||||
Page Usage
|
||||
</SidebarGroupLabel>
|
||||
<SidebarGroupContent>
|
||||
<div className="space-y-2 px-2 py-2">
|
||||
{isCollapsed ? (
|
||||
// Show only a compact progress indicator when collapsed
|
||||
<div className="flex justify-center">
|
||||
<Progress value={usagePercentage} className="h-2 w-8" />
|
||||
</div>
|
||||
) : (
|
||||
// Show full details when expanded
|
||||
<>
|
||||
<div className="flex justify-between items-center text-xs">
|
||||
<span className="text-muted-foreground">
|
||||
{pagesUsed.toLocaleString()} / {pagesLimit.toLocaleString()} pages
|
||||
</span>
|
||||
<span className="font-medium">{usagePercentage.toFixed(0)}%</span>
|
||||
</div>
|
||||
<Progress value={usagePercentage} className="h-2" />
|
||||
<div className="flex items-start gap-2 pt-1">
|
||||
<Mail className="h-3 w-3 text-muted-foreground mt-0.5 flex-shrink-0" />
|
||||
<p className="text-[10px] text-muted-foreground leading-tight">
|
||||
Contact{" "}
|
||||
<a
|
||||
href="mailto:rohan@surfsense.com"
|
||||
className="text-primary hover:underline font-medium"
|
||||
>
|
||||
rohan@surfsense.com
|
||||
</a>{" "}
|
||||
to increase limits
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,3 +1,6 @@
|
|||
export * from "./use-chats";
|
||||
export * from "./use-document-by-chunk";
|
||||
export * from "./use-logs";
|
||||
export * from "./use-search-source-connectors";
|
||||
export * from "./use-search-space";
|
||||
export * from "./use-user";
|
||||
|
|
|
|||
124
surfsense_web/hooks/use-chats.ts
Normal file
124
surfsense_web/hooks/use-chats.ts
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface Chat {
|
||||
created_at: string;
|
||||
id: number;
|
||||
type: string;
|
||||
title: string;
|
||||
messages: string[];
|
||||
search_space_id: number;
|
||||
}
|
||||
|
||||
interface UseChatsOptions {
|
||||
searchSpaceId: string | number;
|
||||
limit?: number;
|
||||
skip?: number;
|
||||
autoFetch?: boolean;
|
||||
}
|
||||
|
||||
export function useChats({
|
||||
searchSpaceId,
|
||||
limit = 5,
|
||||
skip = 0,
|
||||
autoFetch = true,
|
||||
}: UseChatsOptions) {
|
||||
const [chats, setChats] = useState<Chat[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const fetchChats = useCallback(async () => {
|
||||
try {
|
||||
// Only run on client-side
|
||||
if (typeof window === "undefined") return;
|
||||
|
||||
setLoading(true);
|
||||
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 ${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.ok) {
|
||||
throw new Error(`Failed to fetch chats: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Sort chats by created_at in descending order (newest first)
|
||||
const sortedChats = data.sort(
|
||||
(a: Chat, b: Chat) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()
|
||||
);
|
||||
|
||||
setChats(sortedChats);
|
||||
setError(null);
|
||||
} catch (err: any) {
|
||||
setError(err.message || "Failed to fetch chats");
|
||||
console.error("Error fetching chats:", err);
|
||||
setChats([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [searchSpaceId, limit, skip]);
|
||||
|
||||
const deleteChat = useCallback(async (chatId: number) => {
|
||||
try {
|
||||
// Only run on client-side
|
||||
if (typeof window === "undefined") return;
|
||||
|
||||
const response = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/chats/${chatId}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem("surfsense_bearer_token")}`,
|
||||
},
|
||||
method: "DELETE",
|
||||
}
|
||||
);
|
||||
|
||||
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 delete chat: ${response.status}`);
|
||||
}
|
||||
|
||||
// Update local state to remove the deleted chat
|
||||
setChats((prev) => prev.filter((chat) => chat.id !== chatId));
|
||||
} catch (err: any) {
|
||||
console.error("Error deleting chat:", err);
|
||||
throw err;
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (autoFetch) {
|
||||
fetchChats();
|
||||
|
||||
// Set up a refresh interval (every 5 minutes)
|
||||
const intervalId = setInterval(fetchChats, 5 * 60 * 1000);
|
||||
|
||||
// Clean up interval on component unmount
|
||||
return () => clearInterval(intervalId);
|
||||
}
|
||||
}, [autoFetch, fetchChats]);
|
||||
|
||||
return { chats, loading, error, fetchChats, deleteChat };
|
||||
}
|
||||
69
surfsense_web/hooks/use-search-space.ts
Normal file
69
surfsense_web/hooks/use-search-space.ts
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface SearchSpace {
|
||||
created_at: string;
|
||||
id: number;
|
||||
name: string;
|
||||
description: string;
|
||||
user_id: string;
|
||||
}
|
||||
|
||||
interface UseSearchSpaceOptions {
|
||||
searchSpaceId: string | number;
|
||||
autoFetch?: boolean;
|
||||
}
|
||||
|
||||
export function useSearchSpace({ searchSpaceId, autoFetch = true }: UseSearchSpaceOptions) {
|
||||
const [searchSpace, setSearchSpace] = useState<SearchSpace | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const fetchSearchSpace = useCallback(async () => {
|
||||
try {
|
||||
// Only run on client-side
|
||||
if (typeof window === "undefined") return;
|
||||
|
||||
setLoading(true);
|
||||
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.ok) {
|
||||
throw new Error(`Failed to fetch search space: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
setSearchSpace(data);
|
||||
setError(null);
|
||||
} catch (err: any) {
|
||||
setError(err.message || "Failed to fetch search space");
|
||||
console.error("Error fetching search space:", err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [searchSpaceId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (autoFetch) {
|
||||
fetchSearchSpace();
|
||||
}
|
||||
}, [autoFetch, fetchSearchSpace]);
|
||||
|
||||
return { searchSpace, loading, error, fetchSearchSpace };
|
||||
}
|
||||
61
surfsense_web/hooks/use-user.ts
Normal file
61
surfsense_web/hooks/use-user.ts
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface User {
|
||||
id: string;
|
||||
email: string;
|
||||
is_active: boolean;
|
||||
is_superuser: boolean;
|
||||
is_verified: boolean;
|
||||
pages_limit: number;
|
||||
pages_used: number;
|
||||
}
|
||||
|
||||
export function useUser() {
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchUser = async () => {
|
||||
try {
|
||||
// Only run on client-side
|
||||
if (typeof window === "undefined") return;
|
||||
|
||||
setLoading(true);
|
||||
const response = await fetch(`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/users/me`, {
|
||||
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.ok) {
|
||||
throw new Error(`Failed to fetch user: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
setUser(data);
|
||||
setError(null);
|
||||
} catch (err: any) {
|
||||
setError(err.message || "Failed to fetch user");
|
||||
console.error("Error fetching user:", err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchUser();
|
||||
}, []);
|
||||
|
||||
return { user, loading, error };
|
||||
}
|
||||
|
|
@ -1,170 +0,0 @@
|
|||
import { toast } from "sonner";
|
||||
|
||||
/**
|
||||
* Custom fetch wrapper that handles authentication and redirects to home page on 401 Unauthorized
|
||||
*
|
||||
* @param url - The URL to fetch
|
||||
* @param options - Fetch options
|
||||
* @returns The fetch response
|
||||
*/
|
||||
export async function fetchWithAuth(url: string, options: RequestInit = {}): Promise<Response> {
|
||||
// Only run on client-side
|
||||
if (typeof window === "undefined") {
|
||||
return fetch(url, options);
|
||||
}
|
||||
|
||||
// Get token from localStorage
|
||||
const token = localStorage.getItem("surfsense_bearer_token");
|
||||
|
||||
// Add authorization header if token exists
|
||||
const headers = {
|
||||
...options.headers,
|
||||
...(token && { Authorization: `Bearer ${token}` }),
|
||||
};
|
||||
|
||||
// Make the request
|
||||
const response = await fetch(url, {
|
||||
...options,
|
||||
headers,
|
||||
});
|
||||
|
||||
// Handle 401 Unauthorized response
|
||||
if (response.status === 401) {
|
||||
// Show error toast
|
||||
toast.error("Session expired. Please log in again.");
|
||||
|
||||
// Clear token
|
||||
localStorage.removeItem("surfsense_bearer_token");
|
||||
|
||||
// Redirect to home page
|
||||
window.location.href = "/";
|
||||
|
||||
// Throw error to stop further processing
|
||||
throw new Error("Unauthorized: Redirecting to login page");
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the full API URL
|
||||
*
|
||||
* @param path - The API path
|
||||
* @returns The full API URL
|
||||
*/
|
||||
export function getApiUrl(path: string): string {
|
||||
// Remove leading slash if present
|
||||
const cleanPath = path.startsWith("/") ? path.slice(1) : path;
|
||||
|
||||
// Get backend URL from environment variable
|
||||
const baseUrl = process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL;
|
||||
|
||||
if (!baseUrl) {
|
||||
console.error("NEXT_PUBLIC_FASTAPI_BACKEND_URL is not defined");
|
||||
return "";
|
||||
}
|
||||
|
||||
// Combine base URL and path
|
||||
return `${baseUrl}/${cleanPath}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* API client with methods for common operations
|
||||
*/
|
||||
export const apiClient = {
|
||||
/**
|
||||
* Make a GET request
|
||||
*
|
||||
* @param path - The API path
|
||||
* @param options - Additional fetch options
|
||||
* @returns The response data
|
||||
*/
|
||||
async get<T>(path: string, options: RequestInit = {}): Promise<T> {
|
||||
const response = await fetchWithAuth(getApiUrl(path), {
|
||||
method: "GET",
|
||||
...options,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => null);
|
||||
throw new Error(`API error: ${response.status} ${errorData?.detail || response.statusText}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
},
|
||||
|
||||
/**
|
||||
* Make a POST request
|
||||
*
|
||||
* @param path - The API path
|
||||
* @param data - The request body
|
||||
* @param options - Additional fetch options
|
||||
* @returns The response data
|
||||
*/
|
||||
async post<T>(path: string, data: any, options: RequestInit = {}): Promise<T> {
|
||||
const response = await fetchWithAuth(getApiUrl(path), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...options.headers,
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
...options,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => null);
|
||||
throw new Error(`API error: ${response.status} ${errorData?.detail || response.statusText}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
},
|
||||
|
||||
/**
|
||||
* Make a PUT request
|
||||
*
|
||||
* @param path - The API path
|
||||
* @param data - The request body
|
||||
* @param options - Additional fetch options
|
||||
* @returns The response data
|
||||
*/
|
||||
async put<T>(path: string, data: any, options: RequestInit = {}): Promise<T> {
|
||||
const response = await fetchWithAuth(getApiUrl(path), {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...options.headers,
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
...options,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => null);
|
||||
throw new Error(`API error: ${response.status} ${errorData?.detail || response.statusText}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
},
|
||||
|
||||
/**
|
||||
* Make a DELETE request
|
||||
*
|
||||
* @param path - The API path
|
||||
* @param options - Additional fetch options
|
||||
* @returns The response data
|
||||
*/
|
||||
async delete<T>(path: string, options: RequestInit = {}): Promise<T> {
|
||||
const response = await fetchWithAuth(getApiUrl(path), {
|
||||
method: "DELETE",
|
||||
...options,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => null);
|
||||
throw new Error(`API error: ${response.status} ${errorData?.detail || response.statusText}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
},
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue