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:
DESKTOP-RTLN3BA\$punk 2025-11-03 22:34:37 -08:00
parent a3a5b13f48
commit 4dd7e8fc1f
13 changed files with 456 additions and 339 deletions

View file

@ -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}>

View file

@ -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>

View 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>
);
}