Initial formatting using biome

This commit is contained in:
Utkarsh-Patel-13 2025-07-27 10:05:37 -07:00
parent 4c8ff48155
commit 758603b275
156 changed files with 23825 additions and 29508 deletions

View file

@ -1,267 +1,290 @@
'use client';
"use client";
import { useEffect, useState } from 'react';
import { AppSidebar } from '@/components/sidebar/app-sidebar';
import { useEffect, useState } from "react";
import { AppSidebar } from "@/components/sidebar/app-sidebar";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Trash2 } from "lucide-react";
import { apiClient } from '@/lib/api'; // Import the API client
import { apiClient } from "@/lib/api"; // Import the API client
interface Chat {
created_at: string;
id: number;
type: string;
title: string;
messages: string[];
search_space_id: number;
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;
created_at: string;
id: number;
name: string;
description: string;
user_id: string;
}
interface AppSidebarProviderProps {
searchSpaceId: string;
navSecondary: {
title: string;
url: string;
icon: string;
}[];
navMain: {
title: string;
url: string;
icon: string;
isActive?: boolean;
items?: {
title: string;
url: string;
}[];
}[];
searchSpaceId: string;
navSecondary: {
title: string;
url: string;
icon: string;
}[];
navMain: {
title: string;
url: string;
icon: string;
isActive?: boolean;
items?: {
title: string;
url: string;
}[];
}[];
}
export function AppSidebarProvider({
searchSpaceId,
navSecondary,
navMain
searchSpaceId,
navSecondary,
navMain,
}: AppSidebarProviderProps) {
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);
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
const [chatToDelete, setChatToDelete] = useState<{ id: number, name: string } | null>(null);
const [isDeleting, setIsDeleting] = useState(false);
const [isClient, setIsClient] = useState(false);
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);
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
const [chatToDelete, setChatToDelete] = useState<{ id: number; name: string } | null>(null);
const [isDeleting, setIsDeleting] = useState(false);
const [isClient, setIsClient] = useState(false);
// Set isClient to true when component mounts on the client
useEffect(() => {
setIsClient(true);
}, []);
// Set isClient to true when component mounts on the client
useEffect(() => {
setIsClient(true);
}, []);
// Fetch recent chats
useEffect(() => {
const fetchRecentChats = async () => {
try {
// Only run on client-side
if (typeof window === 'undefined') return;
// Fetch recent chats
useEffect(() => {
const fetchRecentChats = async () => {
try {
// Only run on client-side
if (typeof window === "undefined") return;
try {
// Use the API client instead of direct fetch - filter by current search space ID
const chats: Chat[] = await apiClient.get<Chat[]>(`api/v1/chats/?limit=5&skip=0&search_space_id=${searchSpaceId}`);
try {
// Use the API client instead of direct fetch - filter by current search space ID
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()
);
// console.log("sortedChats", sortedChats);
// Transform API response to the format expected by AppSidebar
const formattedChats = sortedChats.map(chat => ({
name: chat.title || `Chat ${chat.id}`, // Fallback if title is empty
url: `/dashboard/${chat.search_space_id}/researcher/${chat.id}`,
icon: 'MessageCircleMore',
id: chat.id,
search_space_id: chat.search_space_id,
actions: [
{
name: 'View Details',
icon: 'ExternalLink',
onClick: () => {
window.location.href = `/dashboard/${chat.search_space_id}/researcher/${chat.id}`;
}
},
{
name: 'Delete',
icon: 'Trash2',
onClick: () => {
setChatToDelete({ id: chat.id, name: chat.title || `Chat ${chat.id}` });
setShowDeleteDialog(true);
}
}
]
}));
// 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()
);
// console.log("sortedChats", sortedChats);
// Transform API response to the format expected by AppSidebar
const formattedChats = sortedChats.map((chat) => ({
name: chat.title || `Chat ${chat.id}`, // Fallback if title is empty
url: `/dashboard/${chat.search_space_id}/researcher/${chat.id}`,
icon: "MessageCircleMore",
id: chat.id,
search_space_id: chat.search_space_id,
actions: [
{
name: "View Details",
icon: "ExternalLink",
onClick: () => {
window.location.href = `/dashboard/${chat.search_space_id}/researcher/${chat.id}`;
},
},
{
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');
// Provide empty array to ensure UI still renders
setRecentChats([]);
} finally {
setIsLoadingChats(false);
}
} catch (error) {
console.error('Error in fetchRecentChats:', error);
setIsLoadingChats(false);
}
};
setRecentChats(formattedChats);
setChatError(null);
} catch (error) {
console.error("Error fetching chats:", error);
setChatError(error instanceof Error ? error.message : "Unknown error occurred");
// Provide empty array to ensure UI still renders
setRecentChats([]);
} finally {
setIsLoadingChats(false);
}
} catch (error) {
console.error("Error in fetchRecentChats:", error);
setIsLoadingChats(false);
}
};
fetchRecentChats();
fetchRecentChats();
// Set up a refresh interval (every 5 minutes)
const intervalId = setInterval(fetchRecentChats, 5 * 60 * 1000);
// Set up a refresh interval (every 5 minutes)
const intervalId = setInterval(fetchRecentChats, 5 * 60 * 1000);
// Clean up interval on component unmount
return () => clearInterval(intervalId);
}, [searchSpaceId]);
// Clean up interval on component unmount
return () => clearInterval(intervalId);
}, [searchSpaceId]);
// Handle delete chat
const handleDeleteChat = async () => {
if (!chatToDelete) return;
// Handle delete chat
const handleDeleteChat = async () => {
if (!chatToDelete) return;
try {
setIsDeleting(true);
try {
setIsDeleting(true);
// Use the API client instead of direct fetch
await apiClient.delete(`api/v1/chats/${chatToDelete.id}`);
// Use the API client instead of direct fetch
await apiClient.delete(`api/v1/chats/${chatToDelete.id}`);
// Close dialog and refresh chats
setRecentChats(recentChats.filter(chat => chat.id !== chatToDelete.id));
// Close dialog and refresh chats
setRecentChats(recentChats.filter((chat) => chat.id !== chatToDelete.id));
} catch (error) {
console.error("Error deleting chat:", error);
} finally {
setIsDeleting(false);
setShowDeleteDialog(false);
setChatToDelete(null);
}
};
} catch (error) {
console.error('Error deleting chat:', error);
} finally {
setIsDeleting(false);
setShowDeleteDialog(false);
setChatToDelete(null);
}
};
// Fetch search space details
useEffect(() => {
const fetchSearchSpace = async () => {
try {
// Only run on client-side
if (typeof window === "undefined") return;
// Fetch search space details
useEffect(() => {
const fetchSearchSpace = async () => {
try {
// Only run on client-side
if (typeof window === 'undefined') return;
try {
// Use the API client instead of direct fetch
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);
}
} catch (error) {
console.error("Error in fetchSearchSpace:", error);
setIsLoadingSearchSpace(false);
}
};
try {
// Use the API client instead of direct fetch
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);
}
} catch (error) {
console.error('Error in fetchSearchSpace:', error);
setIsLoadingSearchSpace(false);
}
};
fetchSearchSpace();
}, [searchSpaceId]);
fetchSearchSpace();
}, [searchSpaceId]);
// Create a fallback chat if there's an error or no chats
const fallbackChats =
chatError || (!isLoadingChats && recentChats.length === 0)
? [
{
name: chatError ? "Error loading chats" : "No recent chats",
url: "#",
icon: chatError ? "AlertCircle" : "MessageCircleMore",
id: 0,
search_space_id: Number(searchSpaceId),
actions: [],
},
]
: [];
// Create a fallback chat if there's an error or no chats
const fallbackChats = chatError || (!isLoadingChats && recentChats.length === 0)
? [{
name: chatError ? "Error loading chats" : "No recent chats",
url: "#",
icon: chatError ? "AlertCircle" : "MessageCircleMore",
id: 0,
search_space_id: Number(searchSpaceId),
actions: []
}]
: [];
// Use fallback chats if there's an error or no chats
const displayChats = recentChats.length > 0 ? recentChats : fallbackChats;
// Use fallback chats if there's an error or no chats
const displayChats = recentChats.length > 0 ? recentChats : fallbackChats;
// Update the first item in navSecondary to show the search space name
const updatedNavSecondary = [...navSecondary];
if (updatedNavSecondary.length > 0 && isClient) {
updatedNavSecondary[0] = {
...updatedNavSecondary[0],
title:
searchSpace?.name ||
(isLoadingSearchSpace
? "Loading..."
: searchSpaceError
? "Error loading search space"
: "Unknown Search Space"),
};
}
// Update the first item in navSecondary to show the search space name
const updatedNavSecondary = [...navSecondary];
if (updatedNavSecondary.length > 0 && isClient) {
updatedNavSecondary[0] = {
...updatedNavSecondary[0],
title: searchSpace?.name || (isLoadingSearchSpace ? 'Loading...' : searchSpaceError ? 'Error loading search space' : 'Unknown Search Space'),
};
}
return (
<>
<AppSidebar
navSecondary={updatedNavSecondary}
navMain={navMain}
RecentChats={isClient ? displayChats : []}
/>
return (
<>
<AppSidebar
navSecondary={updatedNavSecondary}
navMain={navMain}
RecentChats={isClient ? displayChats : []}
/>
{/* Delete Confirmation Dialog - Only render on client */}
{isClient && (
<Dialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
<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?.name}</span>? This action cannot be undone.
</DialogDescription>
</DialogHeader>
<DialogFooter className="flex gap-2 sm:justify-end">
<Button
variant="outline"
onClick={() => setShowDeleteDialog(false)}
disabled={isDeleting}
>
Cancel
</Button>
<Button
variant="destructive"
onClick={handleDeleteChat}
disabled={isDeleting}
className="gap-2"
>
{isDeleting ? (
<>
<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>
)}
</>
);
}
{/* Delete Confirmation Dialog - Only render on client */}
{isClient && (
<Dialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
<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?.name}</span>? This action cannot be
undone.
</DialogDescription>
</DialogHeader>
<DialogFooter className="flex gap-2 sm:justify-end">
<Button
variant="outline"
onClick={() => setShowDeleteDialog(false)}
disabled={isDeleting}
>
Cancel
</Button>
<Button
variant="destructive"
onClick={handleDeleteChat}
disabled={isDeleting}
className="gap-2"
>
{isDeleting ? (
<>
<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>
)}
</>
);
}

View file

@ -1,233 +1,235 @@
"use client"
"use client";
import * as React from "react"
import * as React from "react";
import {
BookOpen,
Cable,
FileStack,
Undo2,
MessageCircleMore,
Settings2,
SquareLibrary,
SquareTerminal,
AlertCircle,
Info,
ExternalLink,
Trash2,
Podcast,
type LucideIcon,
FileText,
} from "lucide-react"
BookOpen,
Cable,
FileStack,
Undo2,
MessageCircleMore,
Settings2,
SquareLibrary,
SquareTerminal,
AlertCircle,
Info,
ExternalLink,
Trash2,
Podcast,
type LucideIcon,
FileText,
} from "lucide-react";
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 { NavMain } from "@/components/sidebar/nav-main";
import { NavProjects } from "@/components/sidebar/nav-projects";
import { NavSecondary } from "@/components/sidebar/nav-secondary";
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarHeader,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
} from "@/components/ui/sidebar"
Sidebar,
SidebarContent,
SidebarFooter,
SidebarHeader,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
} from "@/components/ui/sidebar";
// Map of icon names to their components
export const iconMap: Record<string, LucideIcon> = {
BookOpen,
Cable,
FileStack,
Undo2,
MessageCircleMore,
Settings2,
SquareLibrary,
SquareTerminal,
AlertCircle,
Info,
ExternalLink,
Trash2,
Podcast,
FileText
}
BookOpen,
Cable,
FileStack,
Undo2,
MessageCircleMore,
Settings2,
SquareLibrary,
SquareTerminal,
AlertCircle,
Info,
ExternalLink,
Trash2,
Podcast,
FileText,
};
const defaultData = {
user: {
name: "Surf",
email: "m@example.com",
avatar: "/icon-128.png",
},
navMain: [
{
title: "Researcher",
url: "#",
icon: "SquareTerminal",
isActive: true,
items: [],
},
user: {
name: "Surf",
email: "m@example.com",
avatar: "/icon-128.png",
},
navMain: [
{
title: "Researcher",
url: "#",
icon: "SquareTerminal",
isActive: true,
items: [],
},
{
title: "Documents",
url: "#",
icon: "FileStack",
items: [
{
title: "Upload Documents",
url: "#",
},
{
title: "Manage Documents",
url: "#",
},
],
},
{
title: "Connectors",
url: "#",
icon: "Cable",
items: [
{
title: "Add Connector",
url: "#",
},
{
title: "Manage Connectors",
url: "#",
},
],
},
{
title: "Research Synthesizer's",
url: "#",
icon: "SquareLibrary",
items: [
{
title: "Podcast Creator",
url: "#",
},
{
title: "Presentation Creator",
url: "#",
},
],
},
],
navSecondary: [
{
title: "SEARCH SPACE",
url: "#",
icon: "LifeBuoy",
},
],
RecentChats: [
{
name: "Design Engineering",
url: "#",
icon: "MessageCircleMore",
id: 1001,
},
{
name: "Sales & Marketing",
url: "#",
icon: "MessageCircleMore",
id: 1002,
},
{
name: "Travel",
url: "#",
icon: "MessageCircleMore",
id: 1003,
},
],
}
{
title: "Documents",
url: "#",
icon: "FileStack",
items: [
{
title: "Upload Documents",
url: "#",
},
{
title: "Manage Documents",
url: "#",
},
],
},
{
title: "Connectors",
url: "#",
icon: "Cable",
items: [
{
title: "Add Connector",
url: "#",
},
{
title: "Manage Connectors",
url: "#",
},
],
},
{
title: "Research Synthesizer's",
url: "#",
icon: "SquareLibrary",
items: [
{
title: "Podcast Creator",
url: "#",
},
{
title: "Presentation Creator",
url: "#",
},
],
},
],
navSecondary: [
{
title: "SEARCH SPACE",
url: "#",
icon: "LifeBuoy",
},
],
RecentChats: [
{
name: "Design Engineering",
url: "#",
icon: "MessageCircleMore",
id: 1001,
},
{
name: "Sales & Marketing",
url: "#",
icon: "MessageCircleMore",
id: 1002,
},
{
name: "Travel",
url: "#",
icon: "MessageCircleMore",
id: 1003,
},
],
};
interface AppSidebarProps extends React.ComponentProps<typeof Sidebar> {
navMain?: {
title: string
url: string
icon: string
isActive?: boolean
items?: {
title: string
url: string
}[]
}[]
navSecondary?: {
title: string
url: string
icon: string // Changed to string (icon name)
}[]
RecentChats?: {
name: string
url: string
icon: string // Changed to string (icon name)
id?: number
search_space_id?: number
actions?: {
name: string
icon: string
onClick: () => void
}[]
}[]
navMain?: {
title: string;
url: string;
icon: string;
isActive?: boolean;
items?: {
title: string;
url: string;
}[];
}[];
navSecondary?: {
title: string;
url: string;
icon: string; // Changed to string (icon name)
}[];
RecentChats?: {
name: string;
url: string;
icon: string; // Changed to string (icon name)
id?: number;
search_space_id?: number;
actions?: {
name: string;
icon: string;
onClick: () => void;
}[];
}[];
}
export function AppSidebar({
navMain = defaultData.navMain,
navSecondary = defaultData.navSecondary,
RecentChats = defaultData.RecentChats,
...props
export function AppSidebar({
navMain = defaultData.navMain,
navSecondary = defaultData.navSecondary,
RecentChats = defaultData.RecentChats,
...props
}: AppSidebarProps) {
// Process navMain to resolve icon names to components
const processedNavMain = React.useMemo(() => {
return navMain.map(item => ({
...item,
icon: iconMap[item.icon] || SquareTerminal // Fallback to SquareTerminal if icon not found
}))
}, [navMain])
// Process navMain to resolve icon names to components
const processedNavMain = React.useMemo(() => {
return navMain.map((item) => ({
...item,
icon: iconMap[item.icon] || SquareTerminal, // Fallback to SquareTerminal if icon not found
}));
}, [navMain]);
// Process navSecondary to resolve icon names to components
const processedNavSecondary = React.useMemo(() => {
return navSecondary.map(item => ({
...item,
icon: iconMap[item.icon] || Undo2 // Fallback to Undo2 if icon not found
}))
}, [navSecondary])
// Process navSecondary to resolve icon names to components
const processedNavSecondary = React.useMemo(() => {
return navSecondary.map((item) => ({
...item,
icon: iconMap[item.icon] || Undo2, // Fallback to Undo2 if icon not found
}));
}, [navSecondary]);
// Process RecentChats to resolve icon names to components
const processedRecentChats = React.useMemo(() => {
return RecentChats?.map(item => ({
...item,
icon: iconMap[item.icon] || MessageCircleMore // Fallback to MessageCircleMore if icon not found
})) || [];
}, [RecentChats])
// Process RecentChats to resolve icon names to components
const processedRecentChats = React.useMemo(() => {
return (
RecentChats?.map((item) => ({
...item,
icon: iconMap[item.icon] || MessageCircleMore, // Fallback to MessageCircleMore if icon not found
})) || []
);
}, [RecentChats]);
return (
<Sidebar variant="inset" {...props}>
<SidebarHeader>
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton size="lg" asChild>
<div>
<div className="bg-sidebar-primary text-sidebar-primary-foreground flex aspect-square size-8 items-center justify-center rounded-lg">
<Logo className="rounded-lg" />
</div>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-medium">SurfSense</span>
<span className="truncate text-xs">beta v0.0.7</span>
</div>
</div>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarHeader>
<SidebarContent>
<NavMain items={processedNavMain} />
{processedRecentChats.length > 0 && <NavProjects chats={processedRecentChats} />}
<NavSecondary items={processedNavSecondary} className="mt-auto" />
</SidebarContent>
{/* <SidebarFooter>
return (
<Sidebar variant="inset" {...props}>
<SidebarHeader>
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton size="lg" asChild>
<div>
<div className="bg-sidebar-primary text-sidebar-primary-foreground flex aspect-square size-8 items-center justify-center rounded-lg">
<Logo className="rounded-lg" />
</div>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-medium">SurfSense</span>
<span className="truncate text-xs">beta v0.0.7</span>
</div>
</div>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarHeader>
<SidebarContent>
<NavMain items={processedNavMain} />
{processedRecentChats.length > 0 && <NavProjects chats={processedRecentChats} />}
<NavSecondary items={processedNavSecondary} className="mt-auto" />
</SidebarContent>
{/* <SidebarFooter>
footer
</SidebarFooter> */}
</Sidebar>
)
</Sidebar>
);
}

View file

@ -1,78 +1,74 @@
"use client"
"use client";
import { ChevronRight, type LucideIcon } from "lucide-react"
import { ChevronRight, type LucideIcon } from "lucide-react";
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible"
import {
SidebarGroup,
SidebarGroupLabel,
SidebarMenu,
SidebarMenuAction,
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuSub,
SidebarMenuSubButton,
SidebarMenuSubItem,
} from "@/components/ui/sidebar"
SidebarGroup,
SidebarGroupLabel,
SidebarMenu,
SidebarMenuAction,
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuSub,
SidebarMenuSubButton,
SidebarMenuSubItem,
} from "@/components/ui/sidebar";
export function NavMain({
items,
items,
}: {
items: {
title: string
url: string
icon: LucideIcon
isActive?: boolean
items?: {
title: string
url: string
}[]
}[]
items: {
title: string;
url: string;
icon: LucideIcon;
isActive?: boolean;
items?: {
title: string;
url: string;
}[];
}[];
}) {
return (
<SidebarGroup>
<SidebarGroupLabel>Platform</SidebarGroupLabel>
<SidebarMenu>
{items.map((item, index) => (
<Collapsible key={`${item.title}-${index}`} asChild defaultOpen={item.isActive}>
<SidebarMenuItem>
<SidebarMenuButton asChild tooltip={item.title}>
<a href={item.url}>
<item.icon />
<span>{item.title}</span>
</a>
</SidebarMenuButton>
{item.items?.length ? (
<>
<CollapsibleTrigger asChild>
<SidebarMenuAction className="data-[state=open]:rotate-90">
<ChevronRight />
<span className="sr-only">Toggle</span>
</SidebarMenuAction>
</CollapsibleTrigger>
<CollapsibleContent>
<SidebarMenuSub>
{item.items?.map((subItem, subIndex) => (
<SidebarMenuSubItem key={`${subItem.title}-${subIndex}`}>
<SidebarMenuSubButton asChild>
<a href={subItem.url}>
<span>{subItem.title}</span>
</a>
</SidebarMenuSubButton>
</SidebarMenuSubItem>
))}
</SidebarMenuSub>
</CollapsibleContent>
</>
) : null}
</SidebarMenuItem>
</Collapsible>
))}
</SidebarMenu>
</SidebarGroup>
)
return (
<SidebarGroup>
<SidebarGroupLabel>Platform</SidebarGroupLabel>
<SidebarMenu>
{items.map((item, index) => (
<Collapsible key={`${item.title}-${index}`} asChild defaultOpen={item.isActive}>
<SidebarMenuItem>
<SidebarMenuButton asChild tooltip={item.title}>
<a href={item.url}>
<item.icon />
<span>{item.title}</span>
</a>
</SidebarMenuButton>
{item.items?.length ? (
<>
<CollapsibleTrigger asChild>
<SidebarMenuAction className="data-[state=open]:rotate-90">
<ChevronRight />
<span className="sr-only">Toggle</span>
</SidebarMenuAction>
</CollapsibleTrigger>
<CollapsibleContent>
<SidebarMenuSub>
{item.items?.map((subItem, subIndex) => (
<SidebarMenuSubItem key={`${subItem.title}-${subIndex}`}>
<SidebarMenuSubButton asChild>
<a href={subItem.url}>
<span>{subItem.title}</span>
</a>
</SidebarMenuSubButton>
</SidebarMenuSubItem>
))}
</SidebarMenuSub>
</CollapsibleContent>
</>
) : null}
</SidebarMenuItem>
</Collapsible>
))}
</SidebarMenu>
</SidebarGroup>
);
}

View file

@ -1,122 +1,118 @@
"use client"
"use client";
import { ExternalLink, Folder, MoreHorizontal, Share, Trash2, type LucideIcon } from "lucide-react";
import {
ExternalLink,
Folder,
MoreHorizontal,
Share,
Trash2,
type LucideIcon,
} from "lucide-react"
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import {
SidebarGroup,
SidebarGroupLabel,
SidebarMenu,
SidebarMenuAction,
SidebarMenuButton,
SidebarMenuItem,
useSidebar,
} from "@/components/ui/sidebar"
import { useRouter } from "next/navigation"
SidebarGroup,
SidebarGroupLabel,
SidebarMenu,
SidebarMenuAction,
SidebarMenuButton,
SidebarMenuItem,
useSidebar,
} from "@/components/ui/sidebar";
import { useRouter } from "next/navigation";
// Map of icon names to their components
const actionIconMap: Record<string, LucideIcon> = {
ExternalLink,
Folder,
Share,
Trash2,
MoreHorizontal
}
ExternalLink,
Folder,
Share,
Trash2,
MoreHorizontal,
};
interface ChatAction {
name: string;
icon: string;
onClick: () => void;
name: string;
icon: string;
onClick: () => void;
}
export function NavProjects({
chats,
chats,
}: {
chats: {
name: string
url: string
icon: LucideIcon
id?: number
search_space_id?: number
actions?: ChatAction[]
}[]
chats: {
name: string;
url: string;
icon: LucideIcon;
id?: number;
search_space_id?: number;
actions?: ChatAction[];
}[];
}) {
const { isMobile } = useSidebar()
const router = useRouter()
const searchSpaceId = chats[0]?.search_space_id || ""
const { isMobile } = useSidebar();
const router = useRouter();
return (
<SidebarGroup className="group-data-[collapsible=icon]:hidden">
<SidebarGroupLabel>Recent Chats</SidebarGroupLabel>
<SidebarMenu>
{chats.map((item, index) => (
<SidebarMenuItem key={item.id ? `chat-${item.id}` : `chat-${item.name}-${index}`}>
<SidebarMenuButton>
<item.icon />
<span>{item.name}</span>
</SidebarMenuButton>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<SidebarMenuAction showOnHover>
<MoreHorizontal />
<span className="sr-only">More</span>
</SidebarMenuAction>
</DropdownMenuTrigger>
<DropdownMenuContent
className="w-48"
side={isMobile ? "bottom" : "right"}
align={isMobile ? "end" : "start"}
>
{item.actions ? (
// Use the actions provided by the item
item.actions.map((action, actionIndex) => {
const ActionIcon = actionIconMap[action.icon] || Folder;
return (
<DropdownMenuItem key={`${action.name}-${actionIndex}`} onClick={action.onClick}>
<ActionIcon className="text-muted-foreground" />
<span>{action.name}</span>
</DropdownMenuItem>
);
})
) : (
// Default actions if none provided
<>
<DropdownMenuItem>
<Folder className="text-muted-foreground" />
<span>View Chat</span>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem>
<Trash2 className="text-muted-foreground" />
<span>Delete Chat</span>
</DropdownMenuItem>
</>
)}
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
))}
<SidebarMenuItem>
<SidebarMenuButton onClick={() => router.push(`/dashboard/${searchSpaceId}/chats`)}>
<MoreHorizontal />
<span>View All Chats</span>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarGroup>
)
const searchSpaceId = chats[0]?.search_space_id || "";
return (
<SidebarGroup className="group-data-[collapsible=icon]:hidden">
<SidebarGroupLabel>Recent Chats</SidebarGroupLabel>
<SidebarMenu>
{chats.map((item, index) => (
<SidebarMenuItem key={item.id ? `chat-${item.id}` : `chat-${item.name}-${index}`}>
<SidebarMenuButton>
<item.icon />
<span>{item.name}</span>
</SidebarMenuButton>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<SidebarMenuAction showOnHover>
<MoreHorizontal />
<span className="sr-only">More</span>
</SidebarMenuAction>
</DropdownMenuTrigger>
<DropdownMenuContent
className="w-48"
side={isMobile ? "bottom" : "right"}
align={isMobile ? "end" : "start"}
>
{item.actions ? (
// Use the actions provided by the item
item.actions.map((action, actionIndex) => {
const ActionIcon = actionIconMap[action.icon] || Folder;
return (
<DropdownMenuItem
key={`${action.name}-${actionIndex}`}
onClick={action.onClick}
>
<ActionIcon className="text-muted-foreground" />
<span>{action.name}</span>
</DropdownMenuItem>
);
})
) : (
// Default actions if none provided
<>
<DropdownMenuItem>
<Folder className="text-muted-foreground" />
<span>View Chat</span>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem>
<Trash2 className="text-muted-foreground" />
<span>Delete Chat</span>
</DropdownMenuItem>
</>
)}
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
))}
<SidebarMenuItem>
<SidebarMenuButton onClick={() => router.push(`/dashboard/${searchSpaceId}/chats`)}>
<MoreHorizontal />
<span>View All Chats</span>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarGroup>
);
}

View file

@ -1,42 +1,41 @@
"use client"
"use client";
import * as React from "react"
import { type LucideIcon } from "lucide-react"
import type * as React from "react";
import type { LucideIcon } from "lucide-react";
import {
SidebarGroup,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
SidebarGroupLabel,
} from "@/components/ui/sidebar"
SidebarGroup,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
SidebarGroupLabel,
} from "@/components/ui/sidebar";
export function NavSecondary({
items,
...props
items,
...props
}: {
items: {
title: string
url: string
icon: LucideIcon
}[]
items: {
title: string;
url: string;
icon: LucideIcon;
}[];
} & React.ComponentPropsWithoutRef<typeof SidebarGroup>) {
return (
<SidebarGroup {...props}>
<SidebarGroupLabel>SearchSpace</SidebarGroupLabel>
<SidebarMenu>
{items.map((item, index) => (
<SidebarMenuItem key={`${item.title}-${index}`}>
<SidebarMenuButton asChild size="sm">
<a href={item.url}>
<item.icon />
<span>{item.title}</span>
</a>
</SidebarMenuButton>
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroup>
)
return (
<SidebarGroup {...props}>
<SidebarGroupLabel>SearchSpace</SidebarGroupLabel>
<SidebarMenu>
{items.map((item, index) => (
<SidebarMenuItem key={`${item.title}-${index}`}>
<SidebarMenuButton asChild size="sm">
<a href={item.url}>
<item.icon />
<span>{item.title}</span>
</a>
</SidebarMenuButton>
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroup>
);
}

View file

@ -1,110 +1,103 @@
"use client"
"use client";
import {
BadgeCheck,
ChevronsUpDown,
LogOut,
Settings,
} from "lucide-react"
import { BadgeCheck, ChevronsUpDown, LogOut, Settings } from "lucide-react";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import {
Avatar,
AvatarFallback,
AvatarImage,
} from "@/components/ui/avatar"
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import {
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
useSidebar,
} from "@/components/ui/sidebar"
import { useRouter, useParams } from "next/navigation"
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
useSidebar,
} from "@/components/ui/sidebar";
import { useRouter, useParams } from "next/navigation";
export function NavUser({
user,
user,
}: {
user: {
name: string
email: string
avatar: string
}
user: {
name: string;
email: string;
avatar: string;
};
}) {
const { isMobile } = useSidebar()
const router = useRouter()
const { search_space_id } = useParams()
const { isMobile } = useSidebar();
const router = useRouter();
const { search_space_id } = useParams();
const handleLogout = () => {
if (typeof window !== 'undefined') {
localStorage.removeItem('surfsense_bearer_token');
router.push('/');
}
};
return (
<SidebarMenu>
<SidebarMenuItem>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<SidebarMenuButton
size="lg"
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
>
<Avatar className="h-8 w-8 rounded-lg">
<AvatarImage src={user.avatar} alt={user.name} />
<AvatarFallback className="rounded-lg">CN</AvatarFallback>
</Avatar>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-medium">{user.name}</span>
<span className="truncate text-xs">{user.email}</span>
</div>
<ChevronsUpDown className="ml-auto size-4" />
</SidebarMenuButton>
</DropdownMenuTrigger>
<DropdownMenuContent
className="w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg"
side={isMobile ? "bottom" : "right"}
align="end"
sideOffset={4}
>
<DropdownMenuLabel className="p-0 font-normal">
<div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
<Avatar className="h-8 w-8 rounded-lg">
<AvatarImage src={user.avatar} alt={user.name} />
<AvatarFallback className="rounded-lg">CN</AvatarFallback>
</Avatar>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-medium">{user.name}</span>
<span className="truncate text-xs">{user.email}</span>
</div>
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem onClick={() => router.push(`/dashboard/${search_space_id}/api-key`)}>
<BadgeCheck />
API Key
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => router.push(`/settings`)}>
<Settings />
Settings
</DropdownMenuItem>
<DropdownMenuItem onClick={handleLogout}>
<LogOut />
Log out
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
</SidebarMenu>
)
const handleLogout = () => {
if (typeof window !== "undefined") {
localStorage.removeItem("surfsense_bearer_token");
router.push("/");
}
};
return (
<SidebarMenu>
<SidebarMenuItem>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<SidebarMenuButton
size="lg"
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
>
<Avatar className="h-8 w-8 rounded-lg">
<AvatarImage src={user.avatar} alt={user.name} />
<AvatarFallback className="rounded-lg">CN</AvatarFallback>
</Avatar>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-medium">{user.name}</span>
<span className="truncate text-xs">{user.email}</span>
</div>
<ChevronsUpDown className="ml-auto size-4" />
</SidebarMenuButton>
</DropdownMenuTrigger>
<DropdownMenuContent
className="w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg"
side={isMobile ? "bottom" : "right"}
align="end"
sideOffset={4}
>
<DropdownMenuLabel className="p-0 font-normal">
<div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
<Avatar className="h-8 w-8 rounded-lg">
<AvatarImage src={user.avatar} alt={user.name} />
<AvatarFallback className="rounded-lg">CN</AvatarFallback>
</Avatar>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-medium">{user.name}</span>
<span className="truncate text-xs">{user.email}</span>
</div>
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem
onClick={() => router.push(`/dashboard/${search_space_id}/api-key`)}
>
<BadgeCheck />
API Key
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => router.push(`/settings`)}>
<Settings />
Settings
</DropdownMenuItem>
<DropdownMenuItem onClick={handleLogout}>
<LogOut />
Log out
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
</SidebarMenu>
);
}